import React, { useState, useEffect, useRef, useMemo } from 'react'; import * as leaflet from 'leaflet'; import 'leaflet-polylinedecorator'; import { MapContainer, TileLayer, useMap, useMapEvents } from 'react-leaflet'; import Datetime from 'react-datetime'; import 'react-datetime/css/react-datetime.css'; import axios from 'axios'; import moment from 'moment-timezone'; import RangeSlider from 'react-range-slider-input'; import './App.css'; import 'leaflet/dist/leaflet.css'; import 'react-range-slider-input/dist/style.css'; // num: number of steps per duration // secs: number of seconds per step const durations = [ {id: 0, len: 'Day', win: '1m', full: '1 min', delta: [1, 'days'], format: 'HH', num: 1440, secs: 60}, {id: 1, len: 'Week', win: '3m', full: '3 min', delta: [7, 'days'], format: 'HH', num: 3360, secs: 180}, {id: 2, len: 'Month', shortLen: 'Mth', win: '10m', full: '10 min', delta: [1, 'months'], format: 'D', num: 4380, secs: 600}, {id: 3, len: 'Year', win: '2h', full: '2 hour', delta: [1, 'years'], format: 'M/D', num: 4380, secs: 7200}, ]; const parseSlider = (end, duration, slider) => { //console.log(slider); // good luck remembering how this works const lowOffset = slider[0] * duration.secs - duration.num * duration.secs; const highOffset = slider[1] * duration.secs - duration.num * duration.secs; const low = moment.unix(end.unix() + lowOffset); const high = moment.unix(end.unix() + highOffset); const lowStr = low.utc().format('YYYY-MM-DDTHH:mm:ss[Z]'); const highStr = high.utc().format('YYYY-MM-DDTHH:mm:ss[Z]'); //console.log(lowStr, highStr); return [lowStr, highStr]; }; //async function sha256(source) { // const sourceBytes = new TextEncoder().encode(source); // const digest = await crypto.subtle.digest('SHA-26', sourceBytes); // const resultBytes = [...new Uint8Array(digest)]; // return resultBytes.map(x => x.toString(16).padStart(2, '0')).join(''); //} function useSensor(measurement, name, end, duration) { const [data, setData] = useState(false); const [loading, setLoading] = useState(false); useEffect(() => { const get = async() => { setLoading(true); try { const api_key = localStorage.getItem('api_key', 'null'); const params = { end: end.unix(), duration: duration.len.toLowerCase(), window: duration.win, api_key: api_key }; const res = await axios.get( 'https://sensors-api.dns.t0.vc/history/'+measurement+'/'+name, { params: params }, ); setData((d) => (res.data)); setLoading(false); } catch (error) { console.log(error); } }; get(); }, [end, duration]); return [data, loading]; }; function MapViewManager({ coords, mapState, setMapState, loading, setSubmenu }) { const map = useMap(); // Effect 1: Handle map events (pan/zoom) from the user useMapEvents({ mousedown: () => setSubmenu(false), zoomstart: () => setSubmenu(false), moveend: () => { const center = map.getCenter(); const newZoom = map.getZoom(); const newCenter = [center.lat, center.lng]; setMapState(prevState => { // A small tolerance for floating point comparisons const tolerance = 1e-5; if (!prevState.center) { return { zoom: newZoom, center: newCenter }; } const zoomChanged = prevState.zoom !== newZoom; const centerChanged = Math.abs(prevState.center[0] - newCenter[0]) > tolerance || Math.abs(prevState.center[1] - newCenter[1]) > tolerance; if (zoomChanged || centerChanged) { return { zoom: newZoom, center: newCenter }; } // If nothing changed, return the previous state to prevent a re-render return prevState; }); }, }); // Effect 2: Handle programmatic view changes (refitting or setting from state) useEffect(() => { // Don't do anything while loading new data if (loading) return; // Case A: A refit is needed (signaled by null center) if (mapState.center === null && coords.length > 0) { const bounds = leaflet.latLngBounds(coords); if (bounds.isValid()) { map.fitBounds(bounds); // After fitting, the 'moveend' event will fire and update the state naturally. } } // Case B: A center is set in the state, ensure map is synced else if (mapState.center) { const currentCenter = map.getCenter(); const currentZoom = map.getZoom(); if (currentCenter.lat !== mapState.center[0] || currentCenter.lng !== mapState.center[1] || currentZoom !== mapState.zoom) { map.setView(mapState.center, mapState.zoom); } } }, [coords, mapState, loading, map, setMapState]); return null; } function PolylineWithArrows({ coords, showDirection }) { const map = useMap(); const polylineRef = useRef(null); const decoratorRef = useRef(null); useEffect(() => { if (polylineRef.current) { map.removeLayer(polylineRef.current); } if (decoratorRef.current) { map.removeLayer(decoratorRef.current); } if (coords && coords.length > 1) { const polyline = leaflet.polyline(coords, { color: 'blue' }); polylineRef.current = polyline; map.addLayer(polyline); if (showDirection) { const decorator = leaflet.polylineDecorator(polyline, { patterns: [ { offset: 25, repeat: 100, symbol: leaflet.Symbol.arrowHead({ pixelSize: 10, polygon: false, pathOptions: { stroke: true, color: 'blue', weight: 3 } }) } ] }); decoratorRef.current = decorator; map.addLayer(decorator); } else { decoratorRef.current = null; } } return () => { if (polylineRef.current) { map.removeLayer(polylineRef.current); } if (decoratorRef.current) { map.removeLayer(decoratorRef.current); } }; }, [coords, map, showDirection]); return null; } function Map({end, duration, slider, mapState, setMapState, setSubmenu, showDirection}) { const [data, loading] = useSensor('owntracks', 'OwnTracks', end, duration); const range = useMemo(() => parseSlider(end, duration, slider), [end, duration, slider]); const coords = useMemo(() => { // 1. Guard against invalid top-level data if (!Array.isArray(data)) { return []; } const result = []; const [startTime, endTime] = range; // 2. Loop through the data for (const point of data) { // 3. Guard against malformed points if (!point || typeof point !== 'object') { continue; } const { lat, lon, time } = point; // 4. Guard against invalid time if (typeof time !== 'string' || time.length === 0) { continue; } // 5. Guard against invalid coordinates (null, undefined, NaN, non-number) if (typeof lat !== 'number' || typeof lon !== 'number' || !isFinite(lat) || !isFinite(lon)) { continue; } // 6. Now that all data is known to be valid, filter by time if (time >= startTime && time <= endTime) { result.push([lat, lon]); } } return result; }, [data, range]); const handleSubmit = (e) => { e.preventDefault(); const api_key = e.target[0].value; localStorage.setItem('api_key', api_key); } return (
Loading...
: coords.length ? (No data
> }