Skip to content

Commit

Permalink
feat(visual): add, change and remove waypoints
Browse files Browse the repository at this point in the history
  • Loading branch information
devshred committed Mar 27, 2024
1 parent ad0e35e commit 180b4e4
Show file tree
Hide file tree
Showing 11 changed files with 349 additions and 51 deletions.
45 changes: 40 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
},
"dependencies": {
"@hello-pangea/dnd": "^16.5.0",
"@types/geojson": "^7946.0.14",
"@types/leaflet": "^1.9.8",
"axios": "^1.6.7",
"dompurify": "^3.0.9",
Expand All @@ -24,8 +25,10 @@
"react-dropzone": "^14.2.3",
"react-icons": "^5.0.1",
"react-leaflet": "^4.2.1",
"react-leaflet-custom-control": "^1.4.0",
"react-router-dom": "^6.21.3",
"sanitize-html": "^2.12.1",
"uuid": "^9.0.1",
"vite-plugin-package-version": "^1.1.0"
},
"devDependencies": {
Expand All @@ -37,6 +40,7 @@
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@types/sanitize-html": "^2.11.0",
"@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"@vitejs/plugin-react-swc": "^3.5.0",
Expand Down
21 changes: 21 additions & 0 deletions src/@types/gps.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
import { LatLngTuple } from 'leaflet'

type PoiType =
| 'GENERIC'
| 'SUMMIT'
| 'VALLEY'
| 'WATER'
| 'FOOD'
| 'DANGER'
| 'LEFT'
| 'RIGHT'
| 'STRAIGHT'
| 'FIRST_AID'
| 'FOURTH_CATEGORY'
| 'THIRD_CATEGORY'
| 'SECOND_CATEGORY'
| 'FIRST_CATEGORY'
| 'HORS_CATEGORY'
| 'RESIDENCE'
| 'SPRINT'

export type WayPoint = {
id: string
position: LatLngTuple
name: string
type: PoiType
}
14 changes: 13 additions & 1 deletion src/components/common/tools.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'
import { sanitizeFilename } from './tools'
import { decodeFromBase64, encodeToBase64, sanitizeFilename } from './tools'

describe('sanitizeFilename', () => {
it('nothing to sanitize', () => {
Expand Down Expand Up @@ -47,3 +47,15 @@ describe('sanitizeFilename', () => {
expect(sanitized).toStrictEqual('Foo < Bar')
})
})

describe('Base64 encode/decode', () => {
it('encode UTF-8', () => {
const encoded = encodeToBase64('Hotel Don Cándido')
expect(encoded).toStrictEqual('SG90ZWwgRG9uIEPDoW5kaWRv')
})

it('decode UTF-8', () => {
const encoded = decodeFromBase64('SG90ZWwgRG9uIEPDoW5kaWRv')
expect(encoded).toStrictEqual('Hotel Don Cándido')
})
})
12 changes: 12 additions & 0 deletions src/components/common/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,15 @@ export const sanitizeFilename = (input: string) => {
return 'unnamed'
}
}

export const decodeFromBase64 = (input: string) => {
const binString = atob(input)
const bytes = Uint8Array.from(binString, (m) => m.codePointAt(0))
return new TextDecoder().decode(bytes)
}

export const encodeToBase64 = (input: string) => {
const bytes = new TextEncoder().encode(input)
const binString = String.fromCodePoint(...bytes)
return btoa(binString)
}
14 changes: 12 additions & 2 deletions src/components/merge/DownloadLink.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import React from 'react'
import { FiDownload } from 'react-icons/fi'
import { useUploadContext } from '../../context/UploadContext'
import { GeoJsonObject } from 'geojson'
import { encodeToBase64 } from '../common/tools.ts'

type DownloadLinkProps = {
type: string
trackname: string
geoJson?: GeoJsonObject | null
}

const DownloadLink: React.FC<DownloadLinkProps> = ({ type, trackname }) => {
const DownloadLink: React.FC<DownloadLinkProps> = ({
type,
trackname,
geoJson,
}) => {
const { mergedFile } = useUploadContext()

if (mergedFile !== null) {
Expand All @@ -17,7 +24,10 @@ const DownloadLink: React.FC<DownloadLinkProps> = ({ type, trackname }) => {
mergedFile.href +
'?mode=dl&type=' +
type +
(trackname.length > 0 ? `&name=${trackname}` : '')
(trackname.length > 0 ? `&name=${trackname}` : '') +
(geoJson != null
? `&wp=${encodeToBase64(JSON.stringify(geoJson))}`
: '')
}
>
<FiDownload className='inline mr-1 relative bottom-0.5' />
Expand Down
109 changes: 109 additions & 0 deletions src/components/merge/DraggableMarker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React, { FocusEvent, useRef, useState } from 'react'
import L, { LatLngTuple } from 'leaflet'
import { Marker, Popup } from 'react-leaflet'
import { FaTrashCan } from 'react-icons/fa6'
import { WayPoint } from '../../@types/gps.ts'

type DraggableMarkerProps = {
position: LatLngTuple
waypoint: WayPoint
markerPositions: WayPoint[]
setMarkerPositions: (waypoints: WayPoint[]) => void
}

const DraggableMarker: React.FC<DraggableMarkerProps> = ({
position,
waypoint,
markerPositions,
setMarkerPositions,
}) => {
const [isEditing, setIsEditing] = useState(false)
const myInputRef = useRef<HTMLDivElement>(null)

const changePosition = (event: L.DragEndEvent) => {
const newPosition = event.target.getLatLng()
setMarkerPositions(
markerPositions.map((wp) =>
wp.id === waypoint.id
? {
...wp,
position: [newPosition.lat, newPosition.lng],
}
: wp
)
)
}

const changeName = (event: FocusEvent) => {
const newContent = event.target.textContent
setIsEditing(false)
setMarkerPositions(
markerPositions.map((wp) =>
wp.id === waypoint.id
? {
...wp,
name: newContent ?? '',
}
: wp
)
)
}

const removeWaypoint = (id: string) => () => {
setMarkerPositions(markerPositions.filter((wp) => wp.id !== id))
}

const setFocus = () => {
myInputRef.current?.focus()
}

const removeFocus = () => {
myInputRef.current?.blur()
setIsEditing(false)
}

return (
<Marker
position={position}
draggable
eventHandlers={{ dragend: changePosition }}
key={waypoint.id}
>
<Popup>
<div className='flex flex-col'>
<div
ref={myInputRef}
className='text-lg'
contentEditable
onBlur={changeName}
onFocus={() => setIsEditing(true)}
onChange={() => setIsEditing(false)}
dangerouslySetInnerHTML={{ __html: waypoint.name }}
/>

<div className='flex justify-between'>
<div>
{isEditing ? (
<div className='underline' onClick={removeFocus}>
Save
</div>
) : (
<div className='underline' onClick={setFocus}>
Edit
</div>
)}
</div>
<div>
<FaTrashCan
className='ml-1 relative self-end'
onClick={removeWaypoint(waypoint.id)}
/>
</div>
</div>
</div>
</Popup>
</Marker>
)
}

export default DraggableMarker
24 changes: 24 additions & 0 deletions src/components/merge/FitBoundsButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React, { RefObject } from 'react'
import { Polyline as LeafletPolyline } from 'leaflet'
import { useMap } from 'react-leaflet'
import { MdCenterFocusStrong } from 'react-icons/md'

type FitBoundsButtonProps = {
polylineRef: RefObject<LeafletPolyline>
}

const FitBoundsButton: React.FC<FitBoundsButtonProps> = ({ polylineRef }) => {
const map = useMap()

const handleFitBounds = () => {
map.fitBounds(polylineRef.current!.getBounds())
}

return (
<div id='controlButton' onClick={handleFitBounds}>
<MdCenterFocusStrong className='text-5xl text-black' />
</div>
)
}

export default FitBoundsButton
Loading

0 comments on commit 180b4e4

Please sign in to comment.