@@ -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+ }
3138import { pathToLatLngs } from '../util/coords' ;
3239import { FlightPath } from '../types' ;
3340import {
@@ -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
370376function 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 = ( ( ) => {
0 commit comments