1+ import { getType } from "@turf/invariant" ;
2+ import { isFeature , isFeatureCollection } from "geojson-validation" ;
13import L from "leaflet" ;
24import { useEffect , useState } from "react" ;
35import { FormattedMessage } from "react-intl" ;
@@ -116,6 +118,9 @@ const CustomPriorityBoundsField = (props) => {
116118 const [ isMapVisible , setIsMapVisible ] = useState ( false ) ;
117119 const [ viewState , setViewState ] = useState ( { center : [ 0 , 0 ] , zoom : 2 } ) ;
118120 const [ hasZoomed , setHasZoomed ] = useState ( false ) ;
121+ const [ uploadFeedback , setUploadFeedback ] = useState ( null ) ;
122+ const [ isDragOver , setIsDragOver ] = useState ( false ) ;
123+ const [ showTooltip , setShowTooltip ] = useState ( false ) ;
119124
120125 // Determine priority type from field name
121126 const priorityType = props . name ?. includes ( "highPriorityBounds" )
@@ -169,12 +174,111 @@ const CustomPriorityBoundsField = (props) => {
169174 }
170175 } , [ isMapVisible ] ) ;
171176
177+ // Close tooltip when clicking outside
178+ useEffect ( ( ) => {
179+ const handleClickOutside = ( event ) => {
180+ if ( showTooltip && ! event . target . closest ( ".tooltip-container" ) ) {
181+ setShowTooltip ( false ) ;
182+ }
183+ } ;
184+
185+ document . addEventListener ( "click" , handleClickOutside ) ;
186+ return ( ) => document . removeEventListener ( "click" , handleClickOutside ) ;
187+ } , [ showTooltip ] ) ;
188+
172189 const handleChange = ( newData ) => {
173190 if ( typeof props . onChange === "function" ) {
174191 props . onChange ( Array . isArray ( newData ) ? [ ...newData ] : newData ) ;
175192 }
176193 } ;
177194
195+ // Handle file upload
196+ const handleFileUpload = async ( file ) => {
197+ setUploadFeedback ( null ) ;
198+
199+ const validExtensions = [ ".json" , ".geojson" ] ;
200+ const fileExtension = file . name . toLowerCase ( ) . substring ( file . name . lastIndexOf ( "." ) ) ;
201+
202+ if ( ! validExtensions . includes ( fileExtension ) ) {
203+ setUploadFeedback ( { type : "error" , message : messages . fileTypeError } ) ;
204+ return ;
205+ }
206+
207+ try {
208+ const text = await file . text ( ) ;
209+ const geoJson = JSON . parse ( text ) ;
210+
211+ if ( ! isFeature ( geoJson ) && ! isFeatureCollection ( geoJson ) ) {
212+ throw new Error ( "Input must be a valid GeoJSON Feature or FeatureCollection" ) ;
213+ }
214+
215+ const features = getType ( geoJson ) === "Feature" ? [ geoJson ] : geoJson . features || [ ] ;
216+
217+ const polygonFeatures = features . filter (
218+ ( feature ) =>
219+ feature . type === "Feature" && feature . geometry && feature . geometry . type === "Polygon" ,
220+ ) ;
221+
222+ if ( polygonFeatures . length === 0 ) {
223+ throw new Error ( "No valid Polygon features found" ) ;
224+ }
225+
226+ const newData = [ ...formData , ...polygonFeatures ] ;
227+ handleChange ( newData ) ;
228+
229+ setUploadFeedback ( {
230+ type : "success" ,
231+ message : messages . uploadSuccess ,
232+ count : polygonFeatures . length ,
233+ } ) ;
234+
235+ if ( ! isMapVisible ) {
236+ setIsMapVisible ( true ) ;
237+ }
238+
239+ setHasZoomed ( false ) ;
240+
241+ setTimeout ( ( ) => setUploadFeedback ( null ) , 3000 ) ;
242+ } catch ( error ) {
243+ setUploadFeedback ( {
244+ type : "error" ,
245+ message : messages . invalidGeoJSON ,
246+ details : error . message ,
247+ } ) ;
248+ }
249+ } ;
250+
251+ // Handle drag and drop
252+ const handleDragOver = ( e ) => {
253+ e . preventDefault ( ) ;
254+ setIsDragOver ( true ) ;
255+ } ;
256+
257+ const handleDragLeave = ( e ) => {
258+ e . preventDefault ( ) ;
259+ setIsDragOver ( false ) ;
260+ } ;
261+
262+ const handleDrop = ( e ) => {
263+ e . preventDefault ( ) ;
264+ setIsDragOver ( false ) ;
265+
266+ const files = Array . from ( e . dataTransfer . files ) ;
267+ if ( files . length > 0 ) {
268+ handleFileUpload ( files [ 0 ] ) ;
269+ }
270+ } ;
271+
272+ // Handle file input change
273+ const handleFileInputChange = ( e ) => {
274+ const files = Array . from ( e . target . files ) ;
275+ if ( files . length > 0 ) {
276+ handleFileUpload ( files [ 0 ] ) ;
277+ }
278+ // Reset input value to allow same file to be selected again
279+ e . target . value = "" ;
280+ } ;
281+
178282 return (
179283 < div className = "mr-relative mr-mb-4" onClick = { ( e ) => e . stopPropagation ( ) } >
180284 < div className = "mr-flex mr-items-center mr-gap-2 mr-mb-2" >
@@ -189,13 +293,90 @@ const CustomPriorityBoundsField = (props) => {
189293 < FormattedMessage { ...( isMapVisible ? messages . hideMap : messages . showMap ) } />
190294 </ button >
191295
296+ { /* Upload GeoJSON Button */ }
297+ < div
298+ 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 ${
299+ isDragOver
300+ ? "mr-button--green mr-bg-opacity-80"
301+ : "mr-button--white hover:mr-bg-gray-50"
302+ } `}
303+ onDragOver = { handleDragOver }
304+ onDragLeave = { handleDragLeave }
305+ onDrop = { handleDrop }
306+ >
307+ < input
308+ type = "file"
309+ accept = ".json,.geojson"
310+ onChange = { handleFileInputChange }
311+ className = "mr-absolute mr-inset-0 mr-w-full mr-h-full mr-opacity-0 mr-cursor-pointer"
312+ id = { `geojson-upload-${ props . name } ` }
313+ />
314+ < SvgSymbol sym = "upload-icon" viewBox = "0 0 20 20" className = "mr-w-5 mr-h-5" />
315+ < FormattedMessage { ...messages . uploadGeoJSON } />
316+ </ div >
317+
318+ { /* Info Icon */ }
319+ < div className = "mr-relative tooltip-container" >
320+ < button
321+ type = "button"
322+ onClick = { ( e ) => {
323+ e . stopPropagation ( ) ;
324+ setShowTooltip ( ! showTooltip ) ;
325+ } }
326+ 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"
327+ title = "Show GeoJSON format info"
328+ >
329+ < SvgSymbol sym = "info-icon" viewBox = "0 0 20 20" className = "mr-w-4 mr-h-4" />
330+ </ button >
331+
332+ { /* Custom Tooltip */ }
333+ { showTooltip && (
334+ < div
335+ style = { { width : "300px" } }
336+ className = "mr-absolute mr-z-50 mr-text-white mr-text-sm"
337+ >
338+ < pre className = "mr-text-gray-300" >
339+ < FormattedMessage { ...messages . geoJSONFormatInfo } />
340+ </ pre >
341+ </ div >
342+ ) }
343+ </ div >
344+
192345 { formData . length > 0 && (
193346 < span className = "mr-text-green-lighter mr-text-sm mr-font-medium" >
194347 < FormattedMessage { ...messages . polygonsDefined } values = { { count : formData . length } } />
195348 </ span >
196349 ) }
197350 </ div >
198351
352+ { /* Upload Feedback */ }
353+ { uploadFeedback && (
354+ < div
355+ className = { `mr-mb-4 mr-p-2 mr-rounded mr-text-sm ${
356+ uploadFeedback . type === "success"
357+ ? "mr-bg-green-lighter mr-bg-opacity-20 mr-text-green-darker mr-border mr-border-green-lighter"
358+ : "mr-bg-red-lighter mr-bg-opacity-20 mr-text-red-darker mr-border mr-border-red-lighter"
359+ } `}
360+ >
361+ { uploadFeedback . type === "success" ? (
362+ < FormattedMessage
363+ { ...uploadFeedback . message }
364+ values = { { count : uploadFeedback . count } }
365+ />
366+ ) : (
367+ < div >
368+ < FormattedMessage
369+ { ...uploadFeedback . message }
370+ values = { { error : uploadFeedback . details } }
371+ />
372+ { uploadFeedback . details && (
373+ < div className = "mr-text-xs mr-mt-1 mr-opacity-75" > { uploadFeedback . details } </ div >
374+ ) }
375+ </ div >
376+ ) }
377+ </ div >
378+ ) }
379+
199380 { isMapVisible && (
200381 < 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" >
201382 < MapContainer
0 commit comments