Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions map/src/context/AppContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ export const AppContextProvider = (props) => {
const [globalConfirmation, setGlobalConfirmation] = useState(null);

const [openMenu, setOpenMenu] = useState(null);
const [openMainMenu, setOpenMainMenu] = useState(false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

openMainMenu was lifted from GlobalFrame local state into global AppContext solely so GpxMapDropGeometry can read it. But the canonical visible-area logic (MapStateLayer.calcVisibleBboxParamsPx) intentionally ignores menu-open state (the expanded menu is an overlay, not a layout shift), so the overlay shouldn't need it either. Drop the parallel geometry, reuse the existing visible-bounds, and keep openMainMenu local.

const [gpxFileDrag, setGpxFileDrag] = useState({ active: false, hoverFolder: null, overMap: false });
const [openContextMenu, setOpenContextMenu] = useState(false);

const [cloudSettings, setCloudSettings] = useState({
Expand Down Expand Up @@ -632,6 +634,10 @@ export const AppContextProvider = (props) => {
setSelectedWptId,
openMenu,
setOpenMenu,
openMainMenu,
setOpenMainMenu,
gpxFileDrag,
setGpxFileDrag,
openContextMenu,
setOpenContextMenu,
prevPageUrl,
Expand Down
9 changes: 5 additions & 4 deletions map/src/frame/GlobalFrame.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import PhotosModal from '../menu/search/explore/PhotosModal';
import InstallBanner from './components/InstallBanner';
import { hideAllTracks } from '../manager/track/DeleteTrackManager';
import GlobalGraph from '../graph/mapGraph/GlobalGraph';
import TracksFileDragController from './TracksFileDragController';
import LoginContext from '../context/LoginContext';
import { poiUrlParams } from '../manager/PoiManager';
import { createUrlParams } from '../util/Utils';
Expand All @@ -47,7 +48,6 @@ const GlobalFrame = () => {

const [showInfoBlock, setShowInfoBlock] = useState(false);
const [clearState, setClearState] = useState(false);
const [openMainMenu, setOpenMainMenu] = useState(false);
const [openErrorDialog, setOpenErrorDialog] = useState(false);
const [menuInfo, setMenuInfo] = useState(null);

Expand All @@ -58,7 +58,7 @@ const GlobalFrame = () => {

const [showInstallBanner, setShowInstallBanner] = useState(false);

const MAIN_MENU_SIZE = openMainMenu ? `${MAIN_MENU_OPEN_SIZE}px` : `${MAIN_MENU_MIN_SIZE}px`;
const MAIN_MENU_SIZE = ctx.openMainMenu ? `${MAIN_MENU_OPEN_SIZE}px` : `${MAIN_MENU_MIN_SIZE}px`;
const MENU_INFO_SIZE =
menuInfo || ltx.openLoginMenu || ctx.infoBlockWidth === `${MENU_INFO_OPEN_SIZE}px`
? `${MENU_INFO_OPEN_SIZE}px`
Expand Down Expand Up @@ -415,6 +415,7 @@ const GlobalFrame = () => {
}}
>
<GlobalConfirmationDialog />
<TracksFileDragController />
<OsmAndMap mainMenuWidth={MAIN_MENU_MIN_SIZE + 'px'} menuInfoWidth={MENU_INFO_SIZE} />
{ctx.globalGraph?.show && <GlobalGraph type={ctx.globalGraph.type} />}
<GlobalAlert width={width} />
Expand All @@ -441,8 +442,8 @@ const GlobalFrame = () => {
<MainMenu
size={MAIN_MENU_SIZE}
infoSize={MENU_INFO_SIZE}
openMainMenu={openMainMenu}
setOpenMainMenu={setOpenMainMenu}
openMainMenu={ctx.openMainMenu}
setOpenMainMenu={ctx.setOpenMainMenu}
menuInfo={menuInfo}
setMenuInfo={setMenuInfo}
showInfoBlock={showInfoBlock}
Expand Down
103 changes: 103 additions & 0 deletions map/src/frame/TracksFileDragController.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { useContext, useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import AppContext from '../context/AppContext';
import LoginContext from '../context/LoginContext';
import { IMPORT_FOLDER_NAME } from '../manager/track/TracksManager';
import useCloudGpxImport from '../util/hooks/useCloudGpxImport';
import TracksMapDropOverlay from './TracksMapDropOverlay';
import TracksMenuDropOverlay from './TracksMenuDropOverlay';
import { resolveGpxDropTarget } from './TracksMapDropGeometry';

const GPX_FILE_DRAG_IDLE = { active: false, hoverFolder: null, overMap: false };

function hasFiles(e) {
return e.dataTransfer?.types?.includes('Files');
}

export default function TracksFileDragController() {
const ctx = useContext(AppContext);
const ltx = useContext(LoginContext);
const { importGpxFiles } = useCloudGpxImport();
const dragCounterRef = useRef(0);
const ctxRef = useRef(ctx);
ctxRef.current = ctx;
Comment thread
Dima-1 marked this conversation as resolved.

useEffect(() => {
Comment thread
Dima-1 marked this conversation as resolved.
const resetDrag = () => {
dragCounterRef.current = 0;
ctxRef.current.setGpxFileDrag(GPX_FILE_DRAG_IDLE);
};

const onDragEnter = (e) => {
if (!hasFiles(e) || !ltx.isProAccount()) {
return;
}
e.preventDefault();
dragCounterRef.current += 1;
};

const onDragOver = (e) => {
if (!hasFiles(e) || !ltx.isProAccount()) {
return;
}
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';

const currentCtx = ctxRef.current;
const { hoverFolder, overMap } = resolveGpxDropTarget(e.clientX, e.clientY, currentCtx);
currentCtx.setGpxFileDrag({ active: true, hoverFolder, overMap });
Comment thread
Dima-1 marked this conversation as resolved.
};
Comment thread
Dima-1 marked this conversation as resolved.

const onDragLeave = (e) => {
if (!hasFiles(e) || !ltx.isProAccount()) {
return;
}
dragCounterRef.current = Math.max(0, dragCounterRef.current - 1);
if (dragCounterRef.current === 0) {
resetDrag();
}
};

const onDrop = (e) => {
if (!hasFiles(e) || !ltx.isProAccount()) {
return;
}
e.preventDefault();

const currentCtx = ctxRef.current;
const files = Array.from(e.dataTransfer?.files || []);
const { hoverFolder, overMap } = resolveGpxDropTarget(e.clientX, e.clientY, currentCtx);
if (hoverFolder !== null) {
importGpxFiles(files, hoverFolder);
} else if (overMap) {
importGpxFiles(files, IMPORT_FOLDER_NAME);
}
resetDrag();
};

const onDragEnd = () => {
resetDrag();
};

window.addEventListener('dragenter', onDragEnter);
Comment thread
Dima-1 marked this conversation as resolved.
window.addEventListener('dragover', onDragOver);
window.addEventListener('dragleave', onDragLeave);
window.addEventListener('drop', onDrop);
window.addEventListener('dragend', onDragEnd);
return () => {
window.removeEventListener('dragenter', onDragEnter);
window.removeEventListener('dragover', onDragOver);
window.removeEventListener('dragleave', onDragLeave);
window.removeEventListener('drop', onDrop);
window.removeEventListener('dragend', onDragEnd);
};
}, [importGpxFiles, ltx.loginUser, ltx.accountInfo?.account]);

return createPortal(
<>
<TracksMapDropOverlay />
<TracksMenuDropOverlay />
</>,
document.body
);
}
159 changes: 159 additions & 0 deletions map/src/frame/TracksMapDropGeometry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { HEADER_SIZE, MAIN_MENU_MIN_SIZE, MAIN_MENU_OPEN_SIZE } from '../manager/GlobalManager';

export const OVERLAY_MARGIN = 16;

export function getVisibleMapInsets(ctx) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Visible-map-area geometry already exists: MapStateLayer.calcVisibleBboxParamsPx / getVisibleBboxInfo and mtx.fitBoundsPadding. This reimplements the same left/top/bottom insets from the same constants, and the two already diverge (this uses MAIN_MENU_OPEN_SIZE on open menu while MapStateLayer always uses MAIN_MENU_MIN_SIZE). Reuse the existing source of truth instead of a parallel computation.

const infoBlockWidthPx = Number.parseInt(String(ctx.infoBlockWidth), 10) || 0;
const bottomPx = ctx.globalGraph?.show ? ctx.globalGraph.size : 0;

const leftChromePx =
infoBlockWidthPx > 0
? MAIN_MENU_MIN_SIZE + infoBlockWidthPx
: ctx.openMainMenu
? MAIN_MENU_OPEN_SIZE
: MAIN_MENU_MIN_SIZE;

return {
top: HEADER_SIZE + OVERLAY_MARGIN,
left: leftChromePx + OVERLAY_MARGIN,
right: OVERLAY_MARGIN,
bottom: bottomPx + OVERLAY_MARGIN,
};
}

export function isPointInVisibleMap(ctx, clientX, clientY) {
const insets = getVisibleMapInsets(ctx);
const width = window.innerWidth;
const height = window.innerHeight;

return (
clientX >= insets.left &&
clientX <= width - insets.right &&
clientY >= insets.top &&
clientY <= height - insets.bottom
);
}

export function getMenuDropContainers() {
return [
document.getElementById('se-tracks-folder'),
document.getElementById('se-track-menu'),
].filter((el) => el?.hasAttribute('data-cloud-track-folder'));
}

export function getMenuOverlayContainer(hoverFolder) {
if (hoverFolder === null) {
return null;
}

const openFolder = document.getElementById('se-tracks-folder');
if (openFolder?.hasAttribute('data-cloud-track-folder')) {
const openFolderPath = openFolder.getAttribute('data-cloud-track-folder') ?? '';
if (hoverFolder === openFolderPath) {
return openFolder;
}
}

if (hoverFolder === '') {
return document.getElementById('se-track-menu');
}

return null;
}

export function getMenuDropOverlayTop(container) {
const folders = container.querySelectorAll('[id^="se-menu-cloud-"]');
if (folders.length > 0) {
return folders[folders.length - 1].getBoundingClientRect().bottom;
}

const appBar = container.querySelector('.MuiAppBar-root');
if (appBar) {
return appBar.getBoundingClientRect().bottom;
}

const listAnchors = container.querySelectorAll('#se-visible-tracks-menu, [id^="se-shared-folder-"]');
if (listAnchors.length > 0) {
let bottom = 0;
listAnchors.forEach((el) => {
bottom = Math.max(bottom, el.getBoundingClientRect().bottom);
});
return bottom;
}

return container.getBoundingClientRect().top;
}

export function getMenuDropOverlayRect(container, ctx) {
if (!container) {
return null;
}

const containerRect = container.getBoundingClientRect();
const bottomPx = (ctx.globalGraph?.show ? ctx.globalGraph.size : 0) + OVERLAY_MARGIN;
const topPx = getMenuDropOverlayTop(container) + OVERLAY_MARGIN;

if (topPx >= window.innerHeight - bottomPx) {
return null;
}

return {
top: topPx,
left: containerRect.left + OVERLAY_MARGIN,
right: window.innerWidth - containerRect.right + OVERLAY_MARGIN,
bottom: bottomPx,
};
}

function isPointInOverlayRect(clientX, clientY, rect) {
const width = window.innerWidth;
const height = window.innerHeight;

return (
clientX >= rect.left &&
clientX <= width - rect.right &&
clientY >= rect.top &&
clientY <= height - rect.bottom
);
}

function getMenuDropFolderAtPoint(clientX, clientY, ctx) {
for (const container of getMenuDropContainers()) {
const rect = getMenuDropOverlayRect(container, ctx);
if (rect && isPointInOverlayRect(clientX, clientY, rect)) {
return container.getAttribute('data-cloud-track-folder') ?? '';
}
}

return null;
}

function getCloudTrackFolderFromElement(el) {
if (!el) {
return null;
}
const folderEl = el.closest('[data-cloud-track-folder]');
if (!folderEl) {
return null;
}

return folderEl.getAttribute('data-cloud-track-folder') ?? '';
}

export function resolveGpxDropTarget(clientX, clientY, ctx) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Determining the hovered folder by scraping the DOM (elementFromPoint, getElementById, data-*, id-prefix selectors) duplicates state the React tree already owns. Set the hover target from onDragEnter/onDragOver handlers on the folder components into context instead of reading layout back out of the DOM.

const folderFromDom = getCloudTrackFolderFromElement(document.elementFromPoint(clientX, clientY));
if (folderFromDom !== null) {
return { hoverFolder: folderFromDom, overMap: false };
}

const folderFromRect = getMenuDropFolderAtPoint(clientX, clientY, ctx);
if (folderFromRect !== null) {
return { hoverFolder: folderFromRect, overMap: false };
}

if (isPointInVisibleMap(ctx, clientX, clientY)) {
return { hoverFolder: null, overMap: true };
}

return { hoverFolder: null, overMap: false };
}
22 changes: 22 additions & 0 deletions map/src/frame/TracksMapDropOverlay.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useContext, useMemo } from 'react';
import AppContext from '../context/AppContext';
import DropOverlay from './components/DropOverlay';
import { getVisibleMapInsets } from './TracksMapDropGeometry';

export default function TracksMapDropOverlay() {
const ctx = useContext(AppContext);
const active =
ctx.gpxFileDrag?.active && ctx.gpxFileDrag?.overMap && ctx.gpxFileDrag?.hoverFolder === null;
const insets = useMemo(() => getVisibleMapInsets(ctx), [
ctx.infoBlockWidth,
ctx.globalGraph?.show,
ctx.globalGraph?.size,
ctx.openMainMenu,
]);

if (!active) {
return null;
}

return <DropOverlay insets={insets} />;
}
Loading