Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
0a6faeb
Search in tracks
Dima-1 Apr 30, 2026
83720d0
Open track from search
Dima-1 Apr 30, 2026
74a17fd
Fix search after deleting a track. Add a test.
Dima-1 May 1, 2026
d40ca15
Fix return to search result. Improve test.
Dima-1 May 1, 2026
e6fffcc
Handle new type according to existing pattern
Dima-1 May 4, 2026
5d3d775
Merge search results with results from uniqueFiles search
Dima-1 May 6, 2026
4fa726b
Remove openedFromSearch
Dima-1 May 7, 2026
786657b
Rename icon
Dima-1 May 7, 2026
8c87c30
Search in favorites
Dima-1 May 8, 2026
392a1d7
Fix favorite search result item UI
Dima-1 May 11, 2026
cb8e110
Merge branch 'main' into search-fav-or-track
Dima-1 May 12, 2026
c06ef11
Fix secondaryMarker hover icon
Dima-1 May 12, 2026
3883a27
Fix 36 test, refactoring
Dima-1 May 12, 2026
7cc0b82
Add test 37-search-favorite
Dima-1 May 12, 2026
55632ce
Use collator instead normalize('NFKD')
Dima-1 May 13, 2026
8a912b2
FIx review: replace for with flatMap + filter, getFavoriteMenuIconHtm…
Dima-1 May 13, 2026
3660872
Fix back cross and menu flashing
Dima-1 May 13, 2026
4268c5b
Remove code duplication
Dima-1 May 14, 2026
223b924
Fix hover
Dima-1 May 14, 2026
63c4337
Combine tests. Make search collator static.
Dima-1 May 14, 2026
21425f5
Merge branch 'main' into search-fav-or-track
Dima-1 May 14, 2026
9511dc5
Fix favorite state
Dima-1 May 15, 2026
901662d
Fix favorite state refactor
Dima-1 May 15, 2026
e04d8df
Fix remove returnToSearch
Dima-1 May 15, 2026
cff1341
Fix search refresh
Dima-1 May 15, 2026
31008ef
Merge SearchObjectManager in SearchManager, extract FavoriteSearchRes…
Dima-1 May 18, 2026
0fe9313
Refactoring marker use, create common component FavoriteItemContent
Dima-1 May 18, 2026
7b1eacd
Move searchIncludes to SearchLayer
Dima-1 May 18, 2026
ac6ec4c
Remove unnecessary mapObj check. Fix test.
Dima-1 May 19, 2026
73b326a
Refactoring and add comments
alisa911 May 19, 2026
1a39cae
Refactoring
alisa911 May 19, 2026
610b71b
Rallback
alisa911 May 19, 2026
07f241b
Recessary complexity in favorite search results
alisa911 May 19, 2026
b15d4b8
Fix hide markers
Dima-1 May 20, 2026
982f19c
Remove wrapper method, rename cloudFeatures, refactor getTrackInfoText
Dima-1 May 21, 2026
061216c
Fix URL
Dima-1 May 21, 2026
80772da
Format
Dima-1 May 21, 2026
95369b9
Merge branch 'main' into search-fav-or-track
Dima-1 May 21, 2026
3d4cc46
Fix search results for favorites and tracks
alisa911 May 21, 2026
9ed1f15
Remove icon
alisa911 May 21, 2026
14dfdcd
Remove unnecessary changes
alisa911 May 21, 2026
ea59cb8
Refactoring
alisa911 May 21, 2026
c33226f
Fix search results restore for tracks and favorites
alisa911 May 21, 2026
e7515cb
Remove unnecessary changes
alisa911 May 21, 2026
8d5def1
Fix track info
alisa911 May 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions map/public/images/map_icons/ic_action_favorite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions map/public/images/map_icons/ic_action_polygon_dark.svg
Comment thread
Dima-1 marked this conversation as resolved.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 31 additions & 6 deletions map/src/infoblock/components/InformationBlock.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@ import {
MAIN_URL_WITH_SLASH,
MENU_INFO_CLOSE_SIZE,
MENU_INFO_OPEN_SIZE,
SEARCH_RESULT_URL,
SEARCH_URL,
SHARE_FILE_MAIN_URL,
SHARE_MENU_URL,
TRACKS_URL,
} from '../../manager/GlobalManager';
import { buildSearchParamsFromQuery } from '../../util/hooks/search/useSearchNav';
import { isVisibleTrack } from '../../menu/visibletracks/VisibleTracks';
import WptDetails from './wpt/WptDetails';
import WptPhotoList from './wpt/WptPhotoList';
Expand Down Expand Up @@ -366,11 +369,13 @@ export default function InformationBlock({
function handleCloseTrackContextMenu() {
setShowInfoBlock(false);

const wasCloudTrack = isCloudTrack(ctx);

if (!isTrackAnalyzer(ctx)) {
closeTrackAnalyzer();
}
if (ctx.selectedGpxFile.mapObj) {
closeMapObjectMenu();
if (ctx.selectedGpxFile?.mapObj) {
closeMapObjectMenu({ wasCloudTrack });
} else if (isCloudTrack(ctx)) {
closeCloudTrack();
} else if (isLocalTrack(ctx)) {
Expand All @@ -381,10 +386,30 @@ export default function InformationBlock({
}
}

function closeMapObjectMenu() {
ctx.setCloseMapObj(true);
if (!isEmpty(ctx.gpxFiles) && ctx.gpxFiles[ctx.selectedGpxFile.name]) {
ctx.mutateGpxFiles((o) => (o[ctx.selectedGpxFile.name].mapObj = null));
function closeMapObjectMenu({ wasCloudTrack } = {}) {
Comment thread
Dima-1 marked this conversation as resolved.
Outdated
const name = ctx.selectedGpxFile?.name;
if (name && !isEmpty(ctx.gpxFiles) && ctx.gpxFiles[name]) {
ctx.mutateGpxFiles((o) => (o[name].mapObj = null));
}

const returnToSearch =
wasCloudTrack &&
ctx.searchQuery &&
(ctx.searchQuery.query || ctx.searchQuery.type) &&
ctx.searchResult?.features?.length;

if (wasCloudTrack) {
setTrackName(null);
}

if (returnToSearch) {
navigate({
pathname: MAIN_URL_WITH_SLASH + SEARCH_URL + SEARCH_RESULT_URL,
search: buildSearchParamsFromQuery(ctx.searchQuery),
hash: location.hash,
});
} else {
ctx.setCloseMapObj(true);
}
}

Expand Down
46 changes: 40 additions & 6 deletions map/src/manager/FavoritesManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,20 @@ export function getSize(group, t) {
: 'empty';
}

export function getFavoriteMenuIconHtml({ wpt = null, icon, color, background } = {}) {
Comment thread
alisa911 marked this conversation as resolved.
const point = wpt ?? {};
const rawHtml = createPoiIcon({
point,
icon: icon ?? wpt?.icon,
color: color ?? wpt?.color,
background: background ?? wpt?.background,
hasBackgroundLight: false,
}).options.html;
const bg = background ?? wpt?.background;
Comment thread
Dima-1 marked this conversation as resolved.
Outdated

return changeIconSizeWpt(removeShadowFromIconWpt(rawHtml), 18, 30, bg);
}

export function getFavMenuListByLayers({ layers, wpts, currentLoc, pointsGroups = null }) {
let markerList = [];
Object.values(layers).forEach((value) => {
Expand All @@ -705,14 +719,9 @@ export function getFavMenuListByLayers({ layers, wpts, currentLoc, pointsGroups
return;
}
const appearance = resolveWptAppearance(wpt, pointsGroups);
const icon = createPoiIcon({
point: wpt,
...appearance,
hasBackgroundLight: false,
}).options.html;
const marker = {
name: value.options.name,
icon: changeIconSizeWpt(removeShadowFromIconWpt(icon), 18, 30, appearance.background),
icon: getFavoriteMenuIconHtml({ wpt, ...appearance }),
layer: value,
color: appearance.color,
background: appearance.background,
Expand All @@ -726,6 +735,31 @@ export function getWptByTitle(title, wpts) {
return wpts.find((wpt) => wpt.name === title);
}

/** Build menu marker + group for opening a favorite from search (requires map markers loaded). */
export function resolveFavoriteMarkerForSearch(ctx, groupId, wptName) {
const group = ctx.favorites?.groups?.find((g) => g.id === groupId);
const mapObj = ctx.favorites?.mapObjs?.[groupId];
if (!group || !mapObj?.wpts || !mapObj?.markers?._layers) {
return null;
}
const wpt = getWptByTitle(wptName, mapObj.wpts);
if (!wpt) {
return null;
}
const layer = Object.values(mapObj.markers._layers).find((l) => l.options?.name === wptName);
if (!layer) {
return null;
}
const marker = {
name: wptName,
icon: getFavoriteMenuIconHtml({ wpt }),
Comment thread
Dima-1 marked this conversation as resolved.
Outdated
layer,
color: wpt.color,
background: wpt.background,
};
return { group, marker };
}

export function addLocDist({ location, markers = null, wpts = null }) {
let res = [];
if (location && location !== LOCATION_UNAVAILABLE) {
Expand Down
127 changes: 125 additions & 2 deletions map/src/map/layers/SearchLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
POI_NAME,
TYPE_OSM_TAG,
TYPE_OSM_VALUE,
COLOR_NAME_EXTENSION,
BACKGROUND_TYPE_EXTENSION,
} from '../../infoblock/components/wpt/WptTagsProvider';
import { changeIconColor, createPoiIcon, DEFAULT_ICON_SIZE } from '../markers/MarkerOptions';
import i18n from '../../i18n';
Expand All @@ -37,6 +39,9 @@ import { hideMarkersNearPin } from '../util/MarkerSelectionService';
import { POI_OBJECTS_KEY, useRecentDataSaver } from '../../util/hooks/menu/useRecentDataSaver';
import { useNavigate } from 'react-router-dom';
import { getCurrentTimeParams } from '../../util/Utils';
import { getGpxFiles, prepareName, EMPTY_FILE_NAME } from '../../manager/track/TracksManager';
import { resolveFavoriteMarkerForSearch } from '../../manager/FavoritesManager';
import { addFavoriteToMap } from '../../menu/favorite/FavoriteItem';

export const SEARCH_TYPE_CATEGORY = 'category';
export const SEARCH_LAYER_ID = 'search-layer';
Expand All @@ -45,6 +50,7 @@ export const SEARCH_ICON_MAP_LOCATION = 'location';
export const SEARCH_ICON_MAP_BUILDING = 'house';
export const SEARCH_ICON_MAP_STREET = 'street';
export const SEARCH_ICON_MAP_INTERSECTION = 'intersection';
export const SEARCH_ICON_MAP_GPX_TRACK = 'gpx_track';

export const ZOOM_TO_MAP = 17;

Expand All @@ -58,13 +64,18 @@ export const searchTypeMap = {
CITY: 'CITY',
TOWN: 'TOWN',
VILLAGE: 'VILLAGE',
GPX_TRACK: 'GPX_TRACK',
FAVORITE: 'FAVORITE',
};

export const FAVORITE_HIT_GROUP_ID = 'favoriteHitGroupId';

export const typeIconMap = {
[searchTypeMap.LOCATION]: SEARCH_ICON_MAP_LOCATION,
[searchTypeMap.HOUSE]: SEARCH_ICON_MAP_BUILDING,
[searchTypeMap.STREET]: SEARCH_ICON_MAP_STREET,
[searchTypeMap.INTERSECTION]: SEARCH_ICON_MAP_INTERSECTION,
[searchTypeMap.GPX_TRACK]: SEARCH_ICON_MAP_GPX_TRACK,
};

export function getObjIdSearch(obj) {
Expand All @@ -76,6 +87,88 @@ export function getObjIdSearch(obj) {
return `${obj.geometry.coordinates[1]},${obj.geometry.coordinates[0]}`;
}

function normalize(s) {
return s?.normalize('NFKD').replace(/\p{M}/gu, '').toLowerCase().trim();
Comment thread
Dima-1 marked this conversation as resolved.
Outdated
}

export function searchFavoriteFeatures({ favorites, query }) {
Comment thread
Dima-1 marked this conversation as resolved.
Outdated
if (!query || !favorites?.groups?.length || !favorites.mapObjs) {
return [];
}
const q = normalize(query);
Comment thread
Dima-1 marked this conversation as resolved.
Outdated
if (!q) {
return [];
}

const features = [];

for (const group of favorites.groups) {
Comment thread
Dima-1 marked this conversation as resolved.
Outdated
if (!group?.id) {
continue;
}
const mapObj = favorites.mapObjs[group.id];
const wpts = mapObj?.wpts;
if (!wpts?.length) {
continue;
}

for (const wpt of wpts) {
if (!wpt?.name) {
continue;
}
const nameNorm = normalize(wpt.name);
const descNorm = normalize(wpt.desc ?? '');
Comment thread
Dima-1 marked this conversation as resolved.
Outdated
const textHit = nameNorm.includes(q) || (descNorm && descNorm.includes(q));
Comment thread
Dima-1 marked this conversation as resolved.
Outdated
if (!textHit) {
continue;
}

if (wpt.lat == null || wpt.lon == null) {
Comment thread
Dima-1 marked this conversation as resolved.
Outdated
continue;
}

features.push({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [wpt.lon, wpt.lat],
},
properties: {
[CATEGORY_TYPE]: searchTypeMap.FAVORITE,
[CATEGORY_NAME]: wpt.name,
Comment thread
Dima-1 marked this conversation as resolved.
Outdated
[POI_NAME]: wpt.name,
[FAVORITE_HIT_GROUP_ID]: group.id,
[ICON_KEY_NAME]: wpt.icon,
[COLOR_NAME_EXTENSION]: wpt.color,
[BACKGROUND_TYPE_EXTENSION]: wpt.background,
[FINAL_POI_ICON_NAME]: wpt.icon,
...(wpt.address ? { address: wpt.address } : {}),
},
});
}
}

return features;
}

function searchCloudTrackFeatures({ listFiles, query }) {
if (!query || !listFiles?.uniqueFiles) return [];
const q = normalize(query);
Comment thread
Dima-1 marked this conversation as resolved.
Outdated
if (!q) return [];

return getGpxFiles(listFiles)
.filter((f) => !f.name.endsWith(EMPTY_FILE_NAME))
Comment thread
Dima-1 marked this conversation as resolved.
Outdated
.filter((f) => normalize(prepareName(f.name, true)).includes(q))
.map((f) => ({
type: 'Feature',
geometry: { type: 'Point', coordinates: [0, 0] },
properties: {
[CATEGORY_TYPE]: searchTypeMap.GPX_TRACK,
[CATEGORY_NAME]: f.name,
},
}));
}

export default function SearchLayer() {
const ctx = useContext(AppContext);
const map = useMap();
Expand Down Expand Up @@ -186,7 +279,16 @@ export default function SearchLayer() {
});
if (response?.ok) {
const data = await response.json();
ctx.setSearchResult(data);
const cloudFeatures = searchCloudTrackFeatures({
Comment thread
Dima-1 marked this conversation as resolved.
Outdated
listFiles: ctx.listFiles,
query: searchData.query,
});
const favoriteFeatures = searchFavoriteFeatures({
favorites: ctx.favorites,
query: searchData.query,
});
const features = [...cloudFeatures, ...favoriteFeatures, ...(data?.features ?? [])];
ctx.setSearchResult({ ...data, features });
} else {
ctx.setSearchResult(null);
}
Expand Down Expand Up @@ -229,10 +331,21 @@ export default function SearchLayer() {
}, [ctx.searchResult]);

function onClick(e) {
const opts = e.sourceTarget.options;
if (opts[CATEGORY_TYPE] === searchTypeMap.FAVORITE) {
const groupId = opts[FAVORITE_HIT_GROUP_ID];
const wptName = opts[POI_NAME] ?? opts[CATEGORY_NAME];
Comment thread
Dima-1 marked this conversation as resolved.
Outdated
const resolved = resolveFavoriteMarkerForSearch(ctx, groupId, wptName);
if (resolved) {
addFavoriteToMap({ group: resolved.group, marker: resolved.marker, ctx, mapObj: true });
}
return;
}

ctx.setCurrentObjectType(OBJECT_SEARCH);

const poi = {
options: e.sourceTarget.options,
options: opts,
latlng: e.sourceTarget._latlng,
mapObj: true,
};
Expand Down Expand Up @@ -274,6 +387,16 @@ export default function SearchLayer() {
iconName: obj.properties[POI_ICON_NAME],
});
icon = await getPoiIcon(obj, innerCache, finalIconName);
} else if (objType === searchTypeMap.FAVORITE) {
const p = obj.properties;
icon = createPoiIcon({
Comment thread
Dima-1 marked this conversation as resolved.
Outdated
point: {},
icon: p[ICON_KEY_NAME],
color: p[COLOR_NAME_EXTENSION],
background: p[BACKGROUND_TYPE_EXTENSION],
hasBackgroundLight: false,
});
finalIconName = p[ICON_KEY_NAME] ?? null;
} else {
finalIconName = getIconByType(objType);
icon = await getSearchIcon(obj, innerCache, finalIconName);
Expand Down
31 changes: 20 additions & 11 deletions map/src/map/util/Clusterizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { createTooltip, TOOLTIP_MAX_LENGTH } from './MapManager';
import { getObjIdSearch, searchTypeMap } from '../layers/SearchLayer';
import {
CATEGORY_TYPE,
COLOR_NAME_EXTENSION,
FINAL_POI_ICON_NAME,
ICON_KEY_NAME,
POI_ICON_NAME,
Expand Down Expand Up @@ -342,27 +343,35 @@ export function createSecondaryMarker(obj) {
}
const latlng = L.latLng(obj.geometry.coordinates[1], obj.geometry.coordinates[0]);

let finalIconName = obj.properties[FINAL_POI_ICON_NAME];
const props = obj.properties;
let finalIconName = props[FINAL_POI_ICON_NAME];
if (!finalIconName) {
if (searchTypeMap.POI === obj.properties[CATEGORY_TYPE]) {
if (searchTypeMap.POI === props[CATEGORY_TYPE]) {
finalIconName = PoiManager.getIconNameForPoiType({
iconKeyName: obj.properties[ICON_KEY_NAME],
typeOsmTag: obj.properties[TYPE_OSM_TAG],
typeOsmValue: obj.properties[TYPE_OSM_VALUE],
iconName: obj.properties[POI_ICON_NAME],
iconKeyName: props[ICON_KEY_NAME],
typeOsmTag: props[TYPE_OSM_TAG],
typeOsmValue: props[TYPE_OSM_VALUE],
iconName: props[POI_ICON_NAME],
});
} else if (searchTypeMap.FAVORITE === props[CATEGORY_TYPE]) {
finalIconName = props[ICON_KEY_NAME];
} else {
finalIconName = getIconByType(obj.properties[CATEGORY_TYPE]);
finalIconName = getIconByType(props[CATEGORY_TYPE]);
}
}

return new SimpleDotMarker(latlng, obj, {
...obj.properties,
id: obj.properties.id,
const markerOpts = {
...props,
id: props.id,
idObj: getObjIdSearch(obj),
simple: true,
[FINAL_POI_ICON_NAME]: finalIconName,
}).build();
};
if (searchTypeMap.FAVORITE === props[CATEGORY_TYPE]) {
markerOpts.fillColor = props[COLOR_NAME_EXTENSION] ?? SimpleDotMarker.defaultOptions.fillColor;
Comment thread
Dima-1 marked this conversation as resolved.
Outdated
}

return new SimpleDotMarker(latlng, obj, markerOpts).build();
}

export function addMarkerTooltip({
Expand Down
Loading