11import Button from '@mui/joy/Button'
2+ import IconButton from '@mui/joy/IconButton'
23import Input from '@mui/joy/Input'
34import Option from '@mui/joy/Option'
45import Select from '@mui/joy/Select'
@@ -16,6 +17,29 @@ const fetcher = (url: string) => fetch(url).then(r => r.json())
1617
1718const REFERENCE_SOURCES : ItemReferenceSource [ ] = [ 'facebook' , 'google' , 'instagram' , 'wikipedia' , 'youtube' ]
1819
20+ // Parse DMS (Degrees Minutes Seconds) format to decimal
21+ // Supports formats like: 50° 22' 51.51" N or 50°22'51.51"N or 50 22 51.51 N
22+ function parseDMS ( dms : string ) : number | null {
23+ const dmsPattern = / ( \d + ) [ ° \s ] + ( \d + ) [ ' \s ] + ( \d + (?: \. \d + ) ? ) [ " \s ] * ( [ N S E W ] ) ? / i
24+ const match = dms . trim ( ) . match ( dmsPattern )
25+
26+ if ( ! match ) return null
27+
28+ const degrees = parseFloat ( match [ 1 ] )
29+ const minutes = parseFloat ( match [ 2 ] )
30+ const seconds = parseFloat ( match [ 3 ] )
31+ const direction = match [ 4 ] ?. toUpperCase ( )
32+
33+ let decimal = degrees + minutes / 60 + seconds / 3600
34+
35+ // Negate if South or West
36+ if ( direction === 'S' || direction === 'W' ) {
37+ decimal = - decimal
38+ }
39+
40+ return decimal
41+ }
42+
1943export default function Fields (
2044 { xmlAlbum, gallery, item, children, onItemUpdate, editedItems } :
2145 {
@@ -91,7 +115,7 @@ export default function Fields(
91115 ...( geo && { geo } ) ,
92116 ...( ref && { ref } ) ,
93117 }
94-
118+
95119 // Remove empty properties
96120 return removeEmpty ( orderedItem )
97121 } ) . filter ( ( item ) => item !== undefined )
@@ -187,31 +211,101 @@ export default function Fields(
187211 < Input
188212 value = { editedItem ?. geo ?. lat ?? '' }
189213 onChange = { ( e ) => {
190- const lat = e . target . value
191- updateItem ( ( prev : RawXmlItem | null ) => prev ? {
192- ...prev ,
193- geo : lat || prev . geo ?. lon ? { lat, lon : prev . geo ?. lon || '' , accuracy : prev . geo ?. accuracy || '' } : undefined ,
194- } : null )
214+ const value = e . target . value
215+
216+ // Check if value contains DMS format (degrees, minutes, seconds)
217+ if ( / \d + [ ° \s ] + \d + [ ' \s ] + \d + / . test ( value ) ) {
218+ // Split by comma to check for lat,lon DMS pair
219+ const parts = value . split ( ',' ) . map ( s => s . trim ( ) )
220+
221+ if ( parts . length === 2 ) {
222+ // Parse both lat and lon as DMS
223+ const lat = parseDMS ( parts [ 0 ] )
224+ const lon = parseDMS ( parts [ 1 ] )
225+ if ( lat !== null && lon !== null ) {
226+ updateItem ( ( prev : RawXmlItem | null ) => prev ? {
227+ ...prev ,
228+ geo : { lat : lat . toString ( ) , lon : lon . toString ( ) , accuracy : prev . geo ?. accuracy || '' } ,
229+ } : null )
230+ return
231+ }
232+ } else {
233+ // Single DMS value for latitude only
234+ const lat = parseDMS ( value )
235+ if ( lat !== null ) {
236+ updateItem ( ( prev : RawXmlItem | null ) => prev ? {
237+ ...prev ,
238+ geo : { lat : lat . toString ( ) , lon : prev . geo ?. lon || '' , accuracy : prev . geo ?. accuracy || '' } ,
239+ } : null )
240+ return
241+ }
242+ }
243+ }
244+
245+ // Check if the value contains a comma (decimal lat,lon format)
246+ if ( value . includes ( ',' ) ) {
247+ const [ lat , lon ] = value . split ( ',' ) . map ( s => s . trim ( ) )
248+ updateItem ( ( prev : RawXmlItem | null ) => prev ? {
249+ ...prev ,
250+ geo : { lat : lat || '' , lon : lon || '' , accuracy : prev . geo ?. accuracy || '' } ,
251+ } : null )
252+ } else {
253+ // Plain value for latitude
254+ const lat = value
255+ updateItem ( ( prev : RawXmlItem | null ) => prev ? {
256+ ...prev ,
257+ geo : lat || prev . geo ?. lon ? { lat, lon : prev . geo ?. lon || '' , accuracy : prev . geo ?. accuracy || '' } : undefined ,
258+ } : null )
259+ }
195260 } }
196- placeholder = "Latitude"
197- title = "Latitude (geo.lat)"
198- type = "number"
261+ placeholder = "Latitude (or lat,lon or DMS)"
262+ title = "Latitude (geo.lat) - paste lat,lon or DMS format to auto-split"
199263 sx = { { flex : 1 , minWidth : 0 } }
200264 />
201265 < Input
202266 value = { editedItem ?. geo ?. lon ?? '' }
203267 onChange = { ( e ) => {
204- const lon = e . target . value
268+ const value = e . target . value
269+
270+ // Check if value contains DMS format
271+ if ( / \d + [ ° \s ] + \d + [ ' \s ] + \d + / . test ( value ) ) {
272+ const lon = parseDMS ( value )
273+ if ( lon !== null ) {
274+ updateItem ( ( prev : RawXmlItem | null ) => prev ? {
275+ ...prev ,
276+ geo : { lat : prev . geo ?. lat || '' , lon : lon . toString ( ) , accuracy : prev . geo ?. accuracy || '' } ,
277+ } : null )
278+ return
279+ }
280+ }
281+
282+ // Plain value for longitude
283+ const lon = value
205284 updateItem ( ( prev : RawXmlItem | null ) => prev ? {
206285 ...prev ,
207286 geo : lon || prev . geo ?. lat ? { lat : prev . geo ?. lat || '' , lon, accuracy : prev . geo ?. accuracy || '' } : undefined ,
208287 } : null )
209288 } }
210- placeholder = "Longitude"
211- title = "Longitude (geo.lon)"
212- type = "number"
289+ placeholder = "Longitude (or DMS)"
290+ title = "Longitude (geo.lon) - paste DMS format to convert"
213291 sx = { { flex : 1 , minWidth : 0 } }
214292 />
293+ < IconButton
294+ size = "sm"
295+ variant = "outlined"
296+ onClick = { ( ) => {
297+ const lat = editedItem ?. geo ?. lat ?? ''
298+ const lon = editedItem ?. geo ?. lon ?? ''
299+ if ( lat && lon ) {
300+ navigator . clipboard . writeText ( `${ lat } ,${ lon } ` )
301+ }
302+ } }
303+ disabled = { ! editedItem ?. geo ?. lat || ! editedItem ?. geo ?. lon }
304+ title = "Copy lat,lon to clipboard"
305+ sx = { { minWidth : 'auto' , px : 1 } }
306+ >
307+ 📋
308+ </ IconButton >
215309 < Input
216310 value = { editedItem ?. geo ?. accuracy ?? '' }
217311 onChange = { ( e ) => {
0 commit comments