diff --git a/components/ImportFileButton.tsx b/components/ImportFileButton.tsx new file mode 100644 index 0000000..ee175c4 --- /dev/null +++ b/components/ImportFileButton.tsx @@ -0,0 +1,32 @@ +import InputLabel from '@mui/material/InputLabel'; +import { OverridableStringUnion } from '@mui/types'; +import Button, { ButtonPropsColorOverrides } from '@mui/material/Button'; +import { ChangeEvent, ReactNode, useRef } from 'react'; + +export default function ImportFileButton({ + children, + onFile, + color, +}: { + children?: ReactNode | ReactNode[]; + onFile: (file: File) => void; + color?: OverridableStringUnion< + 'inherit' | 'primary' | 'secondary' | 'success' | 'error' | 'info' | 'warning', + ButtonPropsColorOverrides + >; +}) { + const uploadInputRef = useRef(null); + + const onChange = (e: ChangeEvent) => { + onFile(e.target.files[0]); + }; + + return ( + + ); +} diff --git a/components/MapMarker.tsx b/components/map/Marker.tsx similarity index 80% rename from components/MapMarker.tsx rename to components/map/Marker.tsx index 8dab354..2273342 100644 --- a/components/MapMarker.tsx +++ b/components/map/Marker.tsx @@ -2,8 +2,8 @@ import L from 'leaflet'; import { Marker, Popup } from 'react-leaflet'; import { useEffect } from 'react'; -import MarkerIcon from '../node_modules/leaflet/dist/images/marker-icon.png'; -import MarkerShadow from '../node_modules/leaflet/dist/images/marker-shadow.png'; +import MarkerIcon from '../../node_modules/leaflet/dist/images/marker-icon.png'; +import MarkerShadow from '../../node_modules/leaflet/dist/images/marker-shadow.png'; export default function MapMarker({ map, position }) { useEffect(() => { diff --git a/components/OpenStreetMap.tsx b/components/map/OpenStreetMap.tsx similarity index 100% rename from components/OpenStreetMap.tsx rename to components/map/OpenStreetMap.tsx diff --git a/components/map/Track.tsx b/components/map/Track.tsx new file mode 100644 index 0000000..748d623 --- /dev/null +++ b/components/map/Track.tsx @@ -0,0 +1,37 @@ +import { useEffect, useState } from 'react'; +import { Polyline } from 'react-leaflet'; + +const mintrackpointdelta = 0.0001; + +export default function MapTrack({ map, track }) { + const { trackpoints } = track?.tracks[0]?.segments[0] || []; + const [polyline, setPolyline] = useState([]); + + useEffect(() => { + const pointArray = []; + + if (trackpoints.length > 0) { + let lastlon = parseFloat(trackpoints[0].lon); + let lastlat = parseFloat(trackpoints[0].lat); + + pointArray.push([lastlon, lastlat]); + + for (let i = 1; i < trackpoints.length; i++) { + const lon = parseFloat(trackpoints[i].lon); + const lat = parseFloat(trackpoints[i].lat); + + const latdiff = lat - lastlat; + const londiff = lon - lastlon; + if (Math.sqrt(latdiff * latdiff + londiff * londiff) > mintrackpointdelta) { + lastlon = lon; + lastlat = lat; + pointArray.push([lat, lon]); + } + } + } + + setPolyline(pointArray); + }, [trackpoints]); + + return ; +} diff --git a/lib/gpx_parser.ts b/lib/gpx_parser.ts new file mode 100644 index 0000000..e892b75 --- /dev/null +++ b/lib/gpx_parser.ts @@ -0,0 +1,57 @@ +export async function parseGpxFile2Document(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (e) => { + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(e.target.result as string, 'text/xml'); + const errorNode = xmlDoc.querySelector('parsererror'); + if (errorNode) { + reject(new Error('Failed to parse the GPX file')); + } else { + resolve(xmlDoc); + } + }; + reader.readAsText(file); + }); +} + +function* elIter(el: HTMLCollectionOf, callback: (el: Element) => any) { + for (let i = 0; i < el.length; i++) { + yield callback(el[i]); + } +} + +function getElValue(el: HTMLCollectionOf) { + return el[0].childNodes[0].nodeValue; +} + +function parseTrackpoints(trackpoints: HTMLCollectionOf) { + return [ + ...elIter(trackpoints, (trackpoint) => ({ + lon: parseFloat(trackpoint.getAttribute('lon')), + lat: parseFloat(trackpoint.getAttribute('lat')), + ele: getElValue(trackpoint.getElementsByTagName('ele')), + })), + ]; +} + +function parseSegments(segments: HTMLCollectionOf) { + return [ + ...elIter(segments, (segment) => ({ + trackpoints: parseTrackpoints(segment.getElementsByTagName('trkpt')), + })), + ]; +} + +function parseTracks(tracks: HTMLCollectionOf) { + return [ + ...elIter(tracks, (track) => ({ + name: getElValue(track.getElementsByTagName('name')), + segments: parseSegments(track.getElementsByTagName('trkseg')), + })), + ]; +} + +export function gpxDocument2obj(doc: Document) { + return { tracks: parseTracks(doc.documentElement.getElementsByTagName('trk')) }; +} diff --git a/pages/ride/map/index.tsx b/pages/ride/map/index.tsx index 4b6441d..a72ca39 100644 --- a/pages/ride/map/index.tsx +++ b/pages/ride/map/index.tsx @@ -7,16 +7,23 @@ import Button from '@mui/material/Button'; import { useState } from 'react'; import MyHead from '../../../components/MyHead'; import Title from '../../../components/Title'; -import OpenStreetMap from '../../../components/OpenStreetMap'; -import MapMarker from '../../../components/MapMarker'; +import OpenStreetMap from '../../../components/map/OpenStreetMap'; +import MapMarker from '../../../components/map/Marker'; +import Track from '../../../components/map/Track'; +import ImportFileButton from '../../../components/ImportFileButton'; +import { gpxDocument2obj, parseGpxFile2Document } from '../../../lib/gpx_parser'; type OpenStreetMapArg = Parameters[0]; type MapMarkerArg = Parameters[0]; +type TrackArg = Parameters[0]; -const DynamicMap = dynamic(() => import('../../../components/OpenStreetMap'), { +const DynamicMap = dynamic(() => import('../../../components/map/OpenStreetMap'), { ssr: false, }); -const DynamicMapMarker = dynamic(() => import('../../../components/MapMarker'), { +const DynamicMapMarker = dynamic(() => import('../../../components/map/Marker'), { + ssr: false, +}); +const DynamicTrack = dynamic(() => import('../../../components/map/Track'), { ssr: false, }); @@ -38,9 +45,30 @@ function MyLocationButton({ setPosition }) { ); } +function Tracks({ map, tracks }) { + return ( + <> + {tracks.map((track, i: number) => { + return ; + })} + + ); +} + export default function RideMap() { const [map, setMap] = useState(null); const [coord, setCoord] = useState([51.505, -0.09]); + const [tracks, setTracks] = useState([]); // TODO reducer? + + const importGpx = (file: File) => { + parseGpxFile2Document(file) + .then((xmlDoc: Document) => { + setTracks([...tracks, gpxDocument2obj(xmlDoc)]); + }) + .catch((err) => { + console.error('Would be nice to show this:', err); + }); + }; return ( @@ -57,11 +85,12 @@ export default function RideMap() { }} > - + Import GPX + diff --git a/tsconfig.json b/tsconfig.json index 5bee8c4..5ccb0e7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es2022", "lib": [ "dom", "dom.iterable",