diff --git a/mapper/package.json b/mapper/package.json index da41c87..8d0ec7d 100644 --- a/mapper/package.json +++ b/mapper/package.json @@ -15,6 +15,7 @@ "react-dom": "^17.0.2", "react-is": "^17.0.2", "react-leaflet": "^4.2.1", + "react-range-slider-input": "^3.0.7", "react-scripts": "4.0.3", "web-vitals": "^1.0.1" }, @@ -31,8 +32,8 @@ ] }, "browserslist": [ - ">0.2%", - "not dead", - "not op_mini all" + ">0.2%", + "not dead", + "not op_mini all" ] } diff --git a/mapper/src/App.css b/mapper/src/App.css index 65f533d..73aa191 100644 --- a/mapper/src/App.css +++ b/mapper/src/App.css @@ -39,6 +39,16 @@ h2 { z-index: 9999; } +.time-slider { + padding: 1em 0.5em; +} + +.range { + color: white; + text-align: center; + padding: 0.5rem; +} + .submenu { background-color: #666; max-width: 40em; diff --git a/mapper/src/App.js b/mapper/src/App.js index 58cc4cf..300b414 100644 --- a/mapper/src/App.js +++ b/mapper/src/App.js @@ -5,35 +5,23 @@ 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'; + let tzcache = {}; +// 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'}, - {id: 1, len: 'Week', win: '3m', full: '3 min', delta: [7, 'days'], format: 'HH'}, - {id: 2, len: 'Week', win: '10m', full: '10 min', delta: [7, 'days'], format: 'HH'}, - {id: 3, len: 'Month', win: '10m', full: '10 min', delta: [1, 'months'], format: 'D'}, - {id: 4, len: 'Month', win: '1h', full: '1 hour', delta: [1, 'months'], format: 'D'}, - {id: 5, len: 'Year', win: '2h', full: '2 hours', delta: [1, 'years'], format: 'M/D'}, - {id: 6, len: 'Year', win: '1d', full: '1 day', delta: [1, 'years'], format: 'M/D'}, + {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', 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 units = { - 'PM10': ' ug/m³', - 'PM2.5': ' ug/m³', - 'VOC': ' / 500', - 'CO2': ' ppm', - 'Energy': ' kWh', - 'Power': ' W', - 'Temperature': ' °C', - 'Humidity': '%', - 'Setpoint': ' °C', - 'State': '', - 'Lux': ' lx', -}; - function useSensor(measurement, name, end, duration) { const [data, setData] = useState(false); const [loading, setLoading] = useState(false); @@ -63,10 +51,10 @@ function useSensor(measurement, name, end, duration) { -function Owntracks({end, duration}) { +function Owntracks({end, duration, range}) { const [data, loading] = useSensor('owntracks', 'OwnTracks', end, duration); - const coords = data.length ? data.map(({ lat, lon }) => [lat, lon]).filter(([lat, lon]) => lat !== null || lon !== null) : []; + const coords = data.length ? data.filter(x => !range || (x.time >= range[0] && x.time <= range[1])).map(({ lat, lon }) => [lat, lon]).filter(([lat, lon]) => lat !== null || lon !== null) : []; const handleSubmit = (e) => { e.preventDefault(); @@ -102,79 +90,122 @@ function Owntracks({end, duration}) { } -function Graphs({end, duration}) { +function Graphs({end, duration, range}) { return (
- +
); } -function Menu({duration, setDuration, end, setEnd}) { +function Menu({duration, setDuration, end, setEnd, range, setRange}) { const [submenu, setSubmenu] = useState(false); + const [showRange, setShowRange] = useState(false); const chooseDuration = (x) => { setSubmenu(false); + setRange(false); setDuration(x); }; const chooseEnd = (x) => { setSubmenu(false); const newEnd = x.add(...duration.delta); + setRange(false); setEnd(newEnd); }; const chooseNow = (x) => { setSubmenu(false); + setRange(false); setEnd(moment()); }; const next = () => { setSubmenu(false); + setRange(false); setEnd(prevEnd => moment(prevEnd).add(...duration.delta)); } const prev = () => { setSubmenu(false); + setRange(false); setEnd(prevEnd => moment(prevEnd).subtract(...duration.delta)); } + const onSlider = (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); + setRange([lowStr, highStr]); + + } + return (
- {!!submenu &&
- {submenu === 'end' && - <> -
-

Choose start date:

- -
- -
- chooseEnd(x)} - /> -
- - - - } - - {submenu === 'duration' && - <> -
-

Choose duration:

- -
- - {durations.map(x => - - )} - - } + {showRange &&
+ {moment(range[0]).format('lll')} - {moment(range[1]).format('lll')}
} + {submenu ? +
+ {submenu === 'end' && + <> +
+

Choose start date:

+ +
+ +
+ chooseEnd(x)} + /> +
+ + + + } + + {submenu === 'duration' && + <> +
+

Choose duration:

+ +
+ + {durations.map(x => + + )} + + } +
+ : +
+ setShowRange(true)} + onThumbDragEnd={() => setShowRange(false)} + onRangeDragStart={() => setShowRange(true)} + onRangeDragEnd={() => setShowRange(false)} + /> + +
+ } +
@@ -201,6 +232,7 @@ function Menu({duration, setDuration, end, setEnd}) { function App() { const [duration, setDuration] = useState(durations[0]); const [end, setEnd] = useState(moment()); + const [range, setRange] = useState(false); return (
@@ -209,11 +241,14 @@ function App() { setDuration={setDuration} end={end} setEnd={setEnd} + range={range} + setRange={setRange} />
); diff --git a/mapper/yarn.lock b/mapper/yarn.lock index a4f8850..07809cc 100644 --- a/mapper/yarn.lock +++ b/mapper/yarn.lock @@ -3471,6 +3471,11 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" +clsx@^1.1.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" + integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -3710,6 +3715,11 @@ core-js@^2.4.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== +core-js@^3.22.4: + version "3.37.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.37.1.tgz#d21751ddb756518ac5a00e4d66499df981a62db9" + integrity sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw== + core-js@^3.6.5: version "3.33.0" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.33.0.tgz#70366dbf737134761edb017990cf5ce6c6369c40" @@ -9512,6 +9522,14 @@ react-leaflet@^4.2.1: dependencies: "@react-leaflet/core" "^2.1.0" +react-range-slider-input@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/react-range-slider-input/-/react-range-slider-input-3.0.7.tgz#88ceb118b33d7eb0550cec1f77fc3e60e0f880f9" + integrity sha512-yAJDDMUNkILOcZSCLCVbwgnoAM3v0AfdDysTCMXDwY/+ZRNRlv98TyHbVCwPFEd7qiI8Ca/stKb0GAy//NybYw== + dependencies: + clsx "^1.1.1" + core-js "^3.22.4" + react-refresh@^0.8.3: version "0.8.3" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"