@@ -116,6 +116,9 @@ const CustomPriorityBoundsField = (props) => {
116116 const [ isMapVisible , setIsMapVisible ] = useState ( false ) ;
117117 const [ viewState , setViewState ] = useState ( { center : [ 0 , 0 ] , zoom : 2 } ) ;
118118 const [ hasZoomed , setHasZoomed ] = useState ( false ) ;
119+ const [ uploadFeedback , setUploadFeedback ] = useState ( null ) ;
120+ const [ isDragOver , setIsDragOver ] = useState ( false ) ;
121+ const [ showTooltip , setShowTooltip ] = useState ( false ) ;
119122
120123 // Determine priority type from field name
121124 const priorityType = props . name ?. includes ( "highPriorityBounds" )
@@ -169,12 +172,140 @@ const CustomPriorityBoundsField = (props) => {
169172 }
170173 } , [ isMapVisible ] ) ;
171174
175+ // Close tooltip when clicking outside
176+ useEffect ( ( ) => {
177+ const handleClickOutside = ( event ) => {
178+ if ( showTooltip && ! event . target . closest ( ".tooltip-container" ) ) {
179+ setShowTooltip ( false ) ;
180+ }
181+ } ;
182+
183+ document . addEventListener ( "click" , handleClickOutside ) ;
184+ return ( ) => document . removeEventListener ( "click" , handleClickOutside ) ;
185+ } , [ showTooltip ] ) ;
186+
172187 const handleChange = ( newData ) => {
173188 if ( typeof props . onChange === "function" ) {
174189 props . onChange ( Array . isArray ( newData ) ? [ ...newData ] : newData ) ;
175190 }
176191 } ;
177192
193+ // Validate GeoJSON structure
194+ const validateGeoJSON = ( geoJson ) => {
195+ if ( ! geoJson || typeof geoJson !== "object" ) {
196+ return { valid : false , error : "Invalid JSON format" } ;
197+ }
198+
199+ if ( ! Array . isArray ( geoJson . features ) ) {
200+ return { valid : false , error : "Features must be an array" } ;
201+ }
202+
203+ const polygonFeatures = geoJson . features . filter (
204+ ( feature ) =>
205+ feature . type === "Feature" && feature . geometry && feature . geometry . type === "Polygon" ,
206+ ) ;
207+
208+ if ( polygonFeatures . length === 0 ) {
209+ return { valid : false , error : "No valid Polygon features found" } ;
210+ }
211+
212+ return { valid : true , features : polygonFeatures } ;
213+ } ;
214+
215+ // Handle file upload
216+ const handleFileUpload = async ( file ) => {
217+ setUploadFeedback ( null ) ;
218+
219+ // Check file type
220+ const validExtensions = [ ".json" , ".geojson" ] ;
221+ const fileExtension = file . name . toLowerCase ( ) . substring ( file . name . lastIndexOf ( "." ) ) ;
222+
223+ if ( ! validExtensions . includes ( fileExtension ) ) {
224+ setUploadFeedback ( { type : "error" , message : messages . fileTypeError } ) ;
225+ return ;
226+ }
227+
228+ try {
229+ const text = await file . text ( ) ;
230+ const geoJson = JSON . parse ( text ) ;
231+
232+ const validation = validateGeoJSON ( geoJson ) ;
233+ if ( ! validation . valid ) {
234+ setUploadFeedback ( {
235+ type : "error" ,
236+ message : messages . invalidGeoJSON ,
237+ details : validation . error ,
238+ } ) ;
239+ return ;
240+ }
241+
242+ // Convert GeoJSON features to the format expected by the component
243+ const convertedFeatures = validation . features . map ( ( feature ) => ( {
244+ type : "Feature" ,
245+ geometry : feature . geometry ,
246+ properties : feature . properties || { } ,
247+ } ) ) ;
248+
249+ // Merge with existing data or replace
250+ const newData = [ ...formData , ...convertedFeatures ] ;
251+ handleChange ( newData ) ;
252+
253+ setUploadFeedback ( {
254+ type : "success" ,
255+ message : messages . uploadSuccess ,
256+ count : convertedFeatures . length ,
257+ } ) ;
258+
259+ // Show map if not already visible
260+ if ( ! isMapVisible ) {
261+ setIsMapVisible ( true ) ;
262+ }
263+
264+ // Reset zoom flag to trigger auto-zoom to new polygons
265+ setHasZoomed ( false ) ;
266+
267+ // Clear feedback after 3 seconds
268+ setTimeout ( ( ) => setUploadFeedback ( null ) , 3000 ) ;
269+ } catch ( error ) {
270+ setUploadFeedback ( {
271+ type : "error" ,
272+ message : messages . uploadError ,
273+ details : error . message ,
274+ } ) ;
275+ }
276+ } ;
277+
278+ // Handle drag and drop
279+ const handleDragOver = ( e ) => {
280+ e . preventDefault ( ) ;
281+ setIsDragOver ( true ) ;
282+ } ;
283+
284+ const handleDragLeave = ( e ) => {
285+ e . preventDefault ( ) ;
286+ setIsDragOver ( false ) ;
287+ } ;
288+
289+ const handleDrop = ( e ) => {
290+ e . preventDefault ( ) ;
291+ setIsDragOver ( false ) ;
292+
293+ const files = Array . from ( e . dataTransfer . files ) ;
294+ if ( files . length > 0 ) {
295+ handleFileUpload ( files [ 0 ] ) ;
296+ }
297+ } ;
298+
299+ // Handle file input change
300+ const handleFileInputChange = ( e ) => {
301+ const files = Array . from ( e . target . files ) ;
302+ if ( files . length > 0 ) {
303+ handleFileUpload ( files [ 0 ] ) ;
304+ }
305+ // Reset input value to allow same file to be selected again
306+ e . target . value = "" ;
307+ } ;
308+
178309 return (
179310 < div className = "mr-relative mr-mb-4" onClick = { ( e ) => e . stopPropagation ( ) } >
180311 < div className = "mr-flex mr-items-center mr-gap-2 mr-mb-2" >
@@ -189,13 +320,91 @@ const CustomPriorityBoundsField = (props) => {
189320 < FormattedMessage { ...( isMapVisible ? messages . hideMap : messages . showMap ) } />
190321 </ button >
191322
323+ { /* Upload GeoJSON Button */ }
324+ < div
325+ className = { `mr-button mr-button--small mr-flex mr-items-center mr-gap-2 mr-transition-all mr-duration-300 mr-mt-2 mr-py-2 mr-cursor-pointer ${
326+ isDragOver
327+ ? "mr-button--green mr-bg-opacity-80"
328+ : "mr-button--white hover:mr-bg-gray-50"
329+ } `}
330+ onDragOver = { handleDragOver }
331+ onDragLeave = { handleDragLeave }
332+ onDrop = { handleDrop }
333+ >
334+ < input
335+ type = "file"
336+ accept = ".json,.geojson"
337+ onChange = { handleFileInputChange }
338+ className = "mr-absolute mr-inset-0 mr-w-full mr-h-full mr-opacity-0 mr-cursor-pointer"
339+ id = { `geojson-upload-${ props . name } ` }
340+ />
341+ < SvgSymbol sym = "upload-icon" viewBox = "0 0 20 20" className = "mr-w-5 mr-h-5" />
342+ < FormattedMessage { ...messages . uploadGeoJSON } />
343+ </ div >
344+
345+ { /* Info Icon */ }
346+ < div className = "mr-relative tooltip-container" >
347+ < button
348+ type = "button"
349+ onClick = { ( e ) => {
350+ e . stopPropagation ( ) ;
351+ setShowTooltip ( ! showTooltip ) ;
352+ } }
353+ className = "mr-ml-1 mr-mt-2 mr-p-1 mr-rounded-full mr-text-gray-500 hover:mr-text-gray-700 hover:mr-bg-gray-100 mr-transition-colors"
354+ title = "Show GeoJSON format info"
355+ >
356+ < SvgSymbol sym = "info-icon" viewBox = "0 0 20 20" className = "mr-w-4 mr-h-4" />
357+ </ button >
358+
359+ { /* Custom Tooltip */ }
360+ { showTooltip && (
361+ < div
362+ style = { { width : "300px" } }
363+ className = "mr-absolute mr-z-50 mr-text-white mr-text-sm"
364+ >
365+ < pre className = "mr-text-gray-300" >
366+ < FormattedMessage { ...messages . geoJSONFormatInfo } /> :
367+ { `\n{"features":[{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[lng,lat],[lng,lat],...]]}}]}` }
368+ </ pre >
369+ </ div >
370+ ) }
371+ </ div >
372+
192373 { formData . length > 0 && (
193374 < span className = "mr-text-green-lighter mr-text-sm mr-font-medium" >
194375 < FormattedMessage { ...messages . polygonsDefined } values = { { count : formData . length } } />
195376 </ span >
196377 ) }
197378 </ div >
198379
380+ { /* Upload Feedback */ }
381+ { uploadFeedback && (
382+ < div
383+ className = { `mr-mb-4 mr-p-2 mr-rounded mr-text-sm ${
384+ uploadFeedback . type === "success"
385+ ? "mr-bg-green-lighter mr-bg-opacity-20 mr-text-green-darker mr-border mr-border-green-lighter"
386+ : "mr-bg-red-lighter mr-bg-opacity-20 mr-text-red-darker mr-border mr-border-red-lighter"
387+ } `}
388+ >
389+ { uploadFeedback . type === "success" ? (
390+ < FormattedMessage
391+ { ...uploadFeedback . message }
392+ values = { { count : uploadFeedback . count } }
393+ />
394+ ) : (
395+ < div >
396+ < FormattedMessage
397+ { ...uploadFeedback . message }
398+ values = { { error : uploadFeedback . details } }
399+ />
400+ { uploadFeedback . details && (
401+ < div className = "mr-text-xs mr-mt-1 mr-opacity-75" > { uploadFeedback . details } </ div >
402+ ) }
403+ </ div >
404+ ) }
405+ </ div >
406+ ) }
407+
199408 { isMapVisible && (
200409 < div className = "mr-relative mr-rounded-lg mr-overflow-hidden mr-shadow-lg mr-border mr-border-black-10 mr-transition-all mr-duration-300" >
201410 < MapContainer
0 commit comments