11// src/PolylineCreation.js
22
3- import { useState } from "react" ;
4- import { log } from "./Utils " ;
5- import { parsePolylineInput } from "./PolylineUtils " ;
3+ import { useState , useEffect , useRef } from "react" ;
4+ import { parsePolylineInput , calculatePolylineDistanceMeters } from "./PolylineUtils " ;
5+ import { log , formatDistance } from "./Utils " ;
66
7- function PolylineCreation ( { onSubmit, onClose, buttonPosition } ) {
7+ function PolylineCreation ( { map , onSubmit, onClose, buttonPosition } ) {
88 const [ input , setInput ] = useState ( "" ) ;
99 const [ opacity , setOpacity ] = useState ( 0.7 ) ;
1010 const [ color , setColor ] = useState ( "#FF0000" ) ;
1111 const [ strokeWeight , setStrokeWeight ] = useState ( 6 ) ;
12+ const [ distanceUnit , setDistanceUnit ] = useState ( "metric" ) ;
13+
14+ const [ isMeasuring , setIsMeasuring ] = useState ( false ) ;
15+ const [ points , setPoints ] = useState ( [ ] ) ;
16+ const polylineRef = useRef ( null ) ;
17+ const markersRef = useRef ( [ ] ) ;
1218
1319 const handleSubmit = ( e ) => {
1420 e . preventDefault ( ) ;
@@ -28,6 +34,102 @@ function PolylineCreation({ onSubmit, onClose, buttonPosition }) {
2834
2935Or paste an encoded S2 or Google Maps polyline string` ;
3036
37+ useEffect ( ( ) => {
38+ if ( ! map || ! isMeasuring ) return ;
39+
40+ log ( "MeasureMode: Attaching click listener" ) ;
41+ const clickListener = map . addListener ( "click" , ( e ) => {
42+ setPoints ( ( prev ) => [ ...prev , e . latLng ] ) ;
43+ } ) ;
44+
45+ map . setOptions ( { draggableCursor : "crosshair" } ) ;
46+
47+ return ( ) => {
48+ window . google . maps . event . removeListener ( clickListener ) ;
49+ if ( map ) {
50+ map . setOptions ( { draggableCursor : null } ) ;
51+ }
52+ } ;
53+ } , [ map , isMeasuring ] ) ;
54+
55+ useEffect ( ( ) => {
56+ if ( ! map || ! isMeasuring ) return ;
57+
58+ if ( ! polylineRef . current ) {
59+ polylineRef . current = new window . google . maps . Polyline ( {
60+ map,
61+ path : points ,
62+ strokeColor : color ,
63+ strokeOpacity : opacity ,
64+ strokeWeight : strokeWeight ,
65+ geodesic : true ,
66+ } ) ;
67+ } else {
68+ polylineRef . current . setPath ( points ) ;
69+ polylineRef . current . setOptions ( { strokeColor : color , strokeOpacity : opacity , strokeWeight } ) ;
70+ }
71+
72+ markersRef . current . forEach ( ( m ) => m . setMap ( null ) ) ;
73+ markersRef . current = points . map ( ( p , i ) => {
74+ let label = ( i + 1 ) . toString ( ) ;
75+
76+ return new window . google . maps . Marker ( {
77+ map,
78+ position : p ,
79+ label : {
80+ text : label ,
81+ color : "white" ,
82+ fontSize : "12px" ,
83+ fontWeight : "bold" ,
84+ } ,
85+ icon : {
86+ path : window . google . maps . SymbolPath . CIRCLE ,
87+ scale : 10 ,
88+ fillColor : color ,
89+ fillOpacity : 1 ,
90+ strokeWeight : 2 ,
91+ strokeColor : "#FFFFFF" ,
92+ } ,
93+ zIndex : 1000 ,
94+ } ) ;
95+ } ) ;
96+ } , [ points , map , isMeasuring , color , opacity , strokeWeight ] ) ;
97+
98+ useEffect ( ( ) => {
99+ if ( ! isMeasuring ) {
100+ setPoints ( [ ] ) ;
101+ if ( polylineRef . current ) {
102+ polylineRef . current . setMap ( null ) ;
103+ polylineRef . current = null ;
104+ }
105+ markersRef . current . forEach ( ( m ) => m . setMap ( null ) ) ;
106+ markersRef . current = [ ] ;
107+ }
108+
109+ return ( ) => {
110+ if ( polylineRef . current ) {
111+ polylineRef . current . setMap ( null ) ;
112+ }
113+ markersRef . current . forEach ( ( m ) => m . setMap ( null ) ) ;
114+ } ;
115+ } , [ isMeasuring ] ) ;
116+
117+ const handleCreateFromMeasure = ( ) => {
118+ if ( points . length < 2 ) return ;
119+ const formattedPoints = points . map ( ( p ) => ( { latitude : p . lat ( ) , longitude : p . lng ( ) } ) ) ;
120+
121+ const distanceMeters = calculatePolylineDistanceMeters ( formattedPoints ) ;
122+
123+ onSubmit ( formattedPoints , { opacity, color, strokeWeight, distanceMeters, distanceUnit } ) ;
124+ setIsMeasuring ( false ) ;
125+ } ;
126+
127+ const distanceMeters =
128+ points . length > 1
129+ ? calculatePolylineDistanceMeters ( points . map ( ( p ) => ( { latitude : p . lat ( ) , longitude : p . lng ( ) } ) ) )
130+ : 0 ;
131+ const { metric, imperial } = formatDistance ( distanceMeters ) ;
132+
31133 return (
32134 < div
33135 style = { {
@@ -81,12 +183,78 @@ Or paste an encoded S2 or Google Maps polyline string`;
81183 />
82184 </ label >
83185 </ div >
84- < button type = "submit" className = "map-button inner-button" >
85- Create Polyline
86- </ button >
87- < button type = "button" className = "map-button inner-button" onClick = { onClose } >
88- Close
89- </ button >
186+ < div style = { { margin : "5px" } } >
187+ < label > Distance Marker:</ label >
188+ < div style = { { display : "flex" , gap : "10px" , marginTop : "5px" } } >
189+ < label >
190+ < input
191+ type = "radio"
192+ value = "metric"
193+ checked = { distanceUnit === "metric" }
194+ onChange = { ( e ) => setDistanceUnit ( e . target . value ) }
195+ />
196+ Metric
197+ </ label >
198+ < label >
199+ < input
200+ type = "radio"
201+ value = "imperial"
202+ checked = { distanceUnit === "imperial" }
203+ onChange = { ( e ) => setDistanceUnit ( e . target . value ) }
204+ />
205+ Imperial
206+ </ label >
207+ < label >
208+ < input
209+ type = "radio"
210+ value = "none"
211+ checked = { distanceUnit === "none" }
212+ onChange = { ( e ) => setDistanceUnit ( e . target . value ) }
213+ />
214+ None
215+ </ label >
216+ </ div >
217+ </ div >
218+ < div style = { { margin : "5px" , padding : "10px" , backgroundColor : "#f5f5f5" , borderRadius : "5px" } } >
219+ < div style = { { display : "flex" , justifyContent : "space-between" , alignItems : "center" , marginBottom : "5px" } } >
220+ < label >
221+ < input type = "checkbox" checked = { isMeasuring } onChange = { ( e ) => setIsMeasuring ( e . target . checked ) } />
222+ Enable Map Clicking (Measure)
223+ </ label >
224+ { isMeasuring && < span style = { { fontSize : "12px" , color : "#666" } } > Points: { points . length } </ span > }
225+ </ div >
226+ { isMeasuring && points . length > 1 && (
227+ < div style = { { marginTop : "5px" , display : "flex" , justifyContent : "space-between" } } >
228+ < span style = { { fontWeight : "bold" , color : "#222" } } > { metric } </ span >
229+ < span style = { { fontWeight : "bold" , color : "#222" } } > { imperial } </ span >
230+ </ div >
231+ ) }
232+ </ div >
233+ < div style = { { display : "flex" , gap : "5px" , marginTop : "10px" } } >
234+ { isMeasuring ? (
235+ < button
236+ type = "button"
237+ className = "map-button inner-button"
238+ onClick = { handleCreateFromMeasure }
239+ style = { { flex : 1 } }
240+ disabled = { points . length < 2 }
241+ >
242+ Create Polyline
243+ </ button >
244+ ) : (
245+ < button
246+ type = "submit"
247+ className = "map-button inner-button"
248+ style = { { flex : 1 } }
249+ disabled = { input . trim ( ) === "" }
250+ >
251+ Create Polyline
252+ </ button >
253+ ) }
254+ < button type = "button" className = "map-button inner-button" onClick = { onClose } style = { { flex : 1 } } >
255+ Close
256+ </ button >
257+ </ div >
90258 </ form >
91259 </ div >
92260 ) ;
0 commit comments