Skip to content

Commit 8e2bb62

Browse files
bgrozevclaude
andcommitted
Replace target selection buttons with always-on map drag handles
- Remove useMapClickHandler hook and "Select Target" / "Select Target & Direction" toolbar buttons - Add "Edit on Map" toggle in TargetComponent and toolbar (EditLocation icon, highlights when active) - When active: draggable cyan target marker + orange heading handle (15 m out) with connecting line; map clicks also move the target - On mobile, activating edit navigates to map automatically Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0e90283 commit 8e2bb62

5 files changed

Lines changed: 129 additions & 73 deletions

File tree

src/App.tsx

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,14 @@ import {
3838
WindSummary,
3939
WindsComponent
4040
} from './components';
41-
import { CourseEditTarget } from './components/MapComponent';
41+
import { CourseEditTarget, TargetEditTarget } from './components/MapComponent';
4242
import { SOURCE_DZ, SOURCE_MANUAL } from './forecast/forecast';
4343
import {
4444
AppStateProvider,
4545
DEFAULT_TARGET,
4646
useAppState,
4747
useCustomCourses,
4848
useFetchForecast,
49-
useMapClickHandler,
5049
usePresets
5150
} from './hooks';
5251
import { Course, LatLng, Target, WindSummaryData } from './types';
@@ -172,6 +171,7 @@ function DashboardContent() {
172171

173172
const [forecastTime, setForecastTime] = useState<Date | null>(null);
174173
const [courseEditOpen, setCourseEditOpen] = useState(false);
174+
const [targetEditOpen, setTargetEditOpen] = useState(false);
175175
const [exportOpen, setExportOpen] = useState(false);
176176

177177
const { winds, fetching, fetchWinds, setWinds, resetWinds } = useFetchForecast({
@@ -194,12 +194,6 @@ function DashboardContent() {
194194
const isMobile = useMediaQuery('(max-width:600px)');
195195
const router = useDemoRouter('/map');
196196

197-
const { handleMapClick, selectFromMap, isWaitingForClick } = useMapClickHandler({
198-
currentTarget: target,
199-
onTargetSelected: setTarget,
200-
onNavigateToMap: isMobile ? () => router.navigate('/map') : undefined
201-
});
202-
203197
const {
204198
presets,
205199
activePresetId,
@@ -294,9 +288,13 @@ function DashboardContent() {
294288
} else if (router.pathname === '/target') {
295289
p = (
296290
<TargetComponent
297-
selectFromMap={selectFromMap}
298291
target={target}
299292
setTarget={setTarget}
293+
editOpen={targetEditOpen}
294+
onEditOpenChange={open => {
295+
setTargetEditOpen(open);
296+
if (open && isMobile) router.navigate('/map');
297+
}}
300298
onUpwindClick={onUpwindClick}
301299
/>
302300
);
@@ -362,18 +360,26 @@ function DashboardContent() {
362360
}
363361
: undefined;
364362

363+
const targetEditTarget: TargetEditTarget | undefined = targetEditOpen
364+
? {
365+
target: target.target,
366+
heading: target.finalHeading,
367+
onMove: (pos: LatLng) => setTarget({ ...target, target: pos }),
368+
onHeadingChange: (h: number) => setTarget({ ...target, finalHeading: Math.round(h) })
369+
}
370+
: undefined;
371+
365372
const map = (
366373
<MapComponent
367374
center={target.target}
368375
pathA={c}
369376
pathB={c2Display}
370377
settings={settings}
371-
onClick={handleMapClick}
372378
windDirection={averageWind_?.direction ?? 0}
373379
windSpeed={averageWind_?.speedKts ?? 0}
374-
waitingForClick={isWaitingForClick}
375380
courses={enabledCourses}
376381
courseEditTarget={courseEditTarget}
382+
targetEditTarget={targetEditTarget}
377383
/>
378384
);
379385
const dashboard = (
@@ -385,9 +391,13 @@ function DashboardContent() {
385391
fetching={fetching}
386392
onMapButtonClick={() => router.navigate('/map')}
387393
onRefreshWindsClick={handleFetchWinds}
388-
onSelectTargetClick={() => selectFromMap(false)}
389-
onSelectTargetAndHeadingClick={() => selectFromMap(true)}
390394
onExportClick={() => setExportOpen(true)}
395+
targetEditOpen={targetEditOpen}
396+
onTargetEditToggle={() => {
397+
const next = !targetEditOpen;
398+
setTargetEditOpen(next);
399+
if (next && isMobile) router.navigate('/map');
400+
}}
391401
showPresets={settings.showPresets}
392402
presets={presets}
393403
activePresetId={activePresetId}

src/components/MapComponent.tsx

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ export interface CourseEditTarget {
2828
onMove: (newCenter: LatLng) => void;
2929
onRotate: (newDirection: number) => void;
3030
}
31+
32+
export interface TargetEditTarget {
33+
target: LatLng;
34+
heading: number;
35+
onMove: (pos: LatLng) => void;
36+
onHeadingChange: (heading: number) => void;
37+
}
3138
import { pathToLatLngs } from '../util/coords';
3239
import { FlightPath } from '../types';
3340
import {
@@ -358,26 +365,24 @@ interface MapComponentProps {
358365
windSpeed: number;
359366
windDirection: number;
360367
center: LatLng;
361-
onClick: (latLng: LatLng) => void;
362368
pathA: FlightPath;
363369
pathB: FlightPath;
364370
settings: Settings;
365-
waitingForClick: boolean;
366371
courses?: Course[];
367372
courseEditTarget?: CourseEditTarget;
373+
targetEditTarget?: TargetEditTarget;
368374
}
369375

370376
function MapComponent({
371377
windSpeed,
372378
windDirection,
373379
center,
374-
onClick,
375380
pathA,
376381
pathB,
377382
settings,
378-
waitingForClick,
379383
courses = [],
380-
courseEditTarget
384+
courseEditTarget,
385+
targetEditTarget
381386
}: MapComponentProps) {
382387
const { showPoms, showPomAltitudes, showPomTooltips, showPreWind, displayWindArrow, highlightCorrespondingPoints, showMeasureTool } = settings;
383388
const { formatAltitude, altitudeLabel } = useUnits();
@@ -387,8 +392,9 @@ function MapComponent({
387392
const [measurePoints, setMeasurePoints] = useState<LatLng[]>([]);
388393
const mapRef = useRef<google.maps.Map | null>(null);
389394
const [zoom, setZoom] = useState<number>(DEFAULT_MAP_OPTIONS.zoom);
390-
// Live position of the rotation handle while dragging (for smooth line preview)
395+
// Live position of drag handles while dragging (for smooth line preview)
391396
const [liveHandlePos, setLiveHandlePos] = useState<LatLng | null>(null);
397+
const [liveTargetHeadingPos, setLiveTargetHeadingPos] = useState<LatLng | null>(null);
392398

393399
const toggleMeasuring = useCallback(() => {
394400
setMeasuring(m => {
@@ -425,7 +431,7 @@ function MapComponent({
425431

426432
// Update cursor without causing map re-render
427433
if (mapRef.current) {
428-
const cursor = (showMeasureTool && measuring) || waitingForClick ? 'crosshair' : 'grab';
434+
const cursor = (showMeasureTool && measuring) || targetEditTarget ? 'crosshair' : 'grab';
429435
mapRef.current.setOptions({ draggableCursor: cursor });
430436
}
431437

@@ -467,8 +473,8 @@ function MapComponent({
467473
const latlng = { lat: ev.latLng.lat(), lng: ev.latLng.lng() };
468474
if (showMeasureTool && measuring) {
469475
setMeasurePoints(pts => [...pts, latlng]);
470-
} else {
471-
onClick(latlng);
476+
} else if (targetEditTarget) {
477+
targetEditTarget.onMove(latlng);
472478
}
473479
}}
474480
options={DEFAULT_MAP_OPTIONS}
@@ -697,6 +703,66 @@ function MapComponent({
697703
)}
698704
</React.Fragment>
699705
))}
706+
{/* Target edit handles — position drag + heading direction handle */}
707+
{targetEditTarget && (() => {
708+
const headingHandlePos = (() => {
709+
const pt = turf.destination(
710+
[targetEditTarget.target.lng, targetEditTarget.target.lat],
711+
15, targetEditTarget.heading, { units: 'meters' }
712+
);
713+
return { lat: pt.geometry.coordinates[1], lng: pt.geometry.coordinates[0] };
714+
})();
715+
const headingLineEnd = liveTargetHeadingPos ?? headingHandlePos;
716+
/* eslint-disable @typescript-eslint/no-explicit-any */
717+
const circleIcon = (color: string, scale: number) => ({
718+
path: (window as any).google.maps.SymbolPath.CIRCLE,
719+
scale,
720+
fillColor: color,
721+
fillOpacity: 0.85,
722+
strokeColor: '#fff',
723+
strokeWeight: 2
724+
});
725+
/* eslint-enable @typescript-eslint/no-explicit-any */
726+
return (
727+
<React.Fragment key="target-edit-handles">
728+
<PolylineF
729+
path={[targetEditTarget.target, headingLineEnd]}
730+
options={{ strokeColor: '#ffaa00', strokeWeight: 2, strokeOpacity: 0.9, zIndex: 25, clickable: false }}
731+
/>
732+
<MarkerF
733+
position={targetEditTarget.target}
734+
draggable
735+
cursor="move"
736+
zIndex={26}
737+
icon={circleIcon('#00ccff', 9)}
738+
onDragEnd={e => {
739+
if (e.latLng) targetEditTarget.onMove({ lat: e.latLng.lat(), lng: e.latLng.lng() });
740+
}}
741+
/>
742+
<MarkerF
743+
position={headingHandlePos}
744+
draggable
745+
cursor="pointer"
746+
zIndex={27}
747+
icon={circleIcon('#ffaa00', 7)}
748+
onDrag={e => {
749+
if (e.latLng) setLiveTargetHeadingPos({ lat: e.latLng.lat(), lng: e.latLng.lng() });
750+
}}
751+
onDragEnd={e => {
752+
setLiveTargetHeadingPos(null);
753+
if (e.latLng) {
754+
const bearing = turf.bearing(
755+
[targetEditTarget.target.lng, targetEditTarget.target.lat],
756+
[e.latLng.lng(), e.latLng.lat()]
757+
);
758+
targetEditTarget.onHeadingChange((bearing + 360) % 360);
759+
}
760+
}}
761+
/>
762+
</React.Fragment>
763+
);
764+
})()}
765+
700766
{/* Course edit handles — center drag + rotation handle */}
701767
{courseEditTarget && (() => {
702768
const rotationHandlePos = (() => {

src/components/TargetComponent.tsx

Lines changed: 14 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import {
2-
Explore as ExploreIcon,
3-
ModeStandby as ModeStandbyIcon
2+
EditLocation as EditLocationIcon
43
} from '@mui/icons-material';
54
import {
65
Button,
@@ -22,14 +21,16 @@ import { LocationComponent } from './';
2221
interface TargetComponentProps {
2322
target: Target;
2423
setTarget: (target: Target) => void;
25-
selectFromMap: (withHeading: boolean) => void;
24+
editOpen: boolean;
25+
onEditOpenChange: (open: boolean) => void;
2626
onUpwindClick: () => void;
2727
}
2828

2929
export default function TargetComponent({
3030
target,
3131
setTarget,
32-
selectFromMap,
32+
editOpen,
33+
onEditOpenChange,
3334
onUpwindClick
3435
}: TargetComponentProps) {
3536
const handleHeadingChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
@@ -41,30 +42,15 @@ export default function TargetComponent({
4142

4243
return (
4344
<Stack spacing={3}>
44-
<Stack direction="row" spacing={1}>
45-
<Tooltip title="Select target by clicking on the map." arrow>
46-
<Button
47-
variant="outlined"
48-
size="small"
49-
startIcon={<ModeStandbyIcon />}
50-
onClick={() => selectFromMap(false)}
51-
sx={{ textTransform: 'none' }}
52-
>
53-
Set Target
54-
</Button>
55-
</Tooltip>
56-
<Tooltip title="Select target and direction by clicking on the map twice." arrow>
57-
<Button
58-
variant="outlined"
59-
size="small"
60-
startIcon={<ExploreIcon />}
61-
onClick={() => selectFromMap(true)}
62-
sx={{ textTransform: 'none' }}
63-
>
64-
Set Target & Direction
65-
</Button>
66-
</Tooltip>
67-
</Stack>
45+
<Button
46+
variant={editOpen ? 'contained' : 'outlined'}
47+
size="small"
48+
startIcon={<EditLocationIcon />}
49+
onClick={() => onEditOpenChange(!editOpen)}
50+
sx={{ textTransform: 'none', alignSelf: 'flex-start' }}
51+
>
52+
{editOpen ? 'Done' : 'Edit on Map'}
53+
</Button>
6854

6955
<Stack direction="row">
7056
<ControlledNumberInput

src/components/ToolbarActions.tsx

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import {
2-
Explore as ExploreIcon,
2+
EditLocation as EditLocationIcon,
33
FileDownload as FileDownloadIcon,
4-
ModeStandby as ModeStandbyIcon,
54
Public as PublicIcon,
65
Refresh as RefreshIcon
76
} from '@mui/icons-material';
8-
import { CircularProgress, Divider, IconButton, Stack, Tooltip } from '@mui/material';
7+
import { CircularProgress, Divider, IconButton, Stack, Tooltip, useTheme } from '@mui/material';
98
import React from 'react';
109

1110
import { Preset } from '../types';
@@ -15,9 +14,9 @@ import PresetSelector from './PresetSelector';
1514
interface ToolbarActionsProps {
1615
onMapButtonClick: () => void;
1716
onRefreshWindsClick: () => void;
18-
onSelectTargetClick: () => void;
19-
onSelectTargetAndHeadingClick: () => void;
2017
onExportClick: () => void;
18+
targetEditOpen: boolean;
19+
onTargetEditToggle: () => void;
2120
fetching: boolean;
2221
showPresets: boolean;
2322
presets: Preset[];
@@ -31,9 +30,9 @@ interface ToolbarActionsProps {
3130
export default function ToolbarActions({
3231
onMapButtonClick,
3332
onRefreshWindsClick,
34-
onSelectTargetClick,
35-
onSelectTargetAndHeadingClick,
3633
onExportClick,
34+
targetEditOpen,
35+
onTargetEditToggle,
3736
fetching,
3837
showPresets,
3938
presets,
@@ -47,8 +46,7 @@ export default function ToolbarActions({
4746
<Stack direction="row" spacing={1} alignItems="center">
4847
<MapButton onClick={onMapButtonClick} />
4948
<Divider orientation="vertical" flexItem />
50-
<SelectTargetAndHeadingButton onClick={onSelectTargetAndHeadingClick} />
51-
<SelectTargetButton onClick={onSelectTargetClick} />
49+
<EditTargetButton active={targetEditOpen} onClick={onTargetEditToggle} />
5250
<RefreshWindsButton onClick={onRefreshWindsClick} fetching={fetching} />
5351
<ExportButton onClick={onExportClick} />
5452
<Divider orientation="vertical" flexItem />
@@ -76,21 +74,18 @@ function MapButton({ onClick }: { onClick: () => void }) {
7674
);
7775
}
7876

79-
function SelectTargetButton({ onClick }: { onClick: () => void }) {
80-
return (
81-
<Tooltip title="Select target">
82-
<IconButton type="button" aria-label="refresh-wind" onClick={onClick}>
83-
<ModeStandbyIcon />
84-
</IconButton>
85-
</Tooltip>
86-
);
87-
}
8877

89-
function SelectTargetAndHeadingButton({ onClick }: { onClick: () => void }) {
78+
function EditTargetButton({ active, onClick }: { active: boolean; onClick: () => void }) {
79+
const theme = useTheme();
9080
return (
91-
<Tooltip title="Select target and direction">
92-
<IconButton type="button" aria-label="refresh-wind" onClick={onClick}>
93-
<ExploreIcon />
81+
<Tooltip title={active ? 'Stop editing target' : 'Edit target on map'}>
82+
<IconButton
83+
type="button"
84+
aria-label="edit-target"
85+
onClick={onClick}
86+
sx={active ? { color: theme.palette.primary.main } : undefined}
87+
>
88+
<EditLocationIcon />
9489
</IconButton>
9590
</Tooltip>
9691
);

src/hooks/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
export { AppStateProvider, useAppState, DEFAULT_TARGET, DEFAULT_SETTINGS } from './useAppState';
22
export { useCustomCourses } from './useCustomCourses';
33
export { useFetchForecast } from './useFetchForecast';
4-
export { useMapClickHandler } from './useMapClickHandler';
54
export { usePresets } from './usePresets';
65
export { TargetProvider, useTarget } from './useTargetContext';
76
export { useUnits } from './useUnits';

0 commit comments

Comments
 (0)