From 23d8188de2d3a5f84a54f6224c6dd06497d7d536 Mon Sep 17 00:00:00 2001 From: Dima-1 Date: Sat, 7 Mar 2026 11:11:30 +0200 Subject: [PATCH 01/24] Get group visibility info from gpx --- map/src/infoblock/components/tabs/WaypointsTab.jsx | 6 ++++-- map/src/map/util/TrackLayerProvider.js | 6 +++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/map/src/infoblock/components/tabs/WaypointsTab.jsx b/map/src/infoblock/components/tabs/WaypointsTab.jsx index 915cdea4b..440043fef 100644 --- a/map/src/infoblock/components/tabs/WaypointsTab.jsx +++ b/map/src/infoblock/components/tabs/WaypointsTab.jsx @@ -11,11 +11,11 @@ import { createPoiIcon } from '../../../map/markers/MarkerOptions'; import isEmpty from 'lodash-es/isEmpty'; // distinct component -const WaypointGroup = ({ ctx, group, points, defaultOpen, massOpen, massVisible }) => { +const WaypointGroup = ({ ctx, group, points, defaultOpen, defaultVisible = true, massOpen, massVisible }) => { const [open, setOpen] = useState(defaultOpen); const switchOpen = () => setOpen(!open); - const [visible, setVisible] = useState(true); + const [visible, setVisible] = useState(defaultVisible); const switchVisible = (e) => { e.stopPropagation(); setVisible(!visible); @@ -317,6 +317,7 @@ export default function WaypointsTab() { const groups = getSortedGroups(); const keys = Object.keys(groups); const trackName = ctx.selectedGpxFile.name; + const pointsGroups = ctx.selectedGpxFile.pointsGroups; setShowMass(keys.length > 1); @@ -329,6 +330,7 @@ export default function WaypointsTab() { group={g} points={groups[g]} defaultOpen={keys.length === 1} + defaultVisible={pointsGroups[g]?.ext?.hidden !== true} massVisible={massVisible} massOpen={massOpen} /> diff --git a/map/src/map/util/TrackLayerProvider.js b/map/src/map/util/TrackLayerProvider.js index bb3925a80..789419906 100644 --- a/map/src/map/util/TrackLayerProvider.js +++ b/map/src/map/util/TrackLayerProvider.js @@ -63,6 +63,7 @@ function createLayersByTrackData({ data, ctx, map, groupId, type = GPX_FILE_TYPE if (layers.length > 0) { let layersGroup = new L.FeatureGroup(layers); layersGroup.options.type = type; + layersGroup.options.pointsGroups = data?.pointsGroups; return layersGroup; } } @@ -720,10 +721,13 @@ function createEditableTempLPolyline(start, end, map, ctx) { export function redrawWptsOnLayer({ layer }) { if (layer) { + const pointsGroups = layer.options?.pointsGroups; layer.getLayers().forEach((l) => { if (l instanceof L.Marker && l.options?.wpt) { if (l._icon?.style) { - l._icon.style.display = null; // visible + const category = l.options?.category || ''; + const isHidden = pointsGroups?.[category]?.ext?.hidden === true; + l._icon.style.display = isHidden ? 'none' : null; } } }); From 60d11d4fae9f0d03ec6cc362f603f90ceb7b7891 Mon Sep 17 00:00:00 2001 From: Dima-1 Date: Thu, 12 Mar 2026 09:49:56 +0200 Subject: [PATCH 02/24] Save point group appearance to the info file --- .../components/tabs/WaypointsTab.jsx | 53 +++++++++++++++++-- map/src/manager/track/SaveTrackManager.js | 35 ++++++++++++ map/src/manager/track/TracksManager.js | 3 +- map/src/menu/settings/CloudSettings.jsx | 3 +- 4 files changed, 88 insertions(+), 6 deletions(-) diff --git a/map/src/infoblock/components/tabs/WaypointsTab.jsx b/map/src/infoblock/components/tabs/WaypointsTab.jsx index 440043fef..a3536cd89 100644 --- a/map/src/infoblock/components/tabs/WaypointsTab.jsx +++ b/map/src/infoblock/components/tabs/WaypointsTab.jsx @@ -1,5 +1,5 @@ -import { useContext, useEffect, useMemo, useState } from 'react'; -import AppContext, { isLocalTrack } from '../../../context/AppContext'; +import { useContext, useEffect, useMemo, useState, useRef } from 'react'; +import AppContext, { isLocalTrack, isCloudTrack } from '../../../context/AppContext'; import { Alert, Box, Button, Collapse, Grid, IconButton, MenuItem, Switch, Tooltip, Typography } from '@mui/material'; import L from 'leaflet'; import { Cancel, ExpandLess, ExpandMore, KeyboardDoubleArrowDown, KeyboardDoubleArrowUp } from '@mui/icons-material'; @@ -9,6 +9,41 @@ import { confirm } from '../../../dialogs/GlobalConfirmationDialog'; import { useWindowSize } from '../../../util/hooks/useWindowSize'; import { createPoiIcon } from '../../../map/markers/MarkerOptions'; import isEmpty from 'lodash-es/isEmpty'; +import { debouncer } from '../../../context/TracksRoutingCache'; +import { saveInfoFile } from '../../../manager/track/SaveTrackManager'; + +const SAVE_DEBOUNCE_MS = 1000; + +function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) { + const updatedPointsGroups = { ...ctx.selectedGpxFile.pointsGroups }; + + groupNames.forEach((groupName) => { + updatedPointsGroups[groupName] = { + ...updatedPointsGroups[groupName], + ext: { + ...updatedPointsGroups[groupName]?.ext, + hidden, + }, + }; + }); + + const updatedFile = { + ...ctx.selectedGpxFile, + pointsGroups: updatedPointsGroups, + }; + + ctx.setSelectedGpxFile(updatedFile); + + if (isCloudTrack(ctx)) { + debouncer( + () => { + saveInfoFile(updatedFile); + }, + debouncerTimer, + SAVE_DEBOUNCE_MS + ); + } +} // distinct component const WaypointGroup = ({ ctx, group, points, defaultOpen, defaultVisible = true, massOpen, massVisible }) => { @@ -16,9 +51,13 @@ const WaypointGroup = ({ ctx, group, points, defaultOpen, defaultVisible = true, const switchOpen = () => setOpen(!open); const [visible, setVisible] = useState(defaultVisible); + const debouncerTimer = useRef(null); + const switchVisible = (e) => { e.stopPropagation(); - setVisible(!visible); + const newVisible = !visible; + setVisible(newVisible); + updateGroupsVisibility(ctx, [group], !newVisible, debouncerTimer); }; const [mounted, setMounted] = useState(false); @@ -302,9 +341,15 @@ export default function WaypointsTab() { const [showMass, setShowMass] = useState(false); const [massOpen, setMassOpen] = useState(false); const [massVisible, setMassVisible] = useState(true); + const debouncerTimer = useRef(null); const switchMassOpen = () => setMassOpen(!massOpen); - const switchMassVisible = () => setMassVisible(!massVisible); + const switchMassVisible = () => { + const newMassVisible = !massVisible; + setMassVisible(newMassVisible); + const groupNames = Object.keys(ctx.selectedGpxFile.pointsGroups || {}); + updateGroupsVisibility(ctx, groupNames, !newMassVisible, debouncerTimer); + }; const pointsChangedString = useMemo(() => { const name = ctx.selectedGpxFile.name; diff --git a/map/src/manager/track/SaveTrackManager.js b/map/src/manager/track/SaveTrackManager.js index 9ead3eb84..5106b2993 100644 --- a/map/src/manager/track/SaveTrackManager.js +++ b/map/src/manager/track/SaveTrackManager.js @@ -14,6 +14,7 @@ import TracksManager, { preparedGpxFile, GPX_FILE_EXT, KMZ_FILE_EXT, + INFO_FILE_EXT, } from './TracksManager'; import cloneDeep from 'lodash-es/cloneDeep'; import isEmpty from 'lodash-es/isEmpty'; @@ -325,6 +326,40 @@ export async function saveEmptyTrack(folderName, ctx) { } } +export async function saveInfoFile(selectedGpxFile) { + const cleanedPointsGroups = {}; + Object.keys(selectedGpxFile.pointsGroups).forEach((groupName) => { + const { points, ...groupWithoutPoints } = selectedGpxFile.pointsGroups[groupName]; + if (groupWithoutPoints.ext?.points) { + const { points: extPoints, ...extWithoutPoints } = groupWithoutPoints.ext; + groupWithoutPoints.ext = extWithoutPoints; + } + cleanedPointsGroups[groupName] = groupWithoutPoints; + }); + const infoData = { + ...selectedGpxFile.info, + pointsGroups: cleanedPointsGroups, + }; + + const jsonString = JSON.stringify(infoData); + const convertedData = new TextEncoder().encode(jsonString); + const zippedResult = require('pako').gzip(convertedData, { to: 'Uint8Array' }); + const convertedZipped = zippedResult.buffer; + const oMyBlob = new Blob([convertedZipped], { type: 'application/json' }); + const data = new FormData(); + const infoFileName = selectedGpxFile.name + INFO_FILE_EXT; + data.append('file', oMyBlob, infoFileName); + + const params = { + type: 'GPX', + name: infoFileName, + }; + + const res = await apiPost(`${process.env.REACT_APP_USER_API_SITE}/mapapi/upload-file`, data, { params }); + + return res?.data?.status === 'ok'; +} + export async function refreshGlobalFiles({ ctx, oldName = null, diff --git a/map/src/manager/track/TracksManager.js b/map/src/manager/track/TracksManager.js index 71356366f..313017c20 100644 --- a/map/src/manager/track/TracksManager.js +++ b/map/src/manager/track/TracksManager.js @@ -28,6 +28,7 @@ import { compressJSONToBlob } from '../../util/GzipCompression'; export const GPX_FILE_TYPE = 'GPX'; export const GPX_FILE_EXT = '.gpx'; +export const INFO_FILE_EXT = '.info'; export const KMZ_FILE_EXT = '.kmz'; export const EMPTY_FILE_NAME = '__folder__.info'; const GET_SRTM_DATA = 'get-srtm-data'; @@ -1539,7 +1540,7 @@ export async function openTrackOnMap({ export function preparedGpxFile({ file, sharedFile = false, oldFile = null }) { const URL = `${process.env.REACT_APP_USER_API_SITE}/mapapi/download-file`; const qs = `?type=${encodeURIComponent(file.type)}&name=${encodeURIComponent(file.name)}&shared=${sharedFile ? 'true' : 'false'}`; - const qsInfo = `?type=${encodeURIComponent(file.type)}&name=${encodeURIComponent(file.name + '.info')}`; + const qsInfo = `?type=${encodeURIComponent(file.type)}&name=${encodeURIComponent(file.name + INFO_FILE_EXT)}`; if (oldFile) { return { url: oldFile.url ? URL + qs : null, diff --git a/map/src/menu/settings/CloudSettings.jsx b/map/src/menu/settings/CloudSettings.jsx index 193fa2cb3..2a0e62168 100644 --- a/map/src/menu/settings/CloudSettings.jsx +++ b/map/src/menu/settings/CloudSettings.jsx @@ -5,6 +5,7 @@ import { apiGet } from '../../util/HttpApi'; import devList from '../../resources/apple_device_model_list.json'; import AppContext from '../../context/AppContext'; import { fmt } from '../../util/dateFmt'; +import { INFO_FILE_EXT } from '../../manager/track/TracksManager'; export default function CloudSettings({ setOpenCloudSettings }) { const ctx = useContext(AppContext); @@ -23,7 +24,7 @@ export default function CloudSettings({ setOpenCloudSettings }) { }, }); if (response.ok) { - const res = response.data.allFiles.filter((f) => !f.name.toLowerCase().endsWith('.info')); + const res = response.data.allFiles.filter((f) => !f.name.toLowerCase().endsWith(INFO_FILE_EXT)); setFilesLoading(false); preparedDevices(res); setAllFilesVersions(res); From 897e7a322e3124fcc684e2592058bd74fa80f9d9 Mon Sep 17 00:00:00 2001 From: Dima-1 Date: Thu, 12 Mar 2026 13:57:19 +0200 Subject: [PATCH 03/24] Fix review --- .../components/tabs/WaypointsTab.jsx | 31 +++++++++++-------- map/src/manager/track/SaveTrackManager.js | 8 +++-- map/src/manager/track/TracksManager.js | 2 +- map/src/map/util/TrackLayerProvider.js | 5 +-- 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/map/src/infoblock/components/tabs/WaypointsTab.jsx b/map/src/infoblock/components/tabs/WaypointsTab.jsx index a3536cd89..8b0c58f3c 100644 --- a/map/src/infoblock/components/tabs/WaypointsTab.jsx +++ b/map/src/infoblock/components/tabs/WaypointsTab.jsx @@ -12,24 +12,29 @@ import isEmpty from 'lodash-es/isEmpty'; import { debouncer } from '../../../context/TracksRoutingCache'; import { saveInfoFile } from '../../../manager/track/SaveTrackManager'; -const SAVE_DEBOUNCE_MS = 1000; - function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) { - const updatedPointsGroups = { ...ctx.selectedGpxFile.pointsGroups }; + const prevFile = ctx.selectedGpxFile; + const updatedPointsGroups = { ...prevFile?.info?.pointsGroups }; + const allGroupNames = groupNames || Object.keys(updatedPointsGroups || {}); + + allGroupNames.forEach((groupName) => { + const group = updatedPointsGroups[groupName] || {}; - groupNames.forEach((groupName) => { updatedPointsGroups[groupName] = { - ...updatedPointsGroups[groupName], + ...group, ext: { - ...updatedPointsGroups[groupName]?.ext, + ...group.ext, hidden, }, }; }); const updatedFile = { - ...ctx.selectedGpxFile, - pointsGroups: updatedPointsGroups, + ...prevFile, + info: { + ...prevFile.info, + pointsGroups: updatedPointsGroups, + } }; ctx.setSelectedGpxFile(updatedFile); @@ -40,18 +45,17 @@ function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) { saveInfoFile(updatedFile); }, debouncerTimer, - SAVE_DEBOUNCE_MS + 500 ); } } // distinct component -const WaypointGroup = ({ ctx, group, points, defaultOpen, defaultVisible = true, massOpen, massVisible }) => { +const WaypointGroup = ({ ctx, group, points, defaultOpen, defaultVisible = true, massOpen, massVisible, debouncerTimer }) => { const [open, setOpen] = useState(defaultOpen); const switchOpen = () => setOpen(!open); const [visible, setVisible] = useState(defaultVisible); - const debouncerTimer = useRef(null); const switchVisible = (e) => { e.stopPropagation(); @@ -362,7 +366,7 @@ export default function WaypointsTab() { const groups = getSortedGroups(); const keys = Object.keys(groups); const trackName = ctx.selectedGpxFile.name; - const pointsGroups = ctx.selectedGpxFile.pointsGroups; + const pointsGroups = ctx.selectedGpxFile.info?.pointsGroups ?? ctx.selectedGpxFile.pointsGroups; setShowMass(keys.length > 1); @@ -375,9 +379,10 @@ export default function WaypointsTab() { group={g} points={groups[g]} defaultOpen={keys.length === 1} - defaultVisible={pointsGroups[g]?.ext?.hidden !== true} + defaultVisible={pointsGroups?.[g]?.ext?.hidden !== true} massVisible={massVisible} massOpen={massOpen} + debouncerTimer={debouncerTimer} /> ))} diff --git a/map/src/manager/track/SaveTrackManager.js b/map/src/manager/track/SaveTrackManager.js index 5106b2993..b8ed868c9 100644 --- a/map/src/manager/track/SaveTrackManager.js +++ b/map/src/manager/track/SaveTrackManager.js @@ -327,9 +327,13 @@ export async function saveEmptyTrack(folderName, ctx) { } export async function saveInfoFile(selectedGpxFile) { + const pointsGroups = selectedGpxFile?.info?.pointsGroups; + if (!pointsGroups) { + return; + } const cleanedPointsGroups = {}; - Object.keys(selectedGpxFile.pointsGroups).forEach((groupName) => { - const { points, ...groupWithoutPoints } = selectedGpxFile.pointsGroups[groupName]; + Object.keys(pointsGroups).forEach((groupName) => { + const { points, ...groupWithoutPoints } = pointsGroups[groupName]; if (groupWithoutPoints.ext?.points) { const { points: extPoints, ...extWithoutPoints } = groupWithoutPoints.ext; groupWithoutPoints.ext = extWithoutPoints; diff --git a/map/src/manager/track/TracksManager.js b/map/src/manager/track/TracksManager.js index 313017c20..c460c9ec4 100644 --- a/map/src/manager/track/TracksManager.js +++ b/map/src/manager/track/TracksManager.js @@ -30,7 +30,7 @@ export const GPX_FILE_TYPE = 'GPX'; export const GPX_FILE_EXT = '.gpx'; export const INFO_FILE_EXT = '.info'; export const KMZ_FILE_EXT = '.kmz'; -export const EMPTY_FILE_NAME = '__folder__.info'; +export const EMPTY_FILE_NAME = `__folder__${INFO_FILE_EXT}`; const GET_SRTM_DATA = 'get-srtm-data'; const GET_ANALYSIS = 'get-analysis'; export const PROFILE_LINE = 'line'; diff --git a/map/src/map/util/TrackLayerProvider.js b/map/src/map/util/TrackLayerProvider.js index 789419906..ad76f36ec 100644 --- a/map/src/map/util/TrackLayerProvider.js +++ b/map/src/map/util/TrackLayerProvider.js @@ -63,7 +63,8 @@ function createLayersByTrackData({ data, ctx, map, groupId, type = GPX_FILE_TYPE if (layers.length > 0) { let layersGroup = new L.FeatureGroup(layers); layersGroup.options.type = type; - layersGroup.options.pointsGroups = data?.pointsGroups; + const pointsGroups = data?.info?.pointsGroups ?? data?.pointsGroups; + layersGroup.options.pointsGroups = pointsGroups; return layersGroup; } } @@ -488,7 +489,7 @@ function parseWpt({ if (point.name) { opt.name = point.name; } - opt.category = point.category ? point.category : 'favorites'; + opt.category = point.category ? point.category : ''; opt.groupId = groupId; if (point.desc) { opt.desc = point.desc; From a58caf71340c894868ebc52168ab021c8b9e87b8 Mon Sep 17 00:00:00 2001 From: Dima-1 Date: Thu, 12 Mar 2026 14:19:31 +0200 Subject: [PATCH 04/24] Move saveInfoFile to separate file --- .../components/tabs/WaypointsTab.jsx | 2 +- map/src/manager/track/SaveTrackManager.js | 39 --------------- .../manager/track/TrackAppearanceManager.js | 47 +++++++++++++++++++ 3 files changed, 48 insertions(+), 40 deletions(-) create mode 100644 map/src/manager/track/TrackAppearanceManager.js diff --git a/map/src/infoblock/components/tabs/WaypointsTab.jsx b/map/src/infoblock/components/tabs/WaypointsTab.jsx index 8b0c58f3c..216244015 100644 --- a/map/src/infoblock/components/tabs/WaypointsTab.jsx +++ b/map/src/infoblock/components/tabs/WaypointsTab.jsx @@ -10,7 +10,7 @@ import { useWindowSize } from '../../../util/hooks/useWindowSize'; import { createPoiIcon } from '../../../map/markers/MarkerOptions'; import isEmpty from 'lodash-es/isEmpty'; import { debouncer } from '../../../context/TracksRoutingCache'; -import { saveInfoFile } from '../../../manager/track/SaveTrackManager'; +import { saveInfoFile } from '../../../manager/track/TrackAppearanceManager'; function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) { const prevFile = ctx.selectedGpxFile; diff --git a/map/src/manager/track/SaveTrackManager.js b/map/src/manager/track/SaveTrackManager.js index b8ed868c9..9ead3eb84 100644 --- a/map/src/manager/track/SaveTrackManager.js +++ b/map/src/manager/track/SaveTrackManager.js @@ -14,7 +14,6 @@ import TracksManager, { preparedGpxFile, GPX_FILE_EXT, KMZ_FILE_EXT, - INFO_FILE_EXT, } from './TracksManager'; import cloneDeep from 'lodash-es/cloneDeep'; import isEmpty from 'lodash-es/isEmpty'; @@ -326,44 +325,6 @@ export async function saveEmptyTrack(folderName, ctx) { } } -export async function saveInfoFile(selectedGpxFile) { - const pointsGroups = selectedGpxFile?.info?.pointsGroups; - if (!pointsGroups) { - return; - } - const cleanedPointsGroups = {}; - Object.keys(pointsGroups).forEach((groupName) => { - const { points, ...groupWithoutPoints } = pointsGroups[groupName]; - if (groupWithoutPoints.ext?.points) { - const { points: extPoints, ...extWithoutPoints } = groupWithoutPoints.ext; - groupWithoutPoints.ext = extWithoutPoints; - } - cleanedPointsGroups[groupName] = groupWithoutPoints; - }); - const infoData = { - ...selectedGpxFile.info, - pointsGroups: cleanedPointsGroups, - }; - - const jsonString = JSON.stringify(infoData); - const convertedData = new TextEncoder().encode(jsonString); - const zippedResult = require('pako').gzip(convertedData, { to: 'Uint8Array' }); - const convertedZipped = zippedResult.buffer; - const oMyBlob = new Blob([convertedZipped], { type: 'application/json' }); - const data = new FormData(); - const infoFileName = selectedGpxFile.name + INFO_FILE_EXT; - data.append('file', oMyBlob, infoFileName); - - const params = { - type: 'GPX', - name: infoFileName, - }; - - const res = await apiPost(`${process.env.REACT_APP_USER_API_SITE}/mapapi/upload-file`, data, { params }); - - return res?.data?.status === 'ok'; -} - export async function refreshGlobalFiles({ ctx, oldName = null, diff --git a/map/src/manager/track/TrackAppearanceManager.js b/map/src/manager/track/TrackAppearanceManager.js new file mode 100644 index 000000000..9a828e1b8 --- /dev/null +++ b/map/src/manager/track/TrackAppearanceManager.js @@ -0,0 +1,47 @@ +import { apiPost } from '../../util/HttpApi'; +import { INFO_FILE_EXT } from './TracksManager'; + +export async function saveInfoFile(selectedGpxFile) { + const pointsGroups = selectedGpxFile?.info?.pointsGroups; + if (!pointsGroups) { + return; + } + + const cleanedPointsGroups = {}; + + Object.keys(pointsGroups).forEach((groupName) => { + const { points, ...groupWithoutPoints } = pointsGroups[groupName]; + + if (groupWithoutPoints.ext?.points) { + const { points: extPoints, ...extWithoutPoints } = groupWithoutPoints.ext; + groupWithoutPoints.ext = extWithoutPoints; + } + + cleanedPointsGroups[groupName] = groupWithoutPoints; + }); + + const infoData = { + ...selectedGpxFile.info, + pointsGroups: cleanedPointsGroups, + }; + + const jsonString = JSON.stringify(infoData); + const convertedData = new TextEncoder().encode(jsonString); + const zippedResult = require('pako').gzip(convertedData, { to: 'Uint8Array' }); + const convertedZipped = zippedResult.buffer; + const oMyBlob = new Blob([convertedZipped], { type: 'application/json' }); + + const data = new FormData(); + const infoFileName = selectedGpxFile.name + INFO_FILE_EXT; + data.append('file', oMyBlob, infoFileName); + + const params = { + type: 'GPX', + name: infoFileName, + }; + + const res = await apiPost(`${process.env.REACT_APP_USER_API_SITE}/mapapi/upload-file`, data, { params }); + + return res?.data?.status === 'ok'; +} + From ec3e7fe657fd4fc9c4bf9f3565258b1894098342 Mon Sep 17 00:00:00 2001 From: Dima-1 Date: Fri, 13 Mar 2026 18:55:07 +0200 Subject: [PATCH 05/24] Update appearance differences only --- .../components/tabs/WaypointsTab.jsx | 20 +++++-- .../manager/track/TrackAppearanceManager.js | 52 +++++-------------- 2 files changed, 30 insertions(+), 42 deletions(-) diff --git a/map/src/infoblock/components/tabs/WaypointsTab.jsx b/map/src/infoblock/components/tabs/WaypointsTab.jsx index 216244015..f75648f8c 100644 --- a/map/src/infoblock/components/tabs/WaypointsTab.jsx +++ b/map/src/infoblock/components/tabs/WaypointsTab.jsx @@ -10,7 +10,7 @@ import { useWindowSize } from '../../../util/hooks/useWindowSize'; import { createPoiIcon } from '../../../map/markers/MarkerOptions'; import isEmpty from 'lodash-es/isEmpty'; import { debouncer } from '../../../context/TracksRoutingCache'; -import { saveInfoFile } from '../../../manager/track/TrackAppearanceManager'; +import { updateInfoFile } from '../../../manager/track/TrackAppearanceManager'; function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) { const prevFile = ctx.selectedGpxFile; @@ -34,7 +34,7 @@ function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) { info: { ...prevFile.info, pointsGroups: updatedPointsGroups, - } + }, }; ctx.setSelectedGpxFile(updatedFile); @@ -42,7 +42,10 @@ function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) { if (isCloudTrack(ctx)) { debouncer( () => { - saveInfoFile(updatedFile); + const diff = { + pointsGroups: Object.fromEntries(allGroupNames.map((name) => [name, { ext: { hidden } }])), + }; + updateInfoFile(updatedFile, diff); }, debouncerTimer, 500 @@ -51,7 +54,16 @@ function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) { } // distinct component -const WaypointGroup = ({ ctx, group, points, defaultOpen, defaultVisible = true, massOpen, massVisible, debouncerTimer }) => { +const WaypointGroup = ({ + ctx, + group, + points, + defaultOpen, + defaultVisible = true, + massOpen, + massVisible, + debouncerTimer, +}) => { const [open, setOpen] = useState(defaultOpen); const switchOpen = () => setOpen(!open); diff --git a/map/src/manager/track/TrackAppearanceManager.js b/map/src/manager/track/TrackAppearanceManager.js index 9a828e1b8..7378b7200 100644 --- a/map/src/manager/track/TrackAppearanceManager.js +++ b/map/src/manager/track/TrackAppearanceManager.js @@ -1,47 +1,23 @@ import { apiPost } from '../../util/HttpApi'; import { INFO_FILE_EXT } from './TracksManager'; -export async function saveInfoFile(selectedGpxFile) { - const pointsGroups = selectedGpxFile?.info?.pointsGroups; - if (!pointsGroups) { - return; +/** + * Update .info file with partial data (only changed fields). + * + * @param {Object} updatedInfoFile - The GPX file object + * @param {Object} diff - Partial info object with only changed fields + * @returns {Promise} Success status + */ +export async function updateInfoFile(updatedInfoFile, diff) { + if (!updatedInfoFile?.name || !diff || Object.keys(diff).length === 0) { + return false; } - const cleanedPointsGroups = {}; - - Object.keys(pointsGroups).forEach((groupName) => { - const { points, ...groupWithoutPoints } = pointsGroups[groupName]; - - if (groupWithoutPoints.ext?.points) { - const { points: extPoints, ...extWithoutPoints } = groupWithoutPoints.ext; - groupWithoutPoints.ext = extWithoutPoints; - } - - cleanedPointsGroups[groupName] = groupWithoutPoints; + const res = await apiPost(`${process.env.REACT_APP_USER_API_SITE}/mapapi/update-info`, diff, { + params: { + name: updatedInfoFile.name + INFO_FILE_EXT, + }, }); - const infoData = { - ...selectedGpxFile.info, - pointsGroups: cleanedPointsGroups, - }; - - const jsonString = JSON.stringify(infoData); - const convertedData = new TextEncoder().encode(jsonString); - const zippedResult = require('pako').gzip(convertedData, { to: 'Uint8Array' }); - const convertedZipped = zippedResult.buffer; - const oMyBlob = new Blob([convertedZipped], { type: 'application/json' }); - - const data = new FormData(); - const infoFileName = selectedGpxFile.name + INFO_FILE_EXT; - data.append('file', oMyBlob, infoFileName); - - const params = { - type: 'GPX', - name: infoFileName, - }; - - const res = await apiPost(`${process.env.REACT_APP_USER_API_SITE}/mapapi/upload-file`, data, { params }); - return res?.data?.status === 'ok'; } - From b4310150fca4851180a1c8401fc34dbbaf354a9b Mon Sep 17 00:00:00 2001 From: Dima-1 Date: Mon, 16 Mar 2026 10:45:00 +0200 Subject: [PATCH 06/24] Update comments, add empty fallback --- map/src/infoblock/components/tabs/WaypointsTab.jsx | 2 +- map/src/manager/track/TrackAppearanceManager.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/map/src/infoblock/components/tabs/WaypointsTab.jsx b/map/src/infoblock/components/tabs/WaypointsTab.jsx index f75648f8c..040d3865e 100644 --- a/map/src/infoblock/components/tabs/WaypointsTab.jsx +++ b/map/src/infoblock/components/tabs/WaypointsTab.jsx @@ -14,7 +14,7 @@ import { updateInfoFile } from '../../../manager/track/TrackAppearanceManager'; function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) { const prevFile = ctx.selectedGpxFile; - const updatedPointsGroups = { ...prevFile?.info?.pointsGroups }; + const updatedPointsGroups = { ...(prevFile?.info?.pointsGroups || {}) }; const allGroupNames = groupNames || Object.keys(updatedPointsGroups || {}); allGroupNames.forEach((groupName) => { diff --git a/map/src/manager/track/TrackAppearanceManager.js b/map/src/manager/track/TrackAppearanceManager.js index 7378b7200..59459875a 100644 --- a/map/src/manager/track/TrackAppearanceManager.js +++ b/map/src/manager/track/TrackAppearanceManager.js @@ -2,9 +2,9 @@ import { apiPost } from '../../util/HttpApi'; import { INFO_FILE_EXT } from './TracksManager'; /** - * Update .info file with partial data (only changed fields). + * Update track appearance in .info file with partial data (only changed fields). * - * @param {Object} updatedInfoFile - The GPX file object + * @param {Object} updatedInfoFile - The info file object * @param {Object} diff - Partial info object with only changed fields * @returns {Promise} Success status */ From 235733eb4e295258ec123afa6a471e65e1e4ed39 Mon Sep 17 00:00:00 2001 From: Dima-1 Date: Wed, 18 Mar 2026 10:01:32 +0200 Subject: [PATCH 07/24] Update entire info file instead the diff --- .../components/tabs/WaypointsTab.jsx | 9 +++----- .../manager/track/TrackAppearanceManager.js | 22 +++++++++++++------ map/src/map/util/TrackLayerProvider.js | 6 ++--- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/map/src/infoblock/components/tabs/WaypointsTab.jsx b/map/src/infoblock/components/tabs/WaypointsTab.jsx index 040d3865e..6c9e84077 100644 --- a/map/src/infoblock/components/tabs/WaypointsTab.jsx +++ b/map/src/infoblock/components/tabs/WaypointsTab.jsx @@ -29,7 +29,7 @@ function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) { }; }); - const updatedFile = { + const updatedGpxFile = { ...prevFile, info: { ...prevFile.info, @@ -37,15 +37,12 @@ function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) { }, }; - ctx.setSelectedGpxFile(updatedFile); + ctx.setSelectedGpxFile(updatedGpxFile); if (isCloudTrack(ctx)) { debouncer( () => { - const diff = { - pointsGroups: Object.fromEntries(allGroupNames.map((name) => [name, { ext: { hidden } }])), - }; - updateInfoFile(updatedFile, diff); + updateInfoFile(updatedGpxFile); }, debouncerTimer, 500 diff --git a/map/src/manager/track/TrackAppearanceManager.js b/map/src/manager/track/TrackAppearanceManager.js index 59459875a..04f2c4c45 100644 --- a/map/src/manager/track/TrackAppearanceManager.js +++ b/map/src/manager/track/TrackAppearanceManager.js @@ -2,20 +2,28 @@ import { apiPost } from '../../util/HttpApi'; import { INFO_FILE_EXT } from './TracksManager'; /** - * Update track appearance in .info file with partial data (only changed fields). + * Update track appearance by sending the entire .info file. * - * @param {Object} updatedInfoFile - The info file object - * @param {Object} diff - Partial info object with only changed fields + * @param {Object} updatedGpxFile - The full info file object to save * @returns {Promise} Success status */ -export async function updateInfoFile(updatedInfoFile, diff) { - if (!updatedInfoFile?.name || !diff || Object.keys(diff).length === 0) { +export async function updateInfoFile(updatedGpxFile) { + if (!updatedGpxFile?.name) { return false; } - const res = await apiPost(`${process.env.REACT_APP_USER_API_SITE}/mapapi/update-info`, diff, { + const jsonString = JSON.stringify(updatedGpxFile.info); + const convertedData = new TextEncoder().encode(jsonString); + const zippedResult = require('pako').gzip(convertedData, { to: 'Uint8Array' }); + const convertedZipped = zippedResult.buffer; + const oMyBlob = new Blob([convertedZipped], { type: 'application/json' }); + const data = new FormData(); + const infoFileName = updatedGpxFile.name + INFO_FILE_EXT; + data.append('file', oMyBlob, infoFileName); + + const res = await apiPost(`${process.env.REACT_APP_USER_API_SITE}/mapapi/update-info`, data, { params: { - name: updatedInfoFile.name + INFO_FILE_EXT, + name: infoFileName, }, }); diff --git a/map/src/map/util/TrackLayerProvider.js b/map/src/map/util/TrackLayerProvider.js index ad76f36ec..e84a39e82 100644 --- a/map/src/map/util/TrackLayerProvider.js +++ b/map/src/map/util/TrackLayerProvider.js @@ -726,9 +726,9 @@ export function redrawWptsOnLayer({ layer }) { layer.getLayers().forEach((l) => { if (l instanceof L.Marker && l.options?.wpt) { if (l._icon?.style) { - const category = l.options?.category || ''; - const isHidden = pointsGroups?.[category]?.ext?.hidden === true; - l._icon.style.display = isHidden ? 'none' : null; + const category = l.options?.category || ''; + const isHidden = pointsGroups?.[category]?.ext?.hidden === true; + l._icon.style.display = isHidden ? 'none' : null; } } }); From 098a82910f0752aec4b41a5e02378f7747d42782 Mon Sep 17 00:00:00 2001 From: Dima-1 Date: Wed, 18 Mar 2026 13:27:20 +0200 Subject: [PATCH 08/24] Add info file updatetime in to api request --- map/src/infoblock/components/tabs/WaypointsTab.jsx | 2 +- map/src/manager/track/TrackAppearanceManager.js | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/map/src/infoblock/components/tabs/WaypointsTab.jsx b/map/src/infoblock/components/tabs/WaypointsTab.jsx index 6c9e84077..ba9f88619 100644 --- a/map/src/infoblock/components/tabs/WaypointsTab.jsx +++ b/map/src/infoblock/components/tabs/WaypointsTab.jsx @@ -42,7 +42,7 @@ function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) { if (isCloudTrack(ctx)) { debouncer( () => { - updateInfoFile(updatedGpxFile); + updateInfoFile(ctx, updatedGpxFile); }, debouncerTimer, 500 diff --git a/map/src/manager/track/TrackAppearanceManager.js b/map/src/manager/track/TrackAppearanceManager.js index 04f2c4c45..e1651c1bc 100644 --- a/map/src/manager/track/TrackAppearanceManager.js +++ b/map/src/manager/track/TrackAppearanceManager.js @@ -4,28 +4,33 @@ import { INFO_FILE_EXT } from './TracksManager'; /** * Update track appearance by sending the entire .info file. * + * @param {Object} ctx - AppContext value * @param {Object} updatedGpxFile - The full info file object to save * @returns {Promise} Success status */ -export async function updateInfoFile(updatedGpxFile) { +export async function updateInfoFile(ctx, updatedGpxFile) { if (!updatedGpxFile?.name) { return false; } + const infoFileName = updatedGpxFile.name + INFO_FILE_EXT; + const infoFile = ctx?.listFiles?.uniqueFiles?.find((file) => file?.name === infoFileName); + const infoUpdateTime = infoFile?.updatetimems ?? null; const jsonString = JSON.stringify(updatedGpxFile.info); const convertedData = new TextEncoder().encode(jsonString); const zippedResult = require('pako').gzip(convertedData, { to: 'Uint8Array' }); const convertedZipped = zippedResult.buffer; const oMyBlob = new Blob([convertedZipped], { type: 'application/json' }); const data = new FormData(); - const infoFileName = updatedGpxFile.name + INFO_FILE_EXT; data.append('file', oMyBlob, infoFileName); const res = await apiPost(`${process.env.REACT_APP_USER_API_SITE}/mapapi/update-info`, data, { params: { name: infoFileName, + updatetime: infoUpdateTime, }, }); + infoFile.updatetimems = res?.data?.updatetime ?? infoUpdateTime; return res?.data?.status === 'ok'; } From 9948b8a2481c7caf44dfb592bee475ffb14d8dab Mon Sep 17 00:00:00 2001 From: Dima-1 Date: Wed, 18 Mar 2026 16:17:52 +0200 Subject: [PATCH 09/24] Fix inconsistent visibility toggle --- .../components/tabs/WaypointsTab.jsx | 67 ++++++++++--------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/map/src/infoblock/components/tabs/WaypointsTab.jsx b/map/src/infoblock/components/tabs/WaypointsTab.jsx index ba9f88619..ec383fbd2 100644 --- a/map/src/infoblock/components/tabs/WaypointsTab.jsx +++ b/map/src/infoblock/components/tabs/WaypointsTab.jsx @@ -13,41 +13,42 @@ import { debouncer } from '../../../context/TracksRoutingCache'; import { updateInfoFile } from '../../../manager/track/TrackAppearanceManager'; function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) { - const prevFile = ctx.selectedGpxFile; - const updatedPointsGroups = { ...(prevFile?.info?.pointsGroups || {}) }; - const allGroupNames = groupNames || Object.keys(updatedPointsGroups || {}); - - allGroupNames.forEach((groupName) => { - const group = updatedPointsGroups[groupName] || {}; - - updatedPointsGroups[groupName] = { - ...group, - ext: { - ...group.ext, - hidden, + ctx.setSelectedGpxFile((prevFile) => { + const updatedPointsGroups = { ...(prevFile?.info?.pointsGroups || {}) }; + const allGroupNames = groupNames || Object.keys(updatedPointsGroups || {}); + + allGroupNames.forEach((groupName) => { + const group = updatedPointsGroups[groupName] || {}; + + updatedPointsGroups[groupName] = { + ...group, + ext: { + ...group.ext, + hidden, + }, + }; + }); + + const updatedGpxFile = { + ...prevFile, + info: { + ...prevFile.info, + pointsGroups: updatedPointsGroups, }, }; - }); - const updatedGpxFile = { - ...prevFile, - info: { - ...prevFile.info, - pointsGroups: updatedPointsGroups, - }, - }; - - ctx.setSelectedGpxFile(updatedGpxFile); + if (isCloudTrack(ctx)) { + debouncer( + () => { + updateInfoFile(ctx, updatedGpxFile); + }, + debouncerTimer, + 500 + ); + } - if (isCloudTrack(ctx)) { - debouncer( - () => { - updateInfoFile(ctx, updatedGpxFile); - }, - debouncerTimer, - 500 - ); - } + return updatedGpxFile; + }); } // distinct component @@ -360,7 +361,9 @@ export default function WaypointsTab() { const switchMassVisible = () => { const newMassVisible = !massVisible; setMassVisible(newMassVisible); - const groupNames = Object.keys(ctx.selectedGpxFile.pointsGroups || {}); + const groupNames = Object.keys( + ctx.selectedGpxFile.info?.pointsGroups || ctx.selectedGpxFile.pointsGroups || {} + ); updateGroupsVisibility(ctx, groupNames, !newMassVisible, debouncerTimer); }; From 977b0d6888ad94d41b074bf9417d27083dfb4b98 Mon Sep 17 00:00:00 2001 From: Dima-1 Date: Wed, 18 Mar 2026 17:37:50 +0200 Subject: [PATCH 10/24] Fix initial wpt group visibility --- map/src/map/util/TrackLayerProvider.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/map/src/map/util/TrackLayerProvider.js b/map/src/map/util/TrackLayerProvider.js index e84a39e82..186288db7 100644 --- a/map/src/map/util/TrackLayerProvider.js +++ b/map/src/map/util/TrackLayerProvider.js @@ -518,6 +518,10 @@ function parseWpt({ }; ctx.setSelectedWpt(wpt); }); + marker.on('add', (e) => { + const visible = data.info.pointsGroups[marker.options.category]?.ext?.hidden !== true; + e.target._icon.style.display = visible ? '' : 'none'; + }); } addMarkerTooltip({ marker, From 9c2c6b44460bb6b86bd8ef0dcb46f817b65bcd8b Mon Sep 17 00:00:00 2001 From: Dima-1 Date: Wed, 18 Mar 2026 18:20:51 +0200 Subject: [PATCH 11/24] Fix initial wpt group visibility --- map/src/infoblock/components/tabs/WaypointsTab.jsx | 5 ++++- map/src/manager/track/TrackAppearanceManager.js | 7 +++++-- map/src/map/util/TrackLayerProvider.js | 7 +++++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/map/src/infoblock/components/tabs/WaypointsTab.jsx b/map/src/infoblock/components/tabs/WaypointsTab.jsx index ec383fbd2..ea23da605 100644 --- a/map/src/infoblock/components/tabs/WaypointsTab.jsx +++ b/map/src/infoblock/components/tabs/WaypointsTab.jsx @@ -14,7 +14,7 @@ import { updateInfoFile } from '../../../manager/track/TrackAppearanceManager'; function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) { ctx.setSelectedGpxFile((prevFile) => { - const updatedPointsGroups = { ...(prevFile?.info?.pointsGroups || {}) }; + const updatedPointsGroups = { ...(prevFile?.info?.pointsGroups || prevFile?.pointsGroups || {}) }; const allGroupNames = groupNames || Object.keys(updatedPointsGroups || {}); allGroupNames.forEach((groupName) => { @@ -28,6 +28,9 @@ function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) { }, }; }); + if (prevFile?.gpx?.options) { + prevFile.gpx.options.pointsGroups = updatedPointsGroups; + } const updatedGpxFile = { ...prevFile, diff --git a/map/src/manager/track/TrackAppearanceManager.js b/map/src/manager/track/TrackAppearanceManager.js index e1651c1bc..1d6a401d2 100644 --- a/map/src/manager/track/TrackAppearanceManager.js +++ b/map/src/manager/track/TrackAppearanceManager.js @@ -31,6 +31,9 @@ export async function updateInfoFile(ctx, updatedGpxFile) { }, }); - infoFile.updatetimems = res?.data?.updatetime ?? infoUpdateTime; - return res?.data?.status === 'ok'; + if (res?.data?.status === 'ok' && res.data.updatetime) { + infoFile.updatetimems = res.data.updatetime; + return true; + } + return false; } diff --git a/map/src/map/util/TrackLayerProvider.js b/map/src/map/util/TrackLayerProvider.js index 186288db7..7f0b7b1ab 100644 --- a/map/src/map/util/TrackLayerProvider.js +++ b/map/src/map/util/TrackLayerProvider.js @@ -519,8 +519,11 @@ function parseWpt({ ctx.setSelectedWpt(wpt); }); marker.on('add', (e) => { - const visible = data.info.pointsGroups[marker.options.category]?.ext?.hidden !== true; - e.target._icon.style.display = visible ? '' : 'none'; + const pointsGroups = data?.info?.pointsGroups || data?.pointsGroups; + const visible = pointsGroups[marker.options.category]?.ext?.hidden !== true; + if (e.target?._icon?.style) { + e.target._icon.style.display = visible ? '' : 'none'; + } }); } addMarkerTooltip({ From 9f0a51fb66caafc41c7a716d945aa8055d57e50f Mon Sep 17 00:00:00 2001 From: Dima-1 Date: Thu, 19 Mar 2026 15:14:05 +0200 Subject: [PATCH 12/24] Move updateGroupsVisibility to manager, fix review --- .../components/tabs/WaypointsTab.jsx | 55 ++-------------- .../manager/track/TrackAppearanceManager.js | 66 ++++++++++++++++--- map/src/map/util/TrackLayerProvider.js | 7 +- 3 files changed, 67 insertions(+), 61 deletions(-) diff --git a/map/src/infoblock/components/tabs/WaypointsTab.jsx b/map/src/infoblock/components/tabs/WaypointsTab.jsx index ea23da605..76e76f264 100644 --- a/map/src/infoblock/components/tabs/WaypointsTab.jsx +++ b/map/src/infoblock/components/tabs/WaypointsTab.jsx @@ -1,5 +1,5 @@ import { useContext, useEffect, useMemo, useState, useRef } from 'react'; -import AppContext, { isLocalTrack, isCloudTrack } from '../../../context/AppContext'; +import AppContext, { isLocalTrack } from '../../../context/AppContext'; import { Alert, Box, Button, Collapse, Grid, IconButton, MenuItem, Switch, Tooltip, Typography } from '@mui/material'; import L from 'leaflet'; import { Cancel, ExpandLess, ExpandMore, KeyboardDoubleArrowDown, KeyboardDoubleArrowUp } from '@mui/icons-material'; @@ -9,49 +9,10 @@ import { confirm } from '../../../dialogs/GlobalConfirmationDialog'; import { useWindowSize } from '../../../util/hooks/useWindowSize'; import { createPoiIcon } from '../../../map/markers/MarkerOptions'; import isEmpty from 'lodash-es/isEmpty'; -import { debouncer } from '../../../context/TracksRoutingCache'; -import { updateInfoFile } from '../../../manager/track/TrackAppearanceManager'; - -function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) { - ctx.setSelectedGpxFile((prevFile) => { - const updatedPointsGroups = { ...(prevFile?.info?.pointsGroups || prevFile?.pointsGroups || {}) }; - const allGroupNames = groupNames || Object.keys(updatedPointsGroups || {}); - - allGroupNames.forEach((groupName) => { - const group = updatedPointsGroups[groupName] || {}; - - updatedPointsGroups[groupName] = { - ...group, - ext: { - ...group.ext, - hidden, - }, - }; - }); - if (prevFile?.gpx?.options) { - prevFile.gpx.options.pointsGroups = updatedPointsGroups; - } +import { updateGroupsVisibility } from '../../../manager/track/TrackAppearanceManager'; - const updatedGpxFile = { - ...prevFile, - info: { - ...prevFile.info, - pointsGroups: updatedPointsGroups, - }, - }; - - if (isCloudTrack(ctx)) { - debouncer( - () => { - updateInfoFile(ctx, updatedGpxFile); - }, - debouncerTimer, - 500 - ); - } - - return updatedGpxFile; - }); +function getPointsGroupsForInfoFile(gpxFile) { + return gpxFile.info?.pointsGroups ?? gpxFile.pointsGroups; } // distinct component @@ -357,16 +318,14 @@ export default function WaypointsTab() { const [showMass, setShowMass] = useState(false); const [massOpen, setMassOpen] = useState(false); - const [massVisible, setMassVisible] = useState(true); + const [massVisible, setMassVisible] = useState(false); const debouncerTimer = useRef(null); const switchMassOpen = () => setMassOpen(!massOpen); const switchMassVisible = () => { const newMassVisible = !massVisible; setMassVisible(newMassVisible); - const groupNames = Object.keys( - ctx.selectedGpxFile.info?.pointsGroups || ctx.selectedGpxFile.pointsGroups || {} - ); + const groupNames = Object.keys(getPointsGroupsForInfoFile(ctx.selectedGpxFile) || {}); updateGroupsVisibility(ctx, groupNames, !newMassVisible, debouncerTimer); }; @@ -381,7 +340,7 @@ export default function WaypointsTab() { const groups = getSortedGroups(); const keys = Object.keys(groups); const trackName = ctx.selectedGpxFile.name; - const pointsGroups = ctx.selectedGpxFile.info?.pointsGroups ?? ctx.selectedGpxFile.pointsGroups; + const pointsGroups = getPointsGroupsForInfoFile(ctx.selectedGpxFile); setShowMass(keys.length > 1); diff --git a/map/src/manager/track/TrackAppearanceManager.js b/map/src/manager/track/TrackAppearanceManager.js index 1d6a401d2..2bbc0e870 100644 --- a/map/src/manager/track/TrackAppearanceManager.js +++ b/map/src/manager/track/TrackAppearanceManager.js @@ -1,21 +1,22 @@ import { apiPost } from '../../util/HttpApi'; import { INFO_FILE_EXT } from './TracksManager'; +import { isCloudTrack } from '../../context/AppContext'; +import { debouncer } from '../../context/TracksRoutingCache'; /** - * Update track appearance by sending the entire .info file. + * Update or create track appearance by sending the entire .info file. * * @param {Object} ctx - AppContext value * @param {Object} updatedGpxFile - The full info file object to save * @returns {Promise} Success status */ -export async function updateInfoFile(ctx, updatedGpxFile) { +export async function createOrUpdateInfoFile(updatedGpxFile, infoFileName, infoFile) { if (!updatedGpxFile?.name) { return false; } - const infoFileName = updatedGpxFile.name + INFO_FILE_EXT; - const infoFile = ctx?.listFiles?.uniqueFiles?.find((file) => file?.name === infoFileName); const infoUpdateTime = infoFile?.updatetimems ?? null; + const jsonString = JSON.stringify(updatedGpxFile.info); const convertedData = new TextEncoder().encode(jsonString); const zippedResult = require('pako').gzip(convertedData, { to: 'Uint8Array' }); @@ -24,16 +25,61 @@ export async function updateInfoFile(ctx, updatedGpxFile) { const data = new FormData(); data.append('file', oMyBlob, infoFileName); + const params = + infoUpdateTime == null + ? { name: infoFileName } // updatetime null => create file + : { name: infoFileName, updatetime: infoUpdateTime }; // updatetime present => update file + const res = await apiPost(`${process.env.REACT_APP_USER_API_SITE}/mapapi/update-info`, data, { - params: { - name: infoFileName, - updatetime: infoUpdateTime, - }, + params, }); - if (res?.data?.status === 'ok' && res.data.updatetime) { - infoFile.updatetimems = res.data.updatetime; + if (res?.data?.updatetime) { + if (infoFile) { + infoFile.updatetimems = res.data.updatetime; + } return true; } return false; } + +export function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) { + ctx.setSelectedGpxFile((prevFile) => { + const updatedPointsGroups = { ...(prevFile?.info?.pointsGroups || prevFile?.pointsGroups || {}) }; + const allGroupNames = groupNames || Object.keys(updatedPointsGroups || {}); + + allGroupNames.forEach((groupName) => { + const group = updatedPointsGroups[groupName] || {}; + + updatedPointsGroups[groupName] = { + ...group, + ext: { + ...group.ext, + hidden, + }, + }; + }); + + const updatedGpxFile = { + ...prevFile, + info: { + ...prevFile.info, + pointsGroups: updatedPointsGroups, + }, + }; + + if (isCloudTrack(ctx)) { + const infoFileName = updatedGpxFile.name + INFO_FILE_EXT; + const infoFile = ctx.listFiles.uniqueFiles?.find((file) => file?.name === infoFileName); + debouncer( + () => { + createOrUpdateInfoFile(updatedGpxFile, infoFileName, infoFile); + }, + debouncerTimer, + 500 + ); + } + + return updatedGpxFile; + }); +} diff --git a/map/src/map/util/TrackLayerProvider.js b/map/src/map/util/TrackLayerProvider.js index 7f0b7b1ab..6c7e10ee4 100644 --- a/map/src/map/util/TrackLayerProvider.js +++ b/map/src/map/util/TrackLayerProvider.js @@ -489,7 +489,7 @@ function parseWpt({ if (point.name) { opt.name = point.name; } - opt.category = point.category ? point.category : ''; + opt.category = point.category ?? ''; opt.groupId = groupId; if (point.desc) { opt.desc = point.desc; @@ -734,8 +734,9 @@ export function redrawWptsOnLayer({ layer }) { if (l instanceof L.Marker && l.options?.wpt) { if (l._icon?.style) { const category = l.options?.category || ''; - const isHidden = pointsGroups?.[category]?.ext?.hidden === true; - l._icon.style.display = isHidden ? 'none' : null; + const visible = pointsGroups?.[category]?.ext?.hidden !== true; + // show Wpt on layer: visible - '' , hidden - 'none' + l._icon.style.display = visible ? '' : 'none'; } } }); From 051262437780750c15e78570f28406827b1c7c18 Mon Sep 17 00:00:00 2001 From: Dima-1 Date: Thu, 19 Mar 2026 17:16:59 +0200 Subject: [PATCH 13/24] Update options and layers --- map/src/infoblock/components/tabs/WaypointsTab.jsx | 6 +++++- map/src/manager/track/TrackAppearanceManager.js | 9 +++++++++ map/src/map/util/TrackLayerProvider.js | 4 ++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/map/src/infoblock/components/tabs/WaypointsTab.jsx b/map/src/infoblock/components/tabs/WaypointsTab.jsx index 76e76f264..55ae7dbb1 100644 --- a/map/src/infoblock/components/tabs/WaypointsTab.jsx +++ b/map/src/infoblock/components/tabs/WaypointsTab.jsx @@ -318,7 +318,11 @@ export default function WaypointsTab() { const [showMass, setShowMass] = useState(false); const [massOpen, setMassOpen] = useState(false); - const [massVisible, setMassVisible] = useState(false); + const [massVisible, setMassVisible] = useState(() => { + const pointsGroups = getPointsGroupsForInfoFile(ctx.selectedGpxFile); + const groupKeys = Object.keys(pointsGroups || {}); + return groupKeys.length > 0 && groupKeys.every((g) => pointsGroups?.[g]?.ext?.hidden !== true); + }); const debouncerTimer = useRef(null); const switchMassOpen = () => setMassOpen(!massOpen); diff --git a/map/src/manager/track/TrackAppearanceManager.js b/map/src/manager/track/TrackAppearanceManager.js index 2bbc0e870..45d62e001 100644 --- a/map/src/manager/track/TrackAppearanceManager.js +++ b/map/src/manager/track/TrackAppearanceManager.js @@ -60,6 +60,15 @@ export function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) }; }); + // Mutate options to preserve instance methods on gpx and layers + if (prevFile?.gpx?.options) { + prevFile.gpx.options.pointsGroups = updatedPointsGroups; + } + + if (prevFile?.layers?.options) { + prevFile.layers.options.pointsGroups = updatedPointsGroups; + } + const updatedGpxFile = { ...prevFile, info: { diff --git a/map/src/map/util/TrackLayerProvider.js b/map/src/map/util/TrackLayerProvider.js index 6c7e10ee4..d6dc1e819 100644 --- a/map/src/map/util/TrackLayerProvider.js +++ b/map/src/map/util/TrackLayerProvider.js @@ -519,8 +519,8 @@ function parseWpt({ ctx.setSelectedWpt(wpt); }); marker.on('add', (e) => { - const pointsGroups = data?.info?.pointsGroups || data?.pointsGroups; - const visible = pointsGroups[marker.options.category]?.ext?.hidden !== true; + const pointsGroups = data.info?.pointsGroups || data.pointsGroups; + const visible = pointsGroups?.[marker.options.category]?.ext?.hidden !== true; if (e.target?._icon?.style) { e.target._icon.style.display = visible ? '' : 'none'; } From af37a52ff241f617dace3c0ff3221e2c712fa0b0 Mon Sep 17 00:00:00 2001 From: Dima-1 Date: Fri, 20 Mar 2026 10:02:11 +0200 Subject: [PATCH 14/24] Add test wpt visibility --- .../infoblock/components/tabs/TrackTabList.js | 2 +- .../components/tabs/WaypointsTab.jsx | 6 +- tests/selenium/gpx/test-wpt-groups.gpx | 48 ++++++ .../src/tests/tracks/89-wpt-visibility.mjs | 139 ++++++++++++++++++ 4 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 tests/selenium/gpx/test-wpt-groups.gpx create mode 100644 tests/selenium/src/tests/tracks/89-wpt-visibility.mjs diff --git a/map/src/infoblock/components/tabs/TrackTabList.js b/map/src/infoblock/components/tabs/TrackTabList.js index edc566a21..a6027738f 100644 --- a/map/src/infoblock/components/tabs/TrackTabList.js +++ b/map/src/infoblock/components/tabs/TrackTabList.js @@ -37,7 +37,7 @@ export default class TrackTabList { // } list = list.concat( - Object.keys(tabs).map((item) => ) + Object.keys(tabs).map((item) => ) ); this.state.tabList = list; diff --git a/map/src/infoblock/components/tabs/WaypointsTab.jsx b/map/src/infoblock/components/tabs/WaypointsTab.jsx index 55ae7dbb1..4982ce648 100644 --- a/map/src/infoblock/components/tabs/WaypointsTab.jsx +++ b/map/src/infoblock/components/tabs/WaypointsTab.jsx @@ -106,7 +106,7 @@ const WaypointGroup = ({ - + @@ -369,7 +369,7 @@ export default function WaypointsTab() { return ( <> - + {ctx.createTrack && ctx.selectedGpxFile?.wpts && !isEmpty(ctx.selectedGpxFile.wpts) && ( @@ -394,7 +394,7 @@ export default function WaypointsTab() { {showMass && ( - + )} diff --git a/tests/selenium/gpx/test-wpt-groups.gpx b/tests/selenium/gpx/test-wpt-groups.gpx new file mode 100644 index 000000000..de85f4164 --- /dev/null +++ b/tests/selenium/gpx/test-wpt-groups.gpx @@ -0,0 +1,48 @@ + + + + Test Waypoint Groups + + + GroupA-Point1 + groupA + + special_star + circle + #ff0000 + + + + GroupA-Point2 + groupA + + special_star + circle + #ff0000 + + + + GroupB-Point1 + groupB + + amenity_fuel + square + #00ff00 + + + + GroupB-Point2 + groupB + + amenity_fuel + square + #00ff00 + + + + + + + + + \ No newline at end of file diff --git a/tests/selenium/src/tests/tracks/89-wpt-visibility.mjs b/tests/selenium/src/tests/tracks/89-wpt-visibility.mjs new file mode 100644 index 000000000..c5e816619 --- /dev/null +++ b/tests/selenium/src/tests/tracks/89-wpt-visibility.mjs @@ -0,0 +1,139 @@ +'use strict'; + +import { By } from 'selenium-webdriver'; +import { clickBy, waitBy, assert } from '../../lib.mjs'; + +import actionOpenMap from '../../actions/map/actionOpenMap.mjs'; +import actionLogIn from '../../actions/login/actionLogIn.mjs'; +import actionLogOut from '../../actions/login/actionLogOut.mjs'; +import actionUploadGpx from '../../actions/actionUploadGpx.mjs'; +import actionLocalToCloud from '../../actions/tracks/actionLocalToCloud.mjs'; +import actionIdleWait from '../../actions/actionIdleWait.mjs'; +import actionDeleteTracksByPattern from '../../actions/tracks/actionDeleteTracksByPattern.mjs'; +import { deleteTrack, getFiles } from '../../util.mjs'; +import { driver } from '../../options.mjs'; + +export default async function test() { + await actionOpenMap(); + await actionLogIn(); + + const tracks = getFiles({ folder: 'gpx' }); + const trackName = 'test-wpt-groups'; + + for (const track of tracks) { + await actionDeleteTracksByPattern(track.name); + } + + // Upload track with multiple waypoint groups + await actionUploadGpx({ mask: trackName + '.gpx' }); + await clickBy(By.id('se-show-menu-planroute')); + await actionLocalToCloud({ mask: trackName }); + + // Open track + await clickBy(By.id('se-cloud-track-' + trackName)); + + // Open Waypoints tab + await clickBy(By.css("[testid='se-tab-waypoints']")); + await waitBy(By.id('se-waypoints-tab-content')); + + const massSwitch = await driver.findElement(By.id('se-wpt-mass-visibility-switch')); + const isOn = await massSwitch.isSelected(); // true = ON, false = OFF + // Toggle mass visibility switch all groups should be visible after + if (isOn) { + await massSwitch.click(); + await massSwitch.click(); + } else { + await massSwitch.click(); + } + const isOnAfter = await massSwitch.isSelected(); + await assert(isOnAfter, 'Switch should be ON after click'); + + // Get initial marker count (find all visible waypoint markers on map) + const initialMarkers = await getVisibleWaypointMarkers(); + const initialCount = initialMarkers.length; + await assert(initialCount === 4, `Track should have 4 waypoints, got ${initialCount}`); + + // Test: Toggle first group visibility OFF + await clickBy(By.id('se-wpt-group-visibility-groupA')); + await actionIdleWait({ idle: 1000 }); + const afterFirstHide = await getVisibleWaypointMarkers(); + await assert( + afterFirstHide.length === 2, + `First group should be hidden. Expected 2, got ${afterFirstHide.length}` + ); + + // log out and log in again + await actionLogOut(); + await clickBy(By.id('se-login-button')); + await actionLogIn(); + + // open track + await clickBy(By.id('se-show-menu-tracks')); + await clickBy(By.id('se-cloud-track-' + trackName)); + const afterReload = await getVisibleWaypointMarkers(); + await assert( + afterReload.length === 2, + `First group should be hidden. Expected 2, got ${afterReload.length}` + ); + await clickBy(By.css("[testid='se-tab-waypoints']")) + + // Test: Toggle second group visibility OFF + await clickBy(By.id('se-wpt-group-visibility-groupB')); + await actionIdleWait({ idle: 1000 }); + + const afterSecondHide = await getVisibleWaypointMarkers(); + await assert( + afterSecondHide.length === 0, + `Both groups should be hidden. Expected 0, got ${afterSecondHide.length}` + ); + + // Test: Toggle first group visibility ON + await clickBy(By.id('se-wpt-group-visibility-groupA')); + await actionIdleWait({ idle: 1000 }); + + // log out and log in again + await actionLogOut(); + await clickBy(By.id('se-login-button')); + await actionLogIn(); + + // open track + await clickBy(By.id('se-show-menu-tracks')); + await clickBy(By.id('se-cloud-track-' + trackName)); + + const afterFirstShow = await getVisibleWaypointMarkers(); + await assert( + afterFirstShow.length === 2, + `First group should be visible again. Expected 2, got ${afterFirstShow.length}` + ); + await clickBy(By.css("[testid='se-tab-waypoints']")) + + // Test: Toggle second group visibility ON + await clickBy(By.id('se-wpt-group-visibility-groupB')); + await actionIdleWait({ idle: 1000 }); + + const afterBothShow = await getVisibleWaypointMarkers(); + await assert( + afterBothShow.length === 4, + `Both groups should be visible. Expected 4, got ${afterBothShow.length}` + ); + + // Close track and cleanup + await clickBy(By.id('se-button-back')); + await deleteTrack(trackName); +} + +// Helper function to count visible waypoint markers on the map +async function getVisibleWaypointMarkers() { + // Find all waypoint markers that are visible (display !== 'none') + const markers = await driver.findElements(By.className('leaflet-marker-icon')); + + const visibleMarkers = []; + for (const marker of markers) { + const display = await marker.getCssValue('display'); + if (display !== 'none') { + visibleMarkers.push(marker); + } + } + + return visibleMarkers; +} \ No newline at end of file From 5faf15554f211472e573945eda59c32b8cf5e9d5 Mon Sep 17 00:00:00 2001 From: Dima-1 Date: Fri, 20 Mar 2026 11:36:21 +0200 Subject: [PATCH 15/24] Remove unnecessary test file, fix formating --- .../infoblock/components/tabs/TrackTabList.js | 9 +++- .../components/tabs/WaypointsTab.jsx | 2 +- .../manager/track/TrackAppearanceManager.js | 2 +- tests/selenium/gpx/test-track-wpt.gpx | 16 ++++++- tests/selenium/gpx/test-wpt-groups.gpx | 48 ------------------- .../src/tests/tracks/89-wpt-visibility.mjs | 24 +++++----- 6 files changed, 37 insertions(+), 64 deletions(-) delete mode 100644 tests/selenium/gpx/test-wpt-groups.gpx diff --git a/map/src/infoblock/components/tabs/TrackTabList.js b/map/src/infoblock/components/tabs/TrackTabList.js index a6027738f..2f9f7eecb 100644 --- a/map/src/infoblock/components/tabs/TrackTabList.js +++ b/map/src/infoblock/components/tabs/TrackTabList.js @@ -37,7 +37,14 @@ export default class TrackTabList { // } list = list.concat( - Object.keys(tabs).map((item) => ) + Object.keys(tabs).map((item) => ( + + )) ); this.state.tabList = list; diff --git a/map/src/infoblock/components/tabs/WaypointsTab.jsx b/map/src/infoblock/components/tabs/WaypointsTab.jsx index 4982ce648..85a373054 100644 --- a/map/src/infoblock/components/tabs/WaypointsTab.jsx +++ b/map/src/infoblock/components/tabs/WaypointsTab.jsx @@ -394,7 +394,7 @@ export default function WaypointsTab() { {showMass && ( - + )} diff --git a/map/src/manager/track/TrackAppearanceManager.js b/map/src/manager/track/TrackAppearanceManager.js index 45d62e001..b3bd9b3d6 100644 --- a/map/src/manager/track/TrackAppearanceManager.js +++ b/map/src/manager/track/TrackAppearanceManager.js @@ -64,7 +64,7 @@ export function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) if (prevFile?.gpx?.options) { prevFile.gpx.options.pointsGroups = updatedPointsGroups; } - + if (prevFile?.layers?.options) { prevFile.layers.options.pointsGroups = updatedPointsGroups; } diff --git a/tests/selenium/gpx/test-track-wpt.gpx b/tests/selenium/gpx/test-track-wpt.gpx index bcbbc6a42..8d4077be2 100644 --- a/tests/selenium/gpx/test-track-wpt.gpx +++ b/tests/selenium/gpx/test-track-wpt.gpx @@ -1,5 +1,5 @@ - + @@ -13,47 +13,61 @@ VELO-6 6/13/2016 9:37:59 PM 6/13/2016 9:37:59 PM + groupA 0.000000 VELO-7 6/13/2016 9:38:12 PM 6/13/2016 9:38:12 PM + groupA 0.000000 VELO-2 6/13/2016 10:17:21 PM 6/13/2016 10:17:21 PM + groupA 0.000000 VELO-3 6/13/2016 10:17:33 PM 6/13/2016 10:17:33 PM + groupB 0.000000 VELO-4 6/13/2016 10:18:44 PM 6/13/2016 10:18:44 PM + groupB 0.000000 VELO-5 6/13/2016 10:26:34 PM 6/13/2016 10:26:34 PM + groupB 0.000000 VELO-1 6/13/2016 10:28:10 PM 6/13/2016 10:28:10 PM + groupB 0.000000 VELO-BAZA 6/14/2016 11:20:28 AM 6/14/2016 11:20:28 AM + groupB + + + + + + diff --git a/tests/selenium/gpx/test-wpt-groups.gpx b/tests/selenium/gpx/test-wpt-groups.gpx deleted file mode 100644 index de85f4164..000000000 --- a/tests/selenium/gpx/test-wpt-groups.gpx +++ /dev/null @@ -1,48 +0,0 @@ - - - - Test Waypoint Groups - - - GroupA-Point1 - groupA - - special_star - circle - #ff0000 - - - - GroupA-Point2 - groupA - - special_star - circle - #ff0000 - - - - GroupB-Point1 - groupB - - amenity_fuel - square - #00ff00 - - - - GroupB-Point2 - groupB - - amenity_fuel - square - #00ff00 - - - - - - - - - \ No newline at end of file diff --git a/tests/selenium/src/tests/tracks/89-wpt-visibility.mjs b/tests/selenium/src/tests/tracks/89-wpt-visibility.mjs index c5e816619..a4e60a21d 100644 --- a/tests/selenium/src/tests/tracks/89-wpt-visibility.mjs +++ b/tests/selenium/src/tests/tracks/89-wpt-visibility.mjs @@ -18,7 +18,7 @@ export default async function test() { await actionLogIn(); const tracks = getFiles({ folder: 'gpx' }); - const trackName = 'test-wpt-groups'; + const trackName = 'test-track-wpt'; for (const track of tracks) { await actionDeleteTracksByPattern(track.name); @@ -51,15 +51,15 @@ export default async function test() { // Get initial marker count (find all visible waypoint markers on map) const initialMarkers = await getVisibleWaypointMarkers(); const initialCount = initialMarkers.length; - await assert(initialCount === 4, `Track should have 4 waypoints, got ${initialCount}`); + await assert(initialCount === 9, `Track should have 9 waypoints, got ${initialCount}`); // Test: Toggle first group visibility OFF await clickBy(By.id('se-wpt-group-visibility-groupA')); await actionIdleWait({ idle: 1000 }); const afterFirstHide = await getVisibleWaypointMarkers(); await assert( - afterFirstHide.length === 2, - `First group should be hidden. Expected 2, got ${afterFirstHide.length}` + afterFirstHide.length === 6, + `First group should be hidden. Expected 6, got ${afterFirstHide.length}` ); // log out and log in again @@ -72,8 +72,8 @@ export default async function test() { await clickBy(By.id('se-cloud-track-' + trackName)); const afterReload = await getVisibleWaypointMarkers(); await assert( - afterReload.length === 2, - `First group should be hidden. Expected 2, got ${afterReload.length}` + afterReload.length === 6, + `First group should be hidden. Expected 6, got ${afterReload.length}` ); await clickBy(By.css("[testid='se-tab-waypoints']")) @@ -83,8 +83,8 @@ export default async function test() { const afterSecondHide = await getVisibleWaypointMarkers(); await assert( - afterSecondHide.length === 0, - `Both groups should be hidden. Expected 0, got ${afterSecondHide.length}` + afterSecondHide.length === 1, + `Both groups should be hidden. Expected 1, got ${afterSecondHide.length}` ); // Test: Toggle first group visibility ON @@ -102,8 +102,8 @@ export default async function test() { const afterFirstShow = await getVisibleWaypointMarkers(); await assert( - afterFirstShow.length === 2, - `First group should be visible again. Expected 2, got ${afterFirstShow.length}` + afterFirstShow.length === 4, + `First group should be visible again. Expected 4, got ${afterFirstShow.length}` ); await clickBy(By.css("[testid='se-tab-waypoints']")) @@ -113,8 +113,8 @@ export default async function test() { const afterBothShow = await getVisibleWaypointMarkers(); await assert( - afterBothShow.length === 4, - `Both groups should be visible. Expected 4, got ${afterBothShow.length}` + afterBothShow.length === 9, + `Both groups should be visible. Expected 9, got ${afterBothShow.length}` ); // Close track and cleanup From 2d69854beb9e0f62ff6b20a3c416d2b2a270eeb6 Mon Sep 17 00:00:00 2001 From: Kseniia Velychko Date: Fri, 20 Mar 2026 12:54:44 +0200 Subject: [PATCH 16/24] remove points from info json --- .../manager/track/TrackAppearanceManager.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/map/src/manager/track/TrackAppearanceManager.js b/map/src/manager/track/TrackAppearanceManager.js index b3bd9b3d6..c74bf01e8 100644 --- a/map/src/manager/track/TrackAppearanceManager.js +++ b/map/src/manager/track/TrackAppearanceManager.js @@ -43,9 +43,26 @@ export async function createOrUpdateInfoFile(updatedGpxFile, infoFileName, infoF return false; } +function sanitizePointsGroups(pointsGroups = {}) { + const result = {}; + Object.keys(pointsGroups).forEach((groupName) => { + const group = pointsGroups[groupName] || {}; + const { points, ...groupWithoutPoints } = group; + const ext = groupWithoutPoints.ext || {}; + const extWithoutPoints = { ...ext }; + delete extWithoutPoints.points; + result[groupName] = { + ...groupWithoutPoints, + ext: extWithoutPoints, + }; + }); + return result; +} + export function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) { ctx.setSelectedGpxFile((prevFile) => { - const updatedPointsGroups = { ...(prevFile?.info?.pointsGroups || prevFile?.pointsGroups || {}) }; + const sourcePointsGroups = prevFile?.info?.pointsGroups || prevFile?.pointsGroups || {}; + const updatedPointsGroups = sanitizePointsGroups(sourcePointsGroups); const allGroupNames = groupNames || Object.keys(updatedPointsGroups || {}); allGroupNames.forEach((groupName) => { From 49c5a01df6217eeb2e59e4f6331cc751863c14e8 Mon Sep 17 00:00:00 2001 From: Kseniia Velychko Date: Fri, 20 Mar 2026 14:20:50 +0200 Subject: [PATCH 17/24] fix double gpx ext --- map/src/manager/track/SaveTrackManager.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/map/src/manager/track/SaveTrackManager.js b/map/src/manager/track/SaveTrackManager.js index 9af0296fd..3d88c694b 100644 --- a/map/src/manager/track/SaveTrackManager.js +++ b/map/src/manager/track/SaveTrackManager.js @@ -27,12 +27,16 @@ import { import Utils from '../../util/Utils'; import { updateSortList } from '../../menu/actions/SortActions'; import { deleteLocalTrack, saveTrackToLocalStorage } from '../../context/LocalTrackStorage'; -import { SMART_TYPE } from '../../menu/share/shareConstants'; export function saveTrackToLocal({ ctx, track, selected = true, overwrite = false, cloudAutoSave = false } = {}) { const newLocalTracks = [...ctx.localTracks]; - const originalName = track.name + GPX_FILE_EXT; + if (!track?.name) { + ctx.setRoutingErrorMsg('⚠️ Cannot save nameless local track.'); + return; + } + + const originalName = removeFileExtension(track.name) + GPX_FILE_EXT; let localName = TracksManager.prepareName(originalName, true); // find free name From 6bc34c006fce227aa8b78a91ae087c2703146aaa Mon Sep 17 00:00:00 2001 From: Kseniia Velychko Date: Fri, 20 Mar 2026 16:07:03 +0200 Subject: [PATCH 18/24] use visible api result --- .../components/tabs/WaypointsTab.jsx | 5 +- .../manager/track/TrackAppearanceManager.js | 48 ++++++++++++------- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/map/src/infoblock/components/tabs/WaypointsTab.jsx b/map/src/infoblock/components/tabs/WaypointsTab.jsx index 85a373054..8b4a5c44a 100644 --- a/map/src/infoblock/components/tabs/WaypointsTab.jsx +++ b/map/src/infoblock/components/tabs/WaypointsTab.jsx @@ -1,5 +1,5 @@ import { useContext, useEffect, useMemo, useState, useRef } from 'react'; -import AppContext, { isLocalTrack } from '../../../context/AppContext'; +import AppContext, { isCloudTrack, isLocalTrack } from '../../../context/AppContext'; import { Alert, Box, Button, Collapse, Grid, IconButton, MenuItem, Switch, Tooltip, Typography } from '@mui/material'; import L from 'leaflet'; import { Cancel, ExpandLess, ExpandMore, KeyboardDoubleArrowDown, KeyboardDoubleArrowUp } from '@mui/icons-material'; @@ -43,12 +43,13 @@ const WaypointGroup = ({ // visibility control useEffect(() => { - mounted && + if (mounted && !isCloudTrack(ctx)) { points.forEach((p) => { if (p.layer?._icon?.style) { p.layer._icon.style.display = visible ? '' : 'none'; } }); + } }, [visible]); useEffect(() => { diff --git a/map/src/manager/track/TrackAppearanceManager.js b/map/src/manager/track/TrackAppearanceManager.js index c74bf01e8..936530c32 100644 --- a/map/src/manager/track/TrackAppearanceManager.js +++ b/map/src/manager/track/TrackAppearanceManager.js @@ -1,7 +1,6 @@ import { apiPost } from '../../util/HttpApi'; import { INFO_FILE_EXT } from './TracksManager'; import { isCloudTrack } from '../../context/AppContext'; -import { debouncer } from '../../context/TracksRoutingCache'; /** * Update or create track appearance by sending the entire .info file. @@ -77,15 +76,6 @@ export function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) }; }); - // Mutate options to preserve instance methods on gpx and layers - if (prevFile?.gpx?.options) { - prevFile.gpx.options.pointsGroups = updatedPointsGroups; - } - - if (prevFile?.layers?.options) { - prevFile.layers.options.pointsGroups = updatedPointsGroups; - } - const updatedGpxFile = { ...prevFile, info: { @@ -97,13 +87,37 @@ export function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) if (isCloudTrack(ctx)) { const infoFileName = updatedGpxFile.name + INFO_FILE_EXT; const infoFile = ctx.listFiles.uniqueFiles?.find((file) => file?.name === infoFileName); - debouncer( - () => { - createOrUpdateInfoFile(updatedGpxFile, infoFileName, infoFile); - }, - debouncerTimer, - 500 - ); + + if (debouncerTimer.current) { + clearTimeout(debouncerTimer.current); + } + debouncerTimer.current = setTimeout(async () => { + debouncerTimer.current = null; + const success = await createOrUpdateInfoFile(updatedGpxFile, infoFileName, infoFile); + if (success) { + ctx.setSelectedGpxFile((cur) => { + if (cur?.gpx?.options) { + cur.gpx.options.pointsGroups = cur.info?.pointsGroups; + } + if (cur?.layers?.options) { + cur.layers.options.pointsGroups = cur.info?.pointsGroups; + } + return { ...cur, cloudRedrawWpts: true }; + }); + } else { + ctx.setTrackErrorMsg({ + title: 'Visibility error', + msg: 'Failed to save waypoint group visibility. Please try again.', + }); + } + }, 1000); + } else { + if (prevFile?.gpx?.options) { + prevFile.gpx.options.pointsGroups = updatedPointsGroups; + } + if (prevFile?.layers?.options) { + prevFile.layers.options.pointsGroups = updatedPointsGroups; + } } return updatedGpxFile; From b37c58eb6c7404c4c6e89915fc798143fd2aa498 Mon Sep 17 00:00:00 2001 From: Kseniia Velychko Date: Sat, 21 Mar 2026 11:43:55 +0200 Subject: [PATCH 19/24] fix update visible wpt groups --- .../components/tabs/WaypointsTab.jsx | 70 ++------- map/src/manager/track/SaveTrackManager.js | 6 +- .../manager/track/TrackAppearanceManager.js | 134 +++++++++--------- map/src/manager/track/TracksManager.js | 11 ++ map/src/map/layers/CloudTrackLayer.js | 9 +- map/src/map/layers/LocalClientTrackLayer.js | 9 +- map/src/map/util/TrackLayerProvider.js | 71 ++++++---- map/src/map/util/creator/EditableMarker.js | 13 +- .../map/util/creator/LocalTrackLayerHelper.js | 11 +- 9 files changed, 171 insertions(+), 163 deletions(-) diff --git a/map/src/infoblock/components/tabs/WaypointsTab.jsx b/map/src/infoblock/components/tabs/WaypointsTab.jsx index 8b4a5c44a..63c14b280 100644 --- a/map/src/infoblock/components/tabs/WaypointsTab.jsx +++ b/map/src/infoblock/components/tabs/WaypointsTab.jsx @@ -1,20 +1,15 @@ import { useContext, useEffect, useMemo, useState, useRef } from 'react'; -import AppContext, { isCloudTrack, isLocalTrack } from '../../../context/AppContext'; +import AppContext, { isLocalTrack } from '../../../context/AppContext'; import { Alert, Box, Button, Collapse, Grid, IconButton, MenuItem, Switch, Tooltip, Typography } from '@mui/material'; -import L from 'leaflet'; import { Cancel, ExpandLess, ExpandMore, KeyboardDoubleArrowDown, KeyboardDoubleArrowUp } from '@mui/icons-material'; import PointManager from '../../../manager/PointManager'; -import TracksManager from '../../../manager/track/TracksManager'; +import TracksManager, { getResolvedPointsGroups, isWptGroupShown } from '../../../manager/track/TracksManager'; import { confirm } from '../../../dialogs/GlobalConfirmationDialog'; import { useWindowSize } from '../../../util/hooks/useWindowSize'; import { createPoiIcon } from '../../../map/markers/MarkerOptions'; import isEmpty from 'lodash-es/isEmpty'; import { updateGroupsVisibility } from '../../../manager/track/TrackAppearanceManager'; -function getPointsGroupsForInfoFile(gpxFile) { - return gpxFile.info?.pointsGroups ?? gpxFile.pointsGroups; -} - // distinct component const WaypointGroup = ({ ctx, @@ -41,17 +36,6 @@ const WaypointGroup = ({ const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); - // visibility control - useEffect(() => { - if (mounted && !isCloudTrack(ctx)) { - points.forEach((p) => { - if (p.layer?._icon?.style) { - p.layer._icon.style.display = visible ? '' : 'none'; - } - }); - } - }, [visible]); - useEffect(() => { mounted && setOpen(massOpen); }, [massOpen]); @@ -263,43 +247,11 @@ export default function WaypointsTab() { } function getSortedPoints() { - const wpts = []; - - if (ctx.selectedGpxFile.wpts) { - const layers = getLayers(); - const wptsMap = Object.fromEntries( - ctx.selectedGpxFile.wpts - .filter((wpt) => wpt.lat != null && wpt.lon != null && !isNaN(wpt.lat) && !isNaN(wpt.lon)) - .map((wpt, index) => [ - parseFloat(wpt.lat).toFixed(6) + ',' + parseFloat(wpt.lon).toFixed(6), - { wpt, index }, - ]) - ); - - layers.forEach((layer) => { - if (layer instanceof L.Marker) { - const coord = layer.getLatLng(); - const mapped = wptsMap[coord.lat.toFixed(6) + ',' + coord.lng.toFixed(6)]; - mapped && wpts.push({ wpt: mapped.wpt, index: mapped.index, layer }); - } - }); - } - const az = (a, b) => (a > b) - (a < b); - - return wpts.sort((a, b) => { - const aName = a.wpt.name; - const bName = b.wpt.name; - - const aCat = a.wpt.category; - const bCat = b.wpt.category; - - if (aCat !== bCat) { - return az(aCat, bCat); - } - - return az(aName, bName); - }); + return (ctx.selectedGpxFile.wpts || []) + .map((wpt, index) => ({ wpt, index })) + .filter(({ wpt }) => wpt.lat != null && wpt.lon != null && !Number.isNaN(wpt.lat) && !Number.isNaN(wpt.lon)) + .sort((a, b) => az(a.wpt.category, b.wpt.category) || az(a.wpt.name, b.wpt.name)); } function getSortedGroups() { @@ -320,9 +272,9 @@ export default function WaypointsTab() { const [showMass, setShowMass] = useState(false); const [massOpen, setMassOpen] = useState(false); const [massVisible, setMassVisible] = useState(() => { - const pointsGroups = getPointsGroupsForInfoFile(ctx.selectedGpxFile); + const pointsGroups = getResolvedPointsGroups(ctx.selectedGpxFile); const groupKeys = Object.keys(pointsGroups || {}); - return groupKeys.length > 0 && groupKeys.every((g) => pointsGroups?.[g]?.ext?.hidden !== true); + return groupKeys.length > 0 && groupKeys.every((g) => isWptGroupShown(pointsGroups, g)); }); const debouncerTimer = useRef(null); @@ -330,7 +282,7 @@ export default function WaypointsTab() { const switchMassVisible = () => { const newMassVisible = !massVisible; setMassVisible(newMassVisible); - const groupNames = Object.keys(getPointsGroupsForInfoFile(ctx.selectedGpxFile) || {}); + const groupNames = Object.keys(getResolvedPointsGroups(ctx.selectedGpxFile) || {}); updateGroupsVisibility(ctx, groupNames, !newMassVisible, debouncerTimer); }; @@ -345,7 +297,7 @@ export default function WaypointsTab() { const groups = getSortedGroups(); const keys = Object.keys(groups); const trackName = ctx.selectedGpxFile.name; - const pointsGroups = getPointsGroupsForInfoFile(ctx.selectedGpxFile); + const pointsGroups = getResolvedPointsGroups(ctx.selectedGpxFile); setShowMass(keys.length > 1); @@ -358,7 +310,7 @@ export default function WaypointsTab() { group={g} points={groups[g]} defaultOpen={keys.length === 1} - defaultVisible={pointsGroups?.[g]?.ext?.hidden !== true} + defaultVisible={isWptGroupShown(pointsGroups, g)} massVisible={massVisible} massOpen={massOpen} debouncerTimer={debouncerTimer} diff --git a/map/src/manager/track/SaveTrackManager.js b/map/src/manager/track/SaveTrackManager.js index 3d88c694b..4c43229b2 100644 --- a/map/src/manager/track/SaveTrackManager.js +++ b/map/src/manager/track/SaveTrackManager.js @@ -16,6 +16,7 @@ import TracksManager, { GPX_FILE_EXT, KMZ_FILE_EXT, } from './TracksManager'; +import { syncCloudTrackInfo } from './TrackAppearanceManager'; import cloneDeep from 'lodash-es/cloneDeep'; import isEmpty from 'lodash-es/isEmpty'; import { @@ -136,7 +137,10 @@ export async function saveTrackToCloud({ // close possibly loaded Cloud track (clean up layers) ctx.mutateGpxFiles((o) => o[params.name] && (o[params.name].url = null)); const res = await apiPost(`${process.env.REACT_APP_USER_API_SITE}/mapapi/upload-file`, data, { params }); - if (res && res?.data?.status === 'ok') { + if (res?.data?.status === 'ok') { + if (type !== FavoritesManager.FAVORITE_FILE_TYPE) { + await syncCloudTrackInfo(ctx, params.name); + } // re-download gpx const downloadFile = { ...currentFile, ...params }; if (open) { diff --git a/map/src/manager/track/TrackAppearanceManager.js b/map/src/manager/track/TrackAppearanceManager.js index 936530c32..c0d69c1df 100644 --- a/map/src/manager/track/TrackAppearanceManager.js +++ b/map/src/manager/track/TrackAppearanceManager.js @@ -1,37 +1,48 @@ import { apiPost } from '../../util/HttpApi'; -import { INFO_FILE_EXT } from './TracksManager'; -import { isCloudTrack } from '../../context/AppContext'; +import { getResolvedPointsGroups, INFO_FILE_EXT } from './TracksManager'; +import { isCloudTrack, isLocalTrack } from '../../context/AppContext'; +import isEmpty from 'lodash-es/isEmpty'; + +const VISIBILITY_DEBOUNCE_MS = 1000; + +/** Strip `points` arrays from groups and their `ext` (they are large and not needed in `.info`). */ +export function sanitizePointsGroups(pointsGroups = {}) { + const result = {}; + for (const [name, group] of Object.entries(pointsGroups)) { + const { points: _pts, ext, ...rest } = group || {}; + const { points: _extPts, ...cleanExt } = ext || {}; + result[name] = { ...rest, ext: cleanExt }; + } + return result; +} + +function findInfoFile(ctx, infoFileName) { + return ctx.listFiles?.uniqueFiles?.find((f) => f?.name === infoFileName); +} /** - * Update or create track appearance by sending the entire .info file. + * Upload (create or update) a track's `.info` file on the server. * - * @param {Object} ctx - AppContext value - * @param {Object} updatedGpxFile - The full info file object to save - * @returns {Promise} Success status + * @param {Object} gpxFile Object with at least `name` and `info` (the payload to persist) + * @param {string} infoFileName Cloud path for the `.info` file + * @param {Object|null} infoFile Existing file entry from `listFiles` (carries `updatetimems` for update) + * @returns {Promise} */ -export async function createOrUpdateInfoFile(updatedGpxFile, infoFileName, infoFile) { - if (!updatedGpxFile?.name) { +export async function createOrUpdateInfoFile(gpxFile, infoFileName, infoFile) { + if (!gpxFile?.name) { return false; } - const infoUpdateTime = infoFile?.updatetimems ?? null; - - const jsonString = JSON.stringify(updatedGpxFile.info); - const convertedData = new TextEncoder().encode(jsonString); - const zippedResult = require('pako').gzip(convertedData, { to: 'Uint8Array' }); - const convertedZipped = zippedResult.buffer; - const oMyBlob = new Blob([convertedZipped], { type: 'application/json' }); - const data = new FormData(); - data.append('file', oMyBlob, infoFileName); + const convertedData = new TextEncoder().encode(JSON.stringify(gpxFile.info)); + const zipped = require('pako').gzip(convertedData, { to: 'Uint8Array' }); + const blob = new Blob([zipped.buffer], { type: 'application/json' }); + const body = new FormData(); + body.append('file', blob, infoFileName); - const params = - infoUpdateTime == null - ? { name: infoFileName } // updatetime null => create file - : { name: infoFileName, updatetime: infoUpdateTime }; // updatetime present => update file + const updatetime = infoFile?.updatetimems ?? null; + const params = updatetime == null ? { name: infoFileName } : { name: infoFileName, updatetime }; - const res = await apiPost(`${process.env.REACT_APP_USER_API_SITE}/mapapi/update-info`, data, { - params, - }); + const res = await apiPost(`${process.env.REACT_APP_USER_API_SITE}/mapapi/update-info`, body, { params }); if (res?.data?.updatetime) { if (infoFile) { @@ -42,38 +53,32 @@ export async function createOrUpdateInfoFile(updatedGpxFile, infoFileName, infoF return false; } -function sanitizePointsGroups(pointsGroups = {}) { - const result = {}; - Object.keys(pointsGroups).forEach((groupName) => { - const group = pointsGroups[groupName] || {}; - const { points, ...groupWithoutPoints } = group; - const ext = groupWithoutPoints.ext || {}; - const extWithoutPoints = { ...ext }; - delete extWithoutPoints.points; - result[groupName] = { - ...groupWithoutPoints, - ext: extWithoutPoints, - }; - }); - return result; +/** + * Sync the current track's `.info` to cloud after GPX upload. + */ +export async function syncCloudTrackInfo(ctx, cloudGpxName) { + const selectedFile = ctx.selectedGpxFile; + const pointsGroups = getResolvedPointsGroups(selectedFile); + if (isEmpty(pointsGroups)) return; + + const info = { ...selectedFile?.info, pointsGroups: sanitizePointsGroups(pointsGroups) }; + const infoFileName = cloudGpxName + INFO_FILE_EXT; + const infoFile = findInfoFile(ctx, infoFileName); + await createOrUpdateInfoFile({ name: selectedFile?.name || cloudGpxName, info }, infoFileName, infoFile); } +/** + * Toggle `hidden` flag on waypoint groups and persist the change. + * - Local track: sets `updateLayers` flag to rebuild map markers. + * - Cloud track: debounces and uploads `.info` to the server. + */ export function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) { ctx.setSelectedGpxFile((prevFile) => { - const sourcePointsGroups = prevFile?.info?.pointsGroups || prevFile?.pointsGroups || {}; - const updatedPointsGroups = sanitizePointsGroups(sourcePointsGroups); - const allGroupNames = groupNames || Object.keys(updatedPointsGroups || {}); - - allGroupNames.forEach((groupName) => { - const group = updatedPointsGroups[groupName] || {}; - - updatedPointsGroups[groupName] = { - ...group, - ext: { - ...group.ext, - hidden, - }, - }; + const updatedPointsGroups = sanitizePointsGroups(getResolvedPointsGroups(prevFile) || {}); + + (groupNames || Object.keys(updatedPointsGroups)).forEach((name) => { + const group = updatedPointsGroups[name] || {}; + updatedPointsGroups[name] = { ...group, ext: { ...group.ext, hidden } }; }); const updatedGpxFile = { @@ -84,9 +89,13 @@ export function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) }, }; + if (isLocalTrack(ctx) && ctx.createTrack?.enable) { + updatedGpxFile.updateLayers = true; + } + if (isCloudTrack(ctx)) { const infoFileName = updatedGpxFile.name + INFO_FILE_EXT; - const infoFile = ctx.listFiles.uniqueFiles?.find((file) => file?.name === infoFileName); + const infoFile = findInfoFile(ctx, infoFileName); if (debouncerTimer.current) { clearTimeout(debouncerTimer.current); @@ -95,29 +104,14 @@ export function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) debouncerTimer.current = null; const success = await createOrUpdateInfoFile(updatedGpxFile, infoFileName, infoFile); if (success) { - ctx.setSelectedGpxFile((cur) => { - if (cur?.gpx?.options) { - cur.gpx.options.pointsGroups = cur.info?.pointsGroups; - } - if (cur?.layers?.options) { - cur.layers.options.pointsGroups = cur.info?.pointsGroups; - } - return { ...cur, cloudRedrawWpts: true }; - }); + ctx.setSelectedGpxFile((cur) => ({ ...cur, cloudRedrawWpts: true })); } else { ctx.setTrackErrorMsg({ title: 'Visibility error', msg: 'Failed to save waypoint group visibility. Please try again.', }); } - }, 1000); - } else { - if (prevFile?.gpx?.options) { - prevFile.gpx.options.pointsGroups = updatedPointsGroups; - } - if (prevFile?.layers?.options) { - prevFile.layers.options.pointsGroups = updatedPointsGroups; - } + }, VISIBILITY_DEBOUNCE_MS); } return updatedGpxFile; diff --git a/map/src/manager/track/TracksManager.js b/map/src/manager/track/TracksManager.js index a3f6db9ba..471b8b8f4 100644 --- a/map/src/manager/track/TracksManager.js +++ b/map/src/manager/track/TracksManager.js @@ -34,6 +34,15 @@ export const EMPTY_FILE_NAME = `__folder__${INFO_FILE_EXT}`; const GET_SRTM_DATA = 'get-srtm-data'; const GET_ANALYSIS = 'get-analysis'; export const PROFILE_LINE = 'line'; + +export function getResolvedPointsGroups(file) { + return file?.info?.pointsGroups ?? file?.pointsGroups; +} + +export function isWptGroupShown(pointsGroups, groupName) { + return pointsGroups?.[groupName]?.ext?.hidden !== true; +} + const PROFILE_CAR = 'car'; const PROFILE_GAP = 'gap'; export const NAN_MARKER = 99999; @@ -75,6 +84,8 @@ export function prepareLocalTrack(track) { // tracks: prepareTrack.tracks, // tracks[] will be back wpts: prepareTrack.wpts, pointsGroups: prepareTrack.pointsGroups, + /** Cloud / .info payload (e.g. pointsGroups visibility); was omitted and lost on IndexedDB round-trip */ + info: prepareTrack.info, ext: prepareTrack.ext, analysis: prepareAnalysis(prepareTrack.analysis), selected: false, diff --git a/map/src/map/layers/CloudTrackLayer.js b/map/src/map/layers/CloudTrackLayer.js index 9ec7c1bc3..85939ca0a 100644 --- a/map/src/map/layers/CloudTrackLayer.js +++ b/map/src/map/layers/CloudTrackLayer.js @@ -2,7 +2,11 @@ import { useContext, useEffect, useState } from 'react'; import AppContext, { isCloudTrack, OBJECT_TYPE_CLOUD_TRACK } from '../../context/AppContext'; import { useMap } from 'react-leaflet'; import TrackLayerProvider, { redrawWptsOnLayer, WPT_SIMPLIFY_THRESHOLD } from '../util/TrackLayerProvider'; -import TracksManager, { fitBoundsOptions, getTracksArrBounds } from '../../manager/track/TracksManager'; +import TracksManager, { + fitBoundsOptions, + getResolvedPointsGroups, + getTracksArrBounds, +} from '../../manager/track/TracksManager'; import { encodeString, useMutator } from '../../util/Utils'; import { INFO_MENU_URL, MAIN_URL_WITH_SLASH, MENU_INFO_OPEN_SIZE, TRACKS_URL } from '../../manager/GlobalManager'; import { clusterMarkers } from '../util/Clusterizer'; @@ -256,7 +260,8 @@ const CloudTrackLayer = () => { } else if (ctxTrack.cloudRedrawWpts) { // skip processing if layer is removed if (ctxTrack.gpx && map.hasLayer(ctxTrack.gpx)) { - redrawWptsOnLayer({ layer: ctxTrack.gpx }); + const pg = getResolvedPointsGroups(ctxTrack); + redrawWptsOnLayer({ layer: ctxTrack.gpx, pointsGroups: pg }); ctx.setSelectedGpxFile((o) => ({ ...o, cloudRedrawWpts: false })); } } else if (ctxTrack.showPoint) { diff --git a/map/src/map/layers/LocalClientTrackLayer.js b/map/src/map/layers/LocalClientTrackLayer.js index d38caf2e0..d14a2c6b7 100644 --- a/map/src/map/layers/LocalClientTrackLayer.js +++ b/map/src/map/layers/LocalClientTrackLayer.js @@ -7,7 +7,11 @@ import TrackLayerProvider, { TEMP_LAYER_FLAG, WPT_SIMPLIFY_THRESHOLD, } from '../util/TrackLayerProvider'; -import TracksManager, { fitBoundsOptions, isEmptyTrack } from '../../manager/track/TracksManager'; +import TracksManager, { + fitBoundsOptions, + getResolvedPointsGroups, + isEmptyTrack, +} from '../../manager/track/TracksManager'; import isEmpty from 'lodash-es/isEmpty'; import cloneDeep from 'lodash-es/cloneDeep'; import EditablePolyline from '../util/creator/EditablePolyline'; @@ -407,7 +411,8 @@ export default function LocalClientTrackLayer() { } function localRedrawWpts() { - redrawWptsOnLayer({ layer: ctxTrack.layers }); + const pg = getResolvedPointsGroups(ctxTrack); + redrawWptsOnLayer({ layer: ctxTrack.layers, pointsGroups: pg }); ctx.setSelectedGpxFile((o) => ({ ...o, localRedrawWpts: false })); } diff --git a/map/src/map/util/TrackLayerProvider.js b/map/src/map/util/TrackLayerProvider.js index 2107a2931..d67067452 100644 --- a/map/src/map/util/TrackLayerProvider.js +++ b/map/src/map/util/TrackLayerProvider.js @@ -2,7 +2,12 @@ import L from 'leaflet'; import MarkerOptions, { createPoiIcon, DEFAULT_ICON_SIZE, DEFAULT_WPT_COLOR } from '../markers/MarkerOptions'; import cloneDeep from 'lodash-es/cloneDeep'; import indexOf from 'lodash-es/indexOf'; -import TracksManager, { GPX_FILE_TYPE, isProtectedSegment } from '../../manager/track/TracksManager'; +import TracksManager, { + getResolvedPointsGroups, + GPX_FILE_TYPE, + isProtectedSegment, + isWptGroupShown, +} from '../../manager/track/TracksManager'; import { getFavoriteId } from '../../manager/FavoritesManager'; import EditablePolyline from './creator/EditablePolyline'; import { clusterMarkers, addMarkerTooltip, removeTooltip } from './Clusterizer'; @@ -24,7 +29,7 @@ export const DEFAULT_TRACK_LINE_WEIGHT = 7; function createLayersByTrackData({ data, ctx, map, groupId, type = GPX_FILE_TYPE, simplifyWpts = false }) { const layers = []; - const emptyInfo = data?.info && data.info.width === '' && data.info.color === '#00000000'; + const emptyInfo = data?.info?.width === '' && data.info.color === '#00000000'; const trackAppearance = data?.info && !emptyInfo ? { @@ -49,7 +54,18 @@ function createLayersByTrackData({ data, ctx, map, groupId, type = GPX_FILE_TYPE } } }); - parseWpt({ points: data.wpts, layers, ctx, data, map, simplify: simplifyWpts, groupId, type }); + const trackPointsGroups = getResolvedPointsGroups(data); + parseWpt({ + points: data.wpts, + layers, + ctx, + data, + map, + simplify: simplifyWpts, + groupId, + type, + pointsGroups: trackPointsGroups, + }); const trackName = data?.name; if (trackName) { @@ -63,8 +79,6 @@ function createLayersByTrackData({ data, ctx, map, groupId, type = GPX_FILE_TYPE if (layers.length > 0) { let layersGroup = new L.FeatureGroup(layers); layersGroup.options.type = type; - const pointsGroups = data?.info?.pointsGroups ?? data?.pointsGroups; - layersGroup.options.pointsGroups = pointsGroups; return layersGroup; } } @@ -451,6 +465,7 @@ function parseWpt({ simplify = false, groupId = null, type = GPX_FILE_TYPE, + pointsGroups = null, }) { const zoom = map.getZoom(); const lat = map.getCenter().lat; @@ -463,6 +478,7 @@ function parseWpt({ isFavorites: true, }) : null; + const resolvedPointsGroups = pointsGroups ?? getResolvedPointsGroups(data); points?.forEach((point) => { let opt = {}; const icon = createPoiIcon({ point, color: point.color, background: point.background, icon: point.icon }); @@ -507,6 +523,7 @@ function parseWpt({ return; } marker.options.idObj = getFavoriteId(marker); + bindWptVisibilityOnAdd(marker, resolvedPointsGroups); if (ctx && map && data) { if (type === GPX_FILE_TYPE) { marker.on('click', (e) => { @@ -519,13 +536,6 @@ function parseWpt({ }; ctx.setSelectedWpt(wpt); }); - marker.on('add', (e) => { - const pointsGroups = data.info?.pointsGroups || data.pointsGroups; - const visible = pointsGroups?.[marker.options.category]?.ext?.hidden !== true; - if (e.target?._icon?.style) { - e.target._icon.style.display = visible ? '' : 'none'; - } - }); } addMarkerTooltip({ marker, @@ -728,20 +738,31 @@ function createEditableTempLPolyline(start, end, map, ctx) { return polylineTemp; } -export function redrawWptsOnLayer({ layer }) { - if (layer) { - const pointsGroups = layer.options?.pointsGroups; - layer.getLayers().forEach((l) => { - if (l instanceof L.Marker && l.options?.wpt) { - if (l._icon?.style) { - const category = l.options?.category || ''; - const visible = pointsGroups?.[category]?.ext?.hidden !== true; - // show Wpt on layer: visible - '' , hidden - 'none' - l._icon.style.display = visible ? '' : 'none'; - } - } - }); +// set visibility each time the marker is added to the map (Leaflet 'add' event) +// visible - '' , hidden - 'none' +export function bindWptVisibilityOnAdd(marker, pointsGroups) { + if (!marker?.options?.wpt || !pointsGroups) { + return; } + marker.on('add', (e) => { + const el = e.target?._icon; + if (!el?.style) { + return; + } + const category = e.target.options?.category ?? ''; + el.style.display = isWptGroupShown(pointsGroups, category) ? '' : 'none'; + }); +} + +// for updating markers +export function redrawWptsOnLayer({ layer, pointsGroups }) { + if (!layer || !pointsGroups) return; + layer.getLayers().forEach((l) => { + if (l instanceof L.Marker && l.options?.wpt && l._icon?.style) { + const category = l.options.category || ''; + l._icon.style.display = isWptGroupShown(pointsGroups, category) ? '' : 'none'; + } + }); } const TrackLayerProvider = { diff --git a/map/src/map/util/creator/EditableMarker.js b/map/src/map/util/creator/EditableMarker.js index b566fe025..d5ba9c88f 100644 --- a/map/src/map/util/creator/EditableMarker.js +++ b/map/src/map/util/creator/EditableMarker.js @@ -3,7 +3,11 @@ import MarkerOptions from '../../markers/MarkerOptions'; import TrackLayerProvider from '../TrackLayerProvider'; import indexOf from 'lodash-es/indexOf'; import cloneDeep from 'lodash-es/cloneDeep'; -import TracksManager, { isPointUnrouted } from '../../../manager/track/TracksManager'; +import TracksManager, { + getResolvedPointsGroups, + isPointUnrouted, + isWptGroupShown, +} from '../../../manager/track/TracksManager'; import TracksRoutingCache from '../../../context/TracksRoutingCache'; export default class EditableMarker { @@ -25,6 +29,13 @@ export default class EditableMarker { let options; let point; if (marker) { + if (marker.options?.wpt) { + const pg = getResolvedPointsGroups(this.track); + const cat = marker.options.category ?? ''; + if (!isWptGroupShown(pg, cat)) { + return null; + } + } point = marker.getLatLng(); options = marker.options; } else if (this.point) { diff --git a/map/src/map/util/creator/LocalTrackLayerHelper.js b/map/src/map/util/creator/LocalTrackLayerHelper.js index 87a6f25e7..dc0d36d1e 100644 --- a/map/src/map/util/creator/LocalTrackLayerHelper.js +++ b/map/src/map/util/creator/LocalTrackLayerHelper.js @@ -1,8 +1,8 @@ import EditableMarker from './EditableMarker'; import L from 'leaflet'; -import TrackLayerProvider, { TEMP_LAYER_FLAG, WPT_SIMPLIFY_THRESHOLD } from '../TrackLayerProvider'; +import TrackLayerProvider, { redrawWptsOnLayer, TEMP_LAYER_FLAG, WPT_SIMPLIFY_THRESHOLD } from '../TrackLayerProvider'; import isEmpty from 'lodash-es/isEmpty'; -import TracksManager from '../../../manager/track/TracksManager'; +import TracksManager, { getResolvedPointsGroups } from '../../../manager/track/TracksManager'; import EditablePolyline from './EditablePolyline'; import { LOCAL_TRACKS_LAYERS_ID } from '../../layers/LocalClientTrackLayer'; import { addLayerToMap } from '../MapManager'; @@ -57,6 +57,7 @@ export function updateLayers({ map, ctx, localLayers, ctxTrack, points, wpts, tr if (points?.length > 0) { TrackLayerProvider.parsePoints({ map, ctx, points, layers, draggable: true }); } + const trackPointsGroups = getResolvedPointsGroups(ctxTrack); if (wpts?.length > 0) { TrackLayerProvider.parseWpt({ points: wpts, @@ -64,6 +65,7 @@ export function updateLayers({ map, ctx, localLayers, ctxTrack, points, wpts, tr map, ctx, simplify: wpts?.length >= WPT_SIMPLIFY_THRESHOLD, + pointsGroups: trackPointsGroups, }); } layers = createEditableLayers(map, ctx, ctxTrack, layers); @@ -97,6 +99,7 @@ export function updateLayers({ map, ctx, localLayers, ctxTrack, points, wpts, tr }); trackLayers.options.type = LOCAL_TRACKS_LAYERS_ID; addLayerToMap(map, trackLayers, 'update-local-track-layers'); + redrawWptsOnLayer({ layer: trackLayers, pointsGroups: trackPointsGroups }); // add active tracks to map for visibility during editing if (!isEmpty(localLayers)) { @@ -123,7 +126,9 @@ function createEditableLayers(map, ctx, ctxTrack, layers) { layers.forEach((layer) => { if (layer instanceof L.Marker) { const editableMarker = new EditableMarker(map, ctx, null, layer, ctxTrack).create(); - res.push(editableMarker); + if (editableMarker) { + res.push(editableMarker); + } } else if (layer instanceof L.Polyline) { const editablePolyline = new EditablePolyline(map, ctx, null, layer, ctxTrack).create(); res.push(editablePolyline); From 7110615193764956463f44f1dc9ac705f3be4b0b Mon Sep 17 00:00:00 2001 From: Kseniia Velychko Date: Mon, 23 Mar 2026 08:58:47 +0200 Subject: [PATCH 20/24] optimize .info sync: skip unchanged uploads, fix missing updatetime on create --- .../manager/track/TrackAppearanceManager.js | 42 +++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/map/src/manager/track/TrackAppearanceManager.js b/map/src/manager/track/TrackAppearanceManager.js index c0d69c1df..fc176715b 100644 --- a/map/src/manager/track/TrackAppearanceManager.js +++ b/map/src/manager/track/TrackAppearanceManager.js @@ -16,6 +16,15 @@ export function sanitizePointsGroups(pointsGroups = {}) { return result; } +/** Build the canonical `.info` payload for a track (pointsGroups + future fields). */ +export function buildInfoPayload(gpxFile) { + const pointsGroups = getResolvedPointsGroups(gpxFile); + return { + ...gpxFile?.info, + pointsGroups: isEmpty(pointsGroups) ? {} : sanitizePointsGroups(pointsGroups), + }; +} + function findInfoFile(ctx, infoFileName) { return ctx.listFiles?.uniqueFiles?.find((f) => f?.name === infoFileName); } @@ -23,16 +32,16 @@ function findInfoFile(ctx, infoFileName) { /** * Upload (create or update) a track's `.info` file on the server. * + * @param {Object} ctx App context (used to register newly created `.info` in `listFiles`) * @param {Object} gpxFile Object with at least `name` and `info` (the payload to persist) * @param {string} infoFileName Cloud path for the `.info` file * @param {Object|null} infoFile Existing file entry from `listFiles` (carries `updatetimems` for update) * @returns {Promise} */ -export async function createOrUpdateInfoFile(gpxFile, infoFileName, infoFile) { +export async function createOrUpdateInfoFile(ctx, gpxFile, infoFileName, infoFile) { if (!gpxFile?.name) { return false; } - const convertedData = new TextEncoder().encode(JSON.stringify(gpxFile.info)); const zipped = require('pako').gzip(convertedData, { to: 'Uint8Array' }); const blob = new Blob([zipped.buffer], { type: 'application/json' }); @@ -47,6 +56,8 @@ export async function createOrUpdateInfoFile(gpxFile, infoFileName, infoFile) { if (res?.data?.updatetime) { if (infoFile) { infoFile.updatetimems = res.data.updatetime; + } else { + ctx.listFiles?.uniqueFiles?.push({ name: infoFileName, updatetimems: res.data.updatetime }); } return true; } @@ -55,16 +66,30 @@ export async function createOrUpdateInfoFile(gpxFile, infoFileName, infoFile) { /** * Sync the current track's `.info` to cloud after GPX upload. + * Skips the request when nothing has changed (infoChanged flag). */ export async function syncCloudTrackInfo(ctx, cloudGpxName) { const selectedFile = ctx.selectedGpxFile; - const pointsGroups = getResolvedPointsGroups(selectedFile); - if (isEmpty(pointsGroups)) return; + if (!selectedFile) return; - const info = { ...selectedFile?.info, pointsGroups: sanitizePointsGroups(pointsGroups) }; const infoFileName = cloudGpxName + INFO_FILE_EXT; const infoFile = findInfoFile(ctx, infoFileName); - await createOrUpdateInfoFile({ name: selectedFile?.name || cloudGpxName, info }, infoFileName, infoFile); + + if (infoFile && !selectedFile.infoChanged) return; + + const payload = buildInfoPayload(selectedFile); + + if (!infoFile && isEmpty(payload.pointsGroups)) return; + + const success = await createOrUpdateInfoFile( + ctx, + { name: selectedFile?.name || cloudGpxName, info: payload }, + infoFileName, + infoFile + ); + if (success) { + ctx.setSelectedGpxFile((prev) => ({ ...prev, infoChanged: false })); + } } /** @@ -83,6 +108,7 @@ export function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) const updatedGpxFile = { ...prevFile, + infoChanged: true, info: { ...prevFile.info, pointsGroups: updatedPointsGroups, @@ -102,9 +128,9 @@ export function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) } debouncerTimer.current = setTimeout(async () => { debouncerTimer.current = null; - const success = await createOrUpdateInfoFile(updatedGpxFile, infoFileName, infoFile); + const success = await createOrUpdateInfoFile(ctx, updatedGpxFile, infoFileName, infoFile); if (success) { - ctx.setSelectedGpxFile((cur) => ({ ...cur, cloudRedrawWpts: true })); + ctx.setSelectedGpxFile((cur) => ({ ...cur, cloudRedrawWpts: true, infoChanged: false })); } else { ctx.setTrackErrorMsg({ title: 'Visibility error', From 6e23d0e146470a472cf15f190703932af5a2c4c1 Mon Sep 17 00:00:00 2001 From: Kseniia Velychko Date: Mon, 23 Mar 2026 09:57:49 +0200 Subject: [PATCH 21/24] move hidden to top-level group field since it's modified on the web --- map/src/manager/FavoritesManager.js | 6 +++--- map/src/manager/track/TrackAppearanceManager.js | 7 +++---- map/src/manager/track/TracksManager.js | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/map/src/manager/FavoritesManager.js b/map/src/manager/FavoritesManager.js index ea834d62a..5a6232927 100644 --- a/map/src/manager/FavoritesManager.js +++ b/map/src/manager/FavoritesManager.js @@ -257,9 +257,9 @@ function createGroup(file) { function addHidden({ pointsGroups, groupName, favArr, mapId, menuId }) { groupName = normalizeFavoritePointsGroupName(groupName); let hidden = false; - if (pointsGroups && pointsGroups[groupName]) { - if (pointsGroups[groupName].ext.hidden !== undefined) { - hidden = pointsGroups[groupName].ext.hidden; + if (pointsGroups?.[groupName]) { + if (pointsGroups[groupName].hidden !== undefined) { + hidden = pointsGroups[groupName].hidden; } else { hidden = isHidden(pointsGroups, groupName); } diff --git a/map/src/manager/track/TrackAppearanceManager.js b/map/src/manager/track/TrackAppearanceManager.js index fc176715b..08838115b 100644 --- a/map/src/manager/track/TrackAppearanceManager.js +++ b/map/src/manager/track/TrackAppearanceManager.js @@ -9,9 +9,8 @@ const VISIBILITY_DEBOUNCE_MS = 1000; export function sanitizePointsGroups(pointsGroups = {}) { const result = {}; for (const [name, group] of Object.entries(pointsGroups)) { - const { points: _pts, ext, ...rest } = group || {}; - const { points: _extPts, ...cleanExt } = ext || {}; - result[name] = { ...rest, ext: cleanExt }; + const { points: _pts, ext: _ext, ...rest } = group || {}; + result[name] = rest; } return result; } @@ -103,7 +102,7 @@ export function updateGroupsVisibility(ctx, groupNames, hidden, debouncerTimer) (groupNames || Object.keys(updatedPointsGroups)).forEach((name) => { const group = updatedPointsGroups[name] || {}; - updatedPointsGroups[name] = { ...group, ext: { ...group.ext, hidden } }; + updatedPointsGroups[name] = { ...group, hidden }; }); const updatedGpxFile = { diff --git a/map/src/manager/track/TracksManager.js b/map/src/manager/track/TracksManager.js index 471b8b8f4..b0bea4bb7 100644 --- a/map/src/manager/track/TracksManager.js +++ b/map/src/manager/track/TracksManager.js @@ -40,7 +40,7 @@ export function getResolvedPointsGroups(file) { } export function isWptGroupShown(pointsGroups, groupName) { - return pointsGroups?.[groupName]?.ext?.hidden !== true; + return pointsGroups?.[groupName]?.hidden !== true; } const PROFILE_CAR = 'car'; From 9edd3e2269ad183814e3f4f6fcb18c113b0395b7 Mon Sep 17 00:00:00 2001 From: Dima-1 Date: Mon, 23 Mar 2026 16:28:51 +0200 Subject: [PATCH 22/24] Fix massVisible switch state to depend on each group's visibility --- .../components/tabs/WaypointsTab.jsx | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/map/src/infoblock/components/tabs/WaypointsTab.jsx b/map/src/infoblock/components/tabs/WaypointsTab.jsx index 63c14b280..07baa14fc 100644 --- a/map/src/infoblock/components/tabs/WaypointsTab.jsx +++ b/map/src/infoblock/components/tabs/WaypointsTab.jsx @@ -18,7 +18,7 @@ const WaypointGroup = ({ defaultOpen, defaultVisible = true, massOpen, - massVisible, + massCommand, debouncerTimer, }) => { const [open, setOpen] = useState(defaultOpen); @@ -41,8 +41,8 @@ const WaypointGroup = ({ }, [massOpen]); useEffect(() => { - mounted && setVisible(massVisible); - }, [massVisible]); + mounted && setVisible(massCommand.value); + }, [massCommand.version]); const point = points[0].wpt; const iconHTML = createPoiIcon({ point, color: point.color, background: point.background, icon: point.icon }) @@ -269,19 +269,30 @@ export default function WaypointsTab() { return groups; } - const [showMass, setShowMass] = useState(false); - const [massOpen, setMassOpen] = useState(false); - const [massVisible, setMassVisible] = useState(() => { + function isMassVisible() { const pointsGroups = getResolvedPointsGroups(ctx.selectedGpxFile); const groupKeys = Object.keys(pointsGroups || {}); return groupKeys.length > 0 && groupKeys.every((g) => isWptGroupShown(pointsGroups, g)); - }); + } + + const [showMass, setShowMass] = useState(false); + const [massOpen, setMassOpen] = useState(false); + const [massVisible, setMassVisible] = useState(isMassVisible()); const debouncerTimer = useRef(null); + const [massCommand, setMassCommand] = useState({ version: 0, value: true }); + + useEffect(() => { + setMassVisible(isMassVisible()); + }, [ctx.selectedGpxFile?.info?.pointsGroups]); const switchMassOpen = () => setMassOpen(!massOpen); const switchMassVisible = () => { const newMassVisible = !massVisible; setMassVisible(newMassVisible); + setMassCommand((prev) => ({ + version: prev.version + 1, + value: newMassVisible, + })); const groupNames = Object.keys(getResolvedPointsGroups(ctx.selectedGpxFile) || {}); updateGroupsVisibility(ctx, groupNames, !newMassVisible, debouncerTimer); }; @@ -311,7 +322,7 @@ export default function WaypointsTab() { points={groups[g]} defaultOpen={keys.length === 1} defaultVisible={isWptGroupShown(pointsGroups, g)} - massVisible={massVisible} + massCommand={massCommand} massOpen={massOpen} debouncerTimer={debouncerTimer} /> From 80421cfeef10ab8bcedb7f143b0966dc83553382 Mon Sep 17 00:00:00 2001 From: Dima-1 Date: Mon, 23 Mar 2026 18:06:27 +0200 Subject: [PATCH 23/24] Revert "Fix massVisible switch state to depend on each group's visibility" This reverts commit 9edd3e2269ad183814e3f4f6fcb18c113b0395b7. --- .../components/tabs/WaypointsTab.jsx | 27 ++++++------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/map/src/infoblock/components/tabs/WaypointsTab.jsx b/map/src/infoblock/components/tabs/WaypointsTab.jsx index 07baa14fc..63c14b280 100644 --- a/map/src/infoblock/components/tabs/WaypointsTab.jsx +++ b/map/src/infoblock/components/tabs/WaypointsTab.jsx @@ -18,7 +18,7 @@ const WaypointGroup = ({ defaultOpen, defaultVisible = true, massOpen, - massCommand, + massVisible, debouncerTimer, }) => { const [open, setOpen] = useState(defaultOpen); @@ -41,8 +41,8 @@ const WaypointGroup = ({ }, [massOpen]); useEffect(() => { - mounted && setVisible(massCommand.value); - }, [massCommand.version]); + mounted && setVisible(massVisible); + }, [massVisible]); const point = points[0].wpt; const iconHTML = createPoiIcon({ point, color: point.color, background: point.background, icon: point.icon }) @@ -269,30 +269,19 @@ export default function WaypointsTab() { return groups; } - function isMassVisible() { + const [showMass, setShowMass] = useState(false); + const [massOpen, setMassOpen] = useState(false); + const [massVisible, setMassVisible] = useState(() => { const pointsGroups = getResolvedPointsGroups(ctx.selectedGpxFile); const groupKeys = Object.keys(pointsGroups || {}); return groupKeys.length > 0 && groupKeys.every((g) => isWptGroupShown(pointsGroups, g)); - } - - const [showMass, setShowMass] = useState(false); - const [massOpen, setMassOpen] = useState(false); - const [massVisible, setMassVisible] = useState(isMassVisible()); + }); const debouncerTimer = useRef(null); - const [massCommand, setMassCommand] = useState({ version: 0, value: true }); - - useEffect(() => { - setMassVisible(isMassVisible()); - }, [ctx.selectedGpxFile?.info?.pointsGroups]); const switchMassOpen = () => setMassOpen(!massOpen); const switchMassVisible = () => { const newMassVisible = !massVisible; setMassVisible(newMassVisible); - setMassCommand((prev) => ({ - version: prev.version + 1, - value: newMassVisible, - })); const groupNames = Object.keys(getResolvedPointsGroups(ctx.selectedGpxFile) || {}); updateGroupsVisibility(ctx, groupNames, !newMassVisible, debouncerTimer); }; @@ -322,7 +311,7 @@ export default function WaypointsTab() { points={groups[g]} defaultOpen={keys.length === 1} defaultVisible={isWptGroupShown(pointsGroups, g)} - massCommand={massCommand} + massVisible={massVisible} massOpen={massOpen} debouncerTimer={debouncerTimer} /> From 5f2e25dd42057ecf1c78ba5a79f9157f7fd86fb3 Mon Sep 17 00:00:00 2001 From: Dima-1 Date: Mon, 23 Mar 2026 20:46:34 +0200 Subject: [PATCH 24/24] Fix massVisible switch state to depend on each group's visibility --- .../components/tabs/WaypointsTab.jsx | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/map/src/infoblock/components/tabs/WaypointsTab.jsx b/map/src/infoblock/components/tabs/WaypointsTab.jsx index 63c14b280..7009bc0f0 100644 --- a/map/src/infoblock/components/tabs/WaypointsTab.jsx +++ b/map/src/infoblock/components/tabs/WaypointsTab.jsx @@ -41,8 +41,8 @@ const WaypointGroup = ({ }, [massOpen]); useEffect(() => { - mounted && setVisible(massVisible); - }, [massVisible]); + setVisible(defaultVisible); + }, [defaultVisible, massVisible]); const point = points[0].wpt; const iconHTML = createPoiIcon({ point, color: point.color, background: point.background, icon: point.icon }) @@ -269,15 +269,21 @@ export default function WaypointsTab() { return groups; } - const [showMass, setShowMass] = useState(false); - const [massOpen, setMassOpen] = useState(false); - const [massVisible, setMassVisible] = useState(() => { + function isMassVisible() { const pointsGroups = getResolvedPointsGroups(ctx.selectedGpxFile); const groupKeys = Object.keys(pointsGroups || {}); - return groupKeys.length > 0 && groupKeys.every((g) => isWptGroupShown(pointsGroups, g)); - }); + return groupKeys.length > 0 && groupKeys.some((g) => isWptGroupShown(pointsGroups, g)); + } + + const [showMass, setShowMass] = useState(false); + const [massOpen, setMassOpen] = useState(false); + const [massVisible, setMassVisible] = useState(isMassVisible()); const debouncerTimer = useRef(null); + useEffect(() => { + setMassVisible(isMassVisible()); + }, [ctx.selectedGpxFile?.info?.pointsGroups]); + const switchMassOpen = () => setMassOpen(!massOpen); const switchMassVisible = () => { const newMassVisible = !massVisible;