From d2c77ac7bdea004b537f6684884b23946d15886c Mon Sep 17 00:00:00 2001 From: Kai Arseneau Date: Tue, 17 Sep 2024 14:13:32 -0400 Subject: [PATCH 01/19] Topbar fix --- front-end/package-lock.json | 432 ++++++++++++++++-- front-end/package.json | 9 +- .../Topbar/{Topbar.jsx => Topbar.tsx} | 9 +- front-end/src/vite-env.d.ts | 1 + front-end/tsconfig.json | 29 ++ front-end/tsconfig.node.json | 9 + front-end/vite.config.js | 30 -- front-end/vite.config.ts | 28 ++ 8 files changed, 471 insertions(+), 76 deletions(-) rename front-end/src/components/Topbar/{Topbar.jsx => Topbar.tsx} (91%) create mode 100644 front-end/src/vite-env.d.ts create mode 100644 front-end/tsconfig.json create mode 100644 front-end/tsconfig.node.json delete mode 100644 front-end/vite.config.js create mode 100644 front-end/vite.config.ts diff --git a/front-end/package-lock.json b/front-end/package-lock.json index fd534d58..4fa61caa 100644 --- a/front-end/package-lock.json +++ b/front-end/package-lock.json @@ -8,6 +8,9 @@ "name": "dataviewer", "version": "1.0.0", "dependencies": { + "@types/node": "^22.5.5", + "@types/react": "^18.3.6", + "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", "eslint": "^8.57.0", "eslint-plugin-react": "^7.34.1", @@ -27,8 +30,10 @@ "react-player": "^2.15.1", "react-resize-detector": "^10.0.1", "react-router-dom": "^6.22.3", + "typescript": "^5.6.2", "vite": "^5.4.3", - "vite-plugin-commonjs": "^0.10.1" + "vite-plugin-commonjs": "^0.10.1", + "vitest": "^2.1.1" }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", @@ -2977,13 +2982,33 @@ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" }, "node_modules/@types/node": { - "version": "20.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz", - "integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==", - "optional": true, - "peer": true, + "version": "22.5.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz", + "integrity": "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" + }, + "node_modules/@types/react": { + "version": "18.3.6", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.6.tgz", + "integrity": "sha512-CnGaRYNu2iZlkGXGrOYtdg5mLK8neySj0woZ4e2wF/eli2E6Sazmq5X+Nrj6OBrrFVQfJWTUFeqAzoRhWQXYvg==", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dependencies": { + "@types/react": "*" } }, "node_modules/@ungap/structured-clone": { @@ -3017,6 +3042,106 @@ "node": ">=0.10.0" } }, + "node_modules/@vitest/expect": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.1.tgz", + "integrity": "sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==", + "dependencies": { + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.1.tgz", + "integrity": "sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==", + "dependencies": { + "@vitest/spy": "^2.1.0-beta.1", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.11" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/spy": "2.1.1", + "msw": "^2.3.5", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.1.tgz", + "integrity": "sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.1.tgz", + "integrity": "sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==", + "dependencies": { + "@vitest/utils": "2.1.1", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.1.tgz", + "integrity": "sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==", + "dependencies": { + "@vitest/pretty-format": "2.1.1", + "magic-string": "^0.30.11", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.1.tgz", + "integrity": "sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==", + "dependencies": { + "tinyspy": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.1.tgz", + "integrity": "sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==", + "dependencies": { + "@vitest/pretty-format": "2.1.1", + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.11.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", @@ -3205,6 +3330,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "engines": { + "node": ">=12" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -3327,6 +3460,14 @@ "optional": true, "peer": true }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -3384,6 +3525,21 @@ } ] }, + "node_modules/chai": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -3397,6 +3553,14 @@ "node": ">=4" } }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "engines": { + "node": ">= 16" + } + }, "node_modules/classnames": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", @@ -3540,6 +3704,11 @@ "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", "dev": true }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -3604,11 +3773,11 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -3619,6 +3788,14 @@ } } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -4225,6 +4402,14 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -4412,6 +4597,14 @@ "node": ">=6.9.0" } }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "engines": { + "node": "*" + } + }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -5225,6 +5418,14 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -5242,6 +5443,14 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", @@ -5285,9 +5494,9 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/nanoid": { "version": "3.3.7", @@ -5560,6 +5769,19 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==" + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", @@ -6139,6 +6361,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==" + }, "node_modules/snake-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", @@ -6178,6 +6405,16 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==" + }, + "node_modules/std-env": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==" + }, "node_modules/string.prototype.matchall": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", @@ -6417,6 +6654,40 @@ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==" + }, + "node_modules/tinyexec": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.0.tgz", + "integrity": "sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==" + }, + "node_modules/tinypool": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz", + "integrity": "sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -6523,18 +6794,15 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, - "optional": true, - "peer": true, + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/unbox-primitive": { @@ -6552,11 +6820,9 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "optional": true, - "peer": true + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", @@ -6698,6 +6964,26 @@ } } }, + "node_modules/vite-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.1.tgz", + "integrity": "sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.6", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/vite-plugin-commonjs": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/vite-plugin-commonjs/-/vite-plugin-commonjs-0.10.1.tgz", @@ -6709,14 +6995,6 @@ "vite-plugin-dynamic-import": "^1.5.0" } }, - "node_modules/vite-plugin-commonjs/node_modules/magic-string": { - "version": "0.30.11", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", - "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, "node_modules/vite-plugin-dynamic-import": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/vite-plugin-dynamic-import/-/vite-plugin-dynamic-import-1.5.0.tgz", @@ -6728,14 +7006,6 @@ "magic-string": "^0.30.1" } }, - "node_modules/vite-plugin-dynamic-import/node_modules/magic-string": { - "version": "0.30.11", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", - "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, "node_modules/vite/node_modules/rollup": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.2.tgz", @@ -6770,6 +7040,69 @@ "fsevents": "~2.3.2" } }, + "node_modules/vitest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.1.tgz", + "integrity": "sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==", + "dependencies": { + "@vitest/expect": "2.1.1", + "@vitest/mocker": "2.1.1", + "@vitest/pretty-format": "^2.1.1", + "@vitest/runner": "2.1.1", + "@vitest/snapshot": "2.1.1", + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", + "chai": "^5.1.1", + "debug": "^4.3.6", + "magic-string": "^0.30.11", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.0", + "tinypool": "^1.0.0", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.1", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.1", + "@vitest/ui": "2.1.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6856,6 +7189,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/front-end/package.json b/front-end/package.json index 6a99d632..902e8bf0 100644 --- a/front-end/package.json +++ b/front-end/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "private": true, "dependencies": { + "@types/node": "^22.5.5", + "@types/react": "^18.3.6", + "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", "eslint": "^8.57.0", "eslint-plugin-react": "^7.34.1", @@ -22,12 +25,14 @@ "react-player": "^2.15.1", "react-resize-detector": "^10.0.1", "react-router-dom": "^6.22.3", + "typescript": "^5.6.2", "vite": "^5.4.3", - "vite-plugin-commonjs": "^0.10.1" + "vite-plugin-commonjs": "^0.10.1", + "vitest": "^2.1.1" }, "scripts": { "start": "vite", - "build": "vite build", + "build": "tsc && vite build", "serve": "vite preview" }, "eslintConfig": { diff --git a/front-end/src/components/Topbar/Topbar.jsx b/front-end/src/components/Topbar/Topbar.tsx similarity index 91% rename from front-end/src/components/Topbar/Topbar.jsx rename to front-end/src/components/Topbar/Topbar.tsx index 035250ec..966fc7e7 100644 --- a/front-end/src/components/Topbar/Topbar.jsx +++ b/front-end/src/components/Topbar/Topbar.tsx @@ -4,10 +4,15 @@ import { ApiUtil } from '@lib/apiUtils.js'; import bajalogo from '@assets/bajalogo.png'; import loadingImg from '@assets/loading.gif'; import { MAX_VIEWS } from '@components/views/viewsConfig.js'; -import { icons } from '@lib/assets.js'; +import { icons } from '@lib/assets'; +interface TopbarProps { + numViews: number; + setNumViews: (num: number) => void; + setModal: (modal: string) => void; +} -const Topbar = ({ setModal, numViews, setNumViews }) => { +const Topbar = ({ setModal, numViews, setNumViews }: TopbarProps) => { const [liveStatus, setLiveStatus] = useState(false); //This function notifies the backend to begin listening on a certain port for live data diff --git a/front-end/src/vite-env.d.ts b/front-end/src/vite-env.d.ts new file mode 100644 index 00000000..151aa685 --- /dev/null +++ b/front-end/src/vite-env.d.ts @@ -0,0 +1 @@ +/// \ No newline at end of file diff --git a/front-end/tsconfig.json b/front-end/tsconfig.json new file mode 100644 index 00000000..d2b5871d --- /dev/null +++ b/front-end/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "noImplicitAny": false, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "baseUrl": "./src", + "paths": { + "@assets/*": ["assets/*"], + "@components/*": ["components/*"], + "@lib/*": ["lib/*"], + "@styles/*": ["styles/*"] + }, + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} \ No newline at end of file diff --git a/front-end/tsconfig.node.json b/front-end/tsconfig.node.json new file mode 100644 index 00000000..aac0ce5a --- /dev/null +++ b/front-end/tsconfig.node.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} \ No newline at end of file diff --git a/front-end/vite.config.js b/front-end/vite.config.js deleted file mode 100644 index 03f8b56b..00000000 --- a/front-end/vite.config.js +++ /dev/null @@ -1,30 +0,0 @@ -import { defineConfig } from 'vite'; -import { resolve } from 'path'; -import react from '@vitejs/plugin-react'; -import commonjs from 'vite-plugin-commonjs'; -import { fileURLToPath } from 'url'; - -const __dirname = fileURLToPath(new URL('.', import.meta.url)); -const root = resolve(__dirname, 'src'); - -export default defineConfig(() => { - return { - build: { - outDir: 'build', - }, - plugins: [ - react(), - commonjs({ - include: ['node_modules/highcharts-multicolor-series/**'] - }) - ], - resolve: { - alias: { - '@assets': resolve(root, 'assets'), - '@components': resolve(root, 'components'), - '@lib': resolve(root, 'lib'), - '@styles': resolve(root, 'styles'), - }, - }, - }; -}); \ No newline at end of file diff --git a/front-end/vite.config.ts b/front-end/vite.config.ts new file mode 100644 index 00000000..93ffaa88 --- /dev/null +++ b/front-end/vite.config.ts @@ -0,0 +1,28 @@ +import { defineConfig } from 'vite'; +import { resolve } from 'path'; +import react from '@vitejs/plugin-react'; +import commonjs from 'vite-plugin-commonjs'; + +const root = resolve(__dirname, 'src'); + +export default defineConfig({ + assetsInclude: ['**/*.svg', '**/*.png', '**/*.jpg', '**/*.jpeg', '**/*.gif', '**/*.webp', '**/*.avif'], + server: { + strictPort: true, + }, + build: { + outDir: 'build', + }, + plugins: [ + react(), + commonjs(), + ], + resolve: { + alias: { + '@assets': resolve(root, 'assets'), + '@components': resolve(root, 'components'), + '@lib': resolve(root, 'lib'), + '@styles': resolve(root, 'styles'), + }, + }, +}); From 33bd2cfdcbaa648001670416c6ac5c5037289caf Mon Sep 17 00:00:00 2001 From: Kai Arseneau Date: Tue, 1 Oct 2024 13:22:41 -0400 Subject: [PATCH 02/19] Clean-up --- front-end/src/components/Topbar/Topbar.tsx | 6 +++--- front-end/src/vite-env.d.ts | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/front-end/src/components/Topbar/Topbar.tsx b/front-end/src/components/Topbar/Topbar.tsx index 966fc7e7..a8b242a2 100644 --- a/front-end/src/components/Topbar/Topbar.tsx +++ b/front-end/src/components/Topbar/Topbar.tsx @@ -1,9 +1,9 @@ import './Topbar.css'; -import React, { useState } from 'react'; -import { ApiUtil } from '@lib/apiUtils.js'; +import { useState } from 'react'; +import { ApiUtil } from '@lib/apiUtils'; import bajalogo from '@assets/bajalogo.png'; import loadingImg from '@assets/loading.gif'; -import { MAX_VIEWS } from '@components/views/viewsConfig.js'; +import { MAX_VIEWS } from '@components/views/viewsConfig'; import { icons } from '@lib/assets'; interface TopbarProps { diff --git a/front-end/src/vite-env.d.ts b/front-end/src/vite-env.d.ts index 151aa685..168fb713 100644 --- a/front-end/src/vite-env.d.ts +++ b/front-end/src/vite-env.d.ts @@ -1 +1,3 @@ +// This file is needed to allow importing of images in TypeScript files +// The solution was found here: https://github.com/vitejs/vite/discussions/6799#discussioncomment-5393727 /// \ No newline at end of file From 372a64c833f7f1d88bff3e88a2f2d3a46b154d6e Mon Sep 17 00:00:00 2001 From: Kai Arseneau Date: Tue, 1 Oct 2024 16:18:32 -0400 Subject: [PATCH 03/19] Added types to API Also upgraded the backend for better types --- .../com/mcmasterbaja/FileAnalyzeResource.java | 10 +- .../com/mcmasterbaja/FileDeleteResource.java | 12 +- .../com/mcmasterbaja/FileUploadResource.java | 4 +- .../java/com/mcmasterbaja/model/MinMax.java | 16 ++ .../services/DefaultFileMetadataService.java | 6 +- .../services/FileMetadataService.java | 6 +- .../modal/download/DownloadModal.jsx | 2 +- .../components/modal/upload/UploadModal.jsx | 2 +- .../src/components/views/Chart/Chart.jsx | 2 +- front-end/src/lib/apiUtils.js | 166 ----------------- front-end/src/lib/apiUtils.ts | 173 ++++++++++++++++++ front-end/tsconfig.node.json | 1 + 12 files changed, 214 insertions(+), 186 deletions(-) create mode 100644 backend/src/main/java/com/mcmasterbaja/model/MinMax.java delete mode 100644 front-end/src/lib/apiUtils.js create mode 100644 front-end/src/lib/apiUtils.ts diff --git a/backend/src/main/java/com/mcmasterbaja/FileAnalyzeResource.java b/backend/src/main/java/com/mcmasterbaja/FileAnalyzeResource.java index f7851cf7..99159090 100644 --- a/backend/src/main/java/com/mcmasterbaja/FileAnalyzeResource.java +++ b/backend/src/main/java/com/mcmasterbaja/FileAnalyzeResource.java @@ -5,6 +5,7 @@ import com.mcmasterbaja.exceptions.InvalidArgumentException; import com.mcmasterbaja.live.Serial; import com.mcmasterbaja.model.AnalyzerParams; +import com.mcmasterbaja.model.MinMax; import com.mcmasterbaja.services.FileMetadataService; import com.mcmasterbaja.services.StorageService; import jakarta.inject.Inject; @@ -67,21 +68,22 @@ public RestResponse runAnalyzer(@BeanParam AnalyzerParams params) { @GET @jakarta.ws.rs.Path("minMax/{filekey}") - public Double[] getMinMax( + public MinMax getMinMax( @PathParam("filekey") String filekey, @QueryParam("column") String column) { logger.info("Getting min and max for file: " + filekey); String typeFolder = fileMetadataService.getTypeFolder(Paths.get(filekey)); Path targetPath = storageService.load(Paths.get(typeFolder)).resolve(filekey); - Double[] minMax = fileMetadataService.getMinMax(targetPath, column); + MinMax minMax = fileMetadataService.getMinMax(targetPath, column); return minMax; } @PATCH @jakarta.ws.rs.Path("togglelive") - public String toggleLive() { + public Boolean toggleLive() { logger.info("Toggling live data to: " + Serial.exit); + Boolean exit = Serial.exit; if (!Serial.exit) { Serial.exit = true; @@ -93,6 +95,6 @@ public String toggleLive() { .start(); } - return "Live data toggled to " + Serial.exit; + return exit; } } diff --git a/backend/src/main/java/com/mcmasterbaja/FileDeleteResource.java b/backend/src/main/java/com/mcmasterbaja/FileDeleteResource.java index fda9e16b..4546d6e1 100644 --- a/backend/src/main/java/com/mcmasterbaja/FileDeleteResource.java +++ b/backend/src/main/java/com/mcmasterbaja/FileDeleteResource.java @@ -16,34 +16,34 @@ public class FileDeleteResource { @DELETE @jakarta.ws.rs.Path("/file/{filekey}") - public String deleteFile(@PathParam("filekey") String filekey) { + public void deleteFile(@PathParam("filekey") String filekey) { logger.info("Deleting file: " + filekey); Path targetPath = Paths.get(filekey); storageService.delete(targetPath); - return "File deleted successfully"; + return; } @DELETE @jakarta.ws.rs.Path("/folder/{folderkey}") - public String deleteFolder(@PathParam("folderkey") String folderkey) { + public void deleteFolder(@PathParam("folderkey") String folderkey) { logger.info("Deleting folder: " + folderkey); Path targetPath = Paths.get(folderkey); storageService.deleteAll(targetPath); - return "All files deleted successfully"; + return; } @DELETE @jakarta.ws.rs.Path("/all") - public String deleteAllFiles() { + public void deleteAllFiles() { logger.info("Deleting all files"); storageService.deleteAll(); storageService.init(); - return "All files deleted successfully"; + return; } } diff --git a/backend/src/main/java/com/mcmasterbaja/FileUploadResource.java b/backend/src/main/java/com/mcmasterbaja/FileUploadResource.java index 3c7ed25e..b8d90f4e 100644 --- a/backend/src/main/java/com/mcmasterbaja/FileUploadResource.java +++ b/backend/src/main/java/com/mcmasterbaja/FileUploadResource.java @@ -23,7 +23,7 @@ public class FileUploadResource { @POST @jakarta.ws.rs.Path("/file") @Consumes(MediaType.MULTIPART_FORM_DATA) - public String uploadFile( + public void uploadFile( @RestForm("fileName") String fileName, @RestForm("fileData") @PartType(MediaType.APPLICATION_OCTET_STREAM) InputStream fileData) { @@ -71,7 +71,5 @@ public String uploadFile( } throw new IllegalArgumentException("Invalid filetype: " + fileExtension); } - - return "File uploaded successfully"; } } diff --git a/backend/src/main/java/com/mcmasterbaja/model/MinMax.java b/backend/src/main/java/com/mcmasterbaja/model/MinMax.java new file mode 100644 index 00000000..fcac5897 --- /dev/null +++ b/backend/src/main/java/com/mcmasterbaja/model/MinMax.java @@ -0,0 +1,16 @@ +package com.mcmasterbaja.model; + +import lombok.Getter; +import lombok.ToString; + +@ToString +@Getter +public class MinMax { + private final Double min; + private final Double max; + + public MinMax(Double min, Double max) { + this.min = min; + this.max = max; + } +} diff --git a/backend/src/main/java/com/mcmasterbaja/services/DefaultFileMetadataService.java b/backend/src/main/java/com/mcmasterbaja/services/DefaultFileMetadataService.java index 551f9f80..da8fc23b 100644 --- a/backend/src/main/java/com/mcmasterbaja/services/DefaultFileMetadataService.java +++ b/backend/src/main/java/com/mcmasterbaja/services/DefaultFileMetadataService.java @@ -6,6 +6,8 @@ import com.mcmasterbaja.exceptions.FileNotFoundException; import com.mcmasterbaja.exceptions.MalformedCsvException; import com.mcmasterbaja.exceptions.StorageException; +import com.mcmasterbaja.model.MinMax; + import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import java.io.BufferedReader; @@ -49,7 +51,7 @@ public long getSize(Path targetPath) { } } - public Double[] getMinMax(Path targetPath, String column) { + public MinMax getMinMax(Path targetPath, String column) { int columnIndex = -1; Double min = Double.MAX_VALUE; Double max = Double.MIN_VALUE; @@ -92,7 +94,7 @@ public Double[] getMinMax(Path targetPath, String column) { "Failed to get min max of file: " + targetPath.toString(), targetPath.toString(), e); } - return new Double[] {min, max}; + return new MinMax(min, max); } public String getLast(Path targetPath, int columnIndex) { diff --git a/backend/src/main/java/com/mcmasterbaja/services/FileMetadataService.java b/backend/src/main/java/com/mcmasterbaja/services/FileMetadataService.java index 5772119e..cd0aa312 100644 --- a/backend/src/main/java/com/mcmasterbaja/services/FileMetadataService.java +++ b/backend/src/main/java/com/mcmasterbaja/services/FileMetadataService.java @@ -3,6 +3,8 @@ import java.nio.file.Path; import java.time.LocalDateTime; +import com.mcmasterbaja.model.MinMax; + public interface FileMetadataService { /** @@ -26,9 +28,9 @@ public interface FileMetadataService { * * @param targetPath The Path of the file to read. * @param column The column to analyze. - * @return A double[] containing the minimum and maximum values. + * @return A MinMax object containing the minimum and maximum values. */ - Double[] getMinMax(Path targetPath, String column); + MinMax getMinMax(Path targetPath, String column); /** * Gets the last value of the column in the file. diff --git a/front-end/src/components/modal/download/DownloadModal.jsx b/front-end/src/components/modal/download/DownloadModal.jsx index 866b0458..ffb3459c 100644 --- a/front-end/src/components/modal/download/DownloadModal.jsx +++ b/front-end/src/components/modal/download/DownloadModal.jsx @@ -4,7 +4,7 @@ import './DownloadModal.css'; import React, { useState, useRef, useEffect } from 'react'; import FileStorage from '../FileStorage/FileStorage'; import JSZip from 'jszip'; -import { ApiUtil } from '@lib/apiUtils.js'; +import { ApiUtil } from '@lib/apiUtils'; export const DownloadModal = ({ setModal }) => { const [selectedFiles, setSelectedFiles] = useState([]); // the files that the user has selected from the file menu diff --git a/front-end/src/components/modal/upload/UploadModal.jsx b/front-end/src/components/modal/upload/UploadModal.jsx index 6c023b0a..42073b7b 100644 --- a/front-end/src/components/modal/upload/UploadModal.jsx +++ b/front-end/src/components/modal/upload/UploadModal.jsx @@ -4,7 +4,7 @@ import '@styles/modalStyles.css'; import './UploadModal.css'; import { useForm } from 'react-hook-form'; import React, { useState, useRef } from 'react'; -import { ApiUtil } from '@lib/apiUtils.js'; +import { ApiUtil } from '@lib/apiUtils'; import loadingImg from '@assets/loading.gif'; export const UploadModal = ({ setModal, setSuccessMessage}) => { diff --git a/front-end/src/components/views/Chart/Chart.jsx b/front-end/src/components/views/Chart/Chart.jsx index bff5783f..2da3fb4e 100644 --- a/front-end/src/components/views/Chart/Chart.jsx +++ b/front-end/src/components/views/Chart/Chart.jsx @@ -1,7 +1,7 @@ import './Chart.css'; import { defaultChartOptions, getChartConfig, movePlotLineX, movePlotLines } from '@lib/chartOptions.js'; import { getSeriesData, getTimestamps, LIVE_DATA_INTERVAL, validateChartInformation } from '@lib/chartUtils.js'; -import { ApiUtil } from '@lib/apiUtils.js'; +import { ApiUtil } from '@lib/apiUtils'; import React, { useState, useEffect, useRef, useCallback } from 'react'; import Highcharts from 'highcharts'; import HighchartsReact from 'highcharts-react-official'; diff --git a/front-end/src/lib/apiUtils.js b/front-end/src/lib/apiUtils.js deleted file mode 100644 index 8914004e..00000000 --- a/front-end/src/lib/apiUtils.js +++ /dev/null @@ -1,166 +0,0 @@ - -export const ApiUtil = { - - /** - * @description Sends a GET request to the server to fetch a specific file. - * @param {string} fileKey - The unique identifier of the file. - * @returns {Promise} A promise that resolves to the server's response. - */ - getFile: async (fileKey) => { - fileKey = encodeURIComponent(fileKey); - const response = await fetch(`http://${window.location.hostname}:8080/files/${fileKey}`); - if (!response.ok) throw Error(response.statusText); - return response; - }, - - /** - * @description Fetches a list of files from the server. - * Each file in the list is represented as an object with the following properties: - * - key: A that represents the unique identifier of the file. - * - fileHeaders: An array of strings that represents the headers of the file. - * - size: A long that represents the size of the file. - * @returns {Promise} A promise that resolves to an array of file objects. - */ - getFiles: async () => { - const response = await fetch(`http://${window.location.hostname}:8080/files`); - if (!response.ok) throw Error(response.statusText); - - return response; - }, - - /** - * @description Sends a GET request to the server to fetch fileInformation about a specific folder. - * Each file in the list is represented as an object with the following properties: - * - key: A that represents the unique identifier of the file. This will be relative to the folder provided. - * - fileHeaders: An array of strings that represents the headers of the file. - * - size: A long that represents the size of the file. - * - * @param {string} folderKey - The unique identifier of the folder. - * @returns {Promise} A promise that resolves to the server's response. - */ - getFolder: async (folderKey) => { - const response = await fetch(`http://${window.location.hostname}:8080/files/information/folder/${folderKey}`); - if (!response.ok) throw Error(response.statusText); - return response; - }, - - /** - * @description Sends a GET request to the server to fetch the timespans of a folder. - * @param {string} folderKey - The unique identifier of the folder. - * @returns {Promise} A promise that resolves to the server's response. - */ - getTimespans: async (folderKey) => { - const response = await fetch(`http://${window.location.hostname}:8080//files/timespan/folder/${folderKey}`); - if (!response.ok) throw Error(response.statusText); - return response; - }, - - /** - * @description Sends a GET request to the server to analyze and return files. - * @param {string} inputFiles - The input files. - * @param {string} inputColumns - The input columns. - * @param {string} outputFiles - The output files. - * @param {Enum} type - The analyzer type. - * @param {string} analyzerOptions - The analyzer options. - * @param {Boolean} live - The live options. - * @returns {Promise} A promise that resolves to the server's response. - */ - analyzeFiles: async (inputFiles, inputColumns, outputFiles, type, analyzerOptions, live) => { - try { - const params = new URLSearchParams(); - const parameters = { inputFiles, inputColumns, outputFiles, type, analyzerOptions, live }; - - Object.entries(parameters).forEach(([key, value]) => { - if (value && value.length !== 0) { - if (Array.isArray(value)) { - value.forEach((val) => { - params.append(key, val); - }); - } else { - params.append(key, value); - } - } - }); - - const response = await fetch(`http://${window.location.hostname}:8080/analyze?` + params.toString(), { - method: 'POST' - }); - - if (!response.ok) { - alert(`An error has occured!\nCode: ${response.status}\n${await response.text()}`); - throw Error(response.statusText); - } - return response; - } catch (error) { - console.log('Analyzer Error:', error); - } - }, - - /** - * @description Fetches the min and max values of a specific column in a file. - * @param {string} filename - The name of the file. - * @param {string} header - The name of the column. - * @returns {Promise} A promise that resolves to the server's response. - */ - getMinMax: async (filename, header) => { - const url = `http://${window.location.hostname}:8080/minMax/${encodeURIComponent(filename)}?column=${header}`; - const response = await fetch(url); - - if (!response.ok) { - alert(`An error has occured!\nCode: ${response.status}\n${await response.text()}`); - throw Error(response.statusText); - } - return response; - }, - - /** - * @description Sends a DELETE request to the server to delete all files. - * @returns {Promise} A promise that resolves to the server's response. - */ - deleteAllFiles: async () => { - const response = await fetch(`http://${window.location.hostname}:8080/delete/all`, { - // method: "DELETE" - }); - - if (!response.ok) throw Error(response.statusText); - return response; - }, - - /** - * @description Sends a POST request to the server to start or stop live data. - * @param {string} port - The port to be used for live data. - * @returns {Promise} A promise that resolves to the server's response. - */ - toggleLiveData: async (port) => { - const formData = new FormData(); - formData.append('port', port); - - const response = await fetch(`http://${window.location.hostname}:8080/togglelive`, { - method: 'PATCH', - //body: formData, - }); - - if (!response.ok) throw Error(response.statusText); - return response; - }, - - /** - * @description Sends a POST request to the server to upload a file. - * @param {FormData} file - The file to be uploaded. - * @returns {Promise} A promise that resolves to the server's response. - */ - uploadFile: async (file) => { - const formData = new FormData(); - formData.set('fileName', file.name); - formData.set('fileData', file); - - const response = await fetch(`http://${window.location.hostname}:8080/upload/file`, { - method: 'POST', - body: formData, - }); - - if (!response.ok) throw Error(response.statusText); - return response; - }, - -}; \ No newline at end of file diff --git a/front-end/src/lib/apiUtils.ts b/front-end/src/lib/apiUtils.ts new file mode 100644 index 00000000..edc96654 --- /dev/null +++ b/front-end/src/lib/apiUtils.ts @@ -0,0 +1,173 @@ +// Some defined API types + +interface FileInformation { + key: string; + fileHeaders: string[]; + size: number; +} + +interface FileTimespan { + key: string; + start: Date; + end: Date; +} + +interface MinMax { + min: number; + max: number; +} + +enum AnalyzerType { + ACCEL_CURVE = 'ACCEL_CURVE', + AVERAGE = 'AVERAGE', + CUBIC = 'CUBIC', + LINEAR_INTERPOLATE = 'LINEAR_INTERPOLATE', + LINEAR_MULTIPLY = 'LINEAR_MULTIPLY', + INTERPOLATER_PRO = 'INTERPOLATER_PRO', + RDP_COMPRESSION = 'RDP_COMPRESSION', + ROLL_AVG = 'ROLL_AVG', + SGOLAY = 'SGOLAY', + SPLIT = 'SPLIT' +} + +export const ApiUtil = { + + /** + * Sends a GET request to the server to fetch a specific file. + * @param {string} fileKey - The unique identifier of the file. + * @returns {Promise} A promise that resolves to the fetched file in the form of a Blob. + */ + getFile: async (fileKey: string): Promise => { + fileKey = encodeURIComponent(fileKey); + const response = await fetch(`http://${window.location.hostname}:8080/files/${fileKey}`); + if (!response.ok) throw Error(response.statusText); + return response.blob(); + }, + + /** + * @description Fetches a list of files from the server. + * @returns {Promise} A promise that resolves to an array of file names. + */ + getFiles: async (): Promise => { + const response = await fetch(`http://${window.location.hostname}:8080/files`); + if (!response.ok) throw Error(response.statusText); + + return response.json(); + }, + + /** + * @description Sends a GET request to the server to fetch fileInformation about a specific folder. + * @param {string} folderKey - The unique identifier of the folder. + * @returns {Promise} A promise that resolves to an array of fileInformation objects. + */ + getFolder: async (folderKey: string): Promise => { + const response = await fetch(`http://${window.location.hostname}:8080/files/information/folder/${folderKey}`); + if (!response.ok) throw Error(response.statusText); + return response.json(); + }, + + /** + * @description Sends a GET request to the server to fetch the timespans of a folder. + * @param {string} folderKey - The unique identifier of the folder. + * @returns {Promise} A promise that resolves to an array of FileTimespan objects. + */ + getTimespans: async (folderKey: string): Promise => { + const response = await fetch(`http://${window.location.hostname}:8080//files/timespan/folder/${folderKey}`); + if (!response.ok) throw Error(response.statusText); + return response.json(); + }, + + // TODO: Test this method + /** + * @description Sends a GET request to the server to analyze and return files. + */ + analyzeFiles: async ( + inputFiles: string[], + inputColumns: string[], + outputFiles: string[], + type: AnalyzerType, + analyzerOptions: string[], // This one is weird as its dependent on which analyzer is run + live: boolean + ): Promise => { + const params = new URLSearchParams(); + + params.append('inputFiles', inputFiles.join(',')); + params.append('inputColumns', inputColumns.join(',')); + params.append('outputFiles', outputFiles.join(',')); + params.append('type', type); + params.append('analyzerOptions', analyzerOptions.join(',')); + params.append('live', live.toString()); + + const response = await fetch(`http://${window.location.hostname}:8080/analyze?` + params.toString(), { + method: 'POST' + }); + + if (!response.ok) { + alert(`An error has occured!\nCode: ${response.status}\n${await response.text()}`); + throw Error(response.statusText); + } + return response; + }, + + /** + * @description Fetches the min and max values of a specific column in a file. + * @returns {Promise} A promise that resolves to an object containing the min and max values of the column. + */ + getMinMax: async (filename: string, header: string): Promise => { + const url = `http://${window.location.hostname}:8080/minMax/${encodeURIComponent(filename)}?column=${header}`; + const response = await fetch(url); + + if (!response.ok) { + alert(`An error has occured!\nCode: ${response.status}\n${await response.text()}`); + throw Error(response.statusText); + } + return response.json(); + }, + + /** + * @description Sends a DELETE request to the server to delete all files. + */ + deleteAllFiles: async (): Promise => { + const response = await fetch(`http://${window.location.hostname}:8080/delete/all`, { + // TODO: Why isn't this included? + // method: "DELETE" + }); + + if (!response.ok) throw Error(response.statusText); + }, + + /** + * @description Sends a POST request to the server to start or stop live data. + * @param {string} port - The port to be used for live data. + * @returns {Promise} A promise that resolves to the updated state of live data. + */ + toggleLiveData: async (port: string): Promise => { + const formData = new FormData(); + formData.append('port', port); + + // TODO: this method should use the form data + const response = await fetch(`http://${window.location.hostname}:8080/togglelive`, { + method: 'PATCH', + //body: formData, + }); + + if (!response.ok) throw Error(response.statusText); + return response.json(); + }, + + /** + * @description Sends a POST request to the server to upload a file. + */ + uploadFile: async (file: File): Promise => { + const formData = new FormData(); + formData.set('fileName', file.name); + formData.set('fileData', file); + + const response = await fetch(`http://${window.location.hostname}:8080/upload/file`, { + method: 'POST', + body: formData, + }); + + if (!response.ok) throw Error(response.statusText); + }, +}; \ No newline at end of file diff --git a/front-end/tsconfig.node.json b/front-end/tsconfig.node.json index aac0ce5a..a32256f6 100644 --- a/front-end/tsconfig.node.json +++ b/front-end/tsconfig.node.json @@ -1,3 +1,4 @@ +// TODO: Figure out if we can remove this file { "compilerOptions": { "composite": true, From 2c7f9620328ebe1ce4f9bb824967d18ded2dde9a Mon Sep 17 00:00:00 2001 From: Kai Arseneau Date: Wed, 2 Oct 2024 18:52:57 -0400 Subject: [PATCH 04/19] Chart V1 refactor --- front-end/package-lock.json | 8 +-- front-end/package.json | 2 +- .../CreateGraphModal/CreateGraphModal.jsx | 3 - .../components/modal/upload/UploadModal.jsx | 21 ++---- .../views/Chart/{Chart.jsx => Chart.tsx} | 69 ++++++++++++------- front-end/src/lib/apiUtils.ts | 8 +-- front-end/src/lib/chartOptions.js | 2 +- .../src/lib/{chartUtils.js => chartUtils.ts} | 54 +++++++++++---- 8 files changed, 101 insertions(+), 66 deletions(-) rename front-end/src/components/views/Chart/{Chart.jsx => Chart.tsx} (78%) rename front-end/src/lib/{chartUtils.js => chartUtils.ts} (76%) diff --git a/front-end/package-lock.json b/front-end/package-lock.json index 4fa61caa..0b60734c 100644 --- a/front-end/package-lock.json +++ b/front-end/package-lock.json @@ -15,7 +15,7 @@ "eslint": "^8.57.0", "eslint-plugin-react": "^7.34.1", "font-awesome": "^4.7.0", - "highcharts": "^11.2.0", + "highcharts": "^11.4.8", "highcharts-multicolor-series": "^2.4.1", "highcharts-react-official": "^3.2.1", "jquery": "^3.7.1", @@ -4782,9 +4782,9 @@ } }, "node_modules/highcharts": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/highcharts/-/highcharts-11.2.0.tgz", - "integrity": "sha512-9i650YK7ZBA1Mgtr3avMkLVCAI45RQvYnwi+eHsdFSaBGuQN6BHoa4j4lMkSJLv0V4LISTK1z7J7G82Lzd7zwg==" + "version": "11.4.8", + "resolved": "https://registry.npmjs.org/highcharts/-/highcharts-11.4.8.tgz", + "integrity": "sha512-5Tke9LuzZszC4osaFisxLIcw7xgNGz4Sy3Jc9pRMV+ydm6sYqsPYdU8ELOgpzGNrbrRNDRBtveoR5xS3SzneEA==" }, "node_modules/highcharts-multicolor-series": { "version": "2.4.1", diff --git a/front-end/package.json b/front-end/package.json index 902e8bf0..57e10bdb 100644 --- a/front-end/package.json +++ b/front-end/package.json @@ -10,7 +10,7 @@ "eslint": "^8.57.0", "eslint-plugin-react": "^7.34.1", "font-awesome": "^4.7.0", - "highcharts": "^11.2.0", + "highcharts": "^11.4.8", "highcharts-multicolor-series": "^2.4.1", "highcharts-react-official": "^3.2.1", "jquery": "^3.7.1", diff --git a/front-end/src/components/modal/create/CreateGraphModal/CreateGraphModal.jsx b/front-end/src/components/modal/create/CreateGraphModal/CreateGraphModal.jsx index 2d0015f8..294c933b 100644 --- a/front-end/src/components/modal/create/CreateGraphModal/CreateGraphModal.jsx +++ b/front-end/src/components/modal/create/CreateGraphModal/CreateGraphModal.jsx @@ -42,17 +42,14 @@ export const CreateGraphModal = ({ useEffect(() => { // Fetch data when the component mounts ApiUtil.getFolder('csv') - .then((response) => response.json()) .then((data) => { setFiles(data); }); ApiUtil.getTimespans('mp4') - .then((response) => response.json()) .then((data) => { setvideoTimespans(data); }); ApiUtil.getTimespans('csv') - .then((response) => response.json()) .then((data) => { setfileTimespans(data); }); diff --git a/front-end/src/components/modal/upload/UploadModal.jsx b/front-end/src/components/modal/upload/UploadModal.jsx index 42073b7b..736e8d92 100644 --- a/front-end/src/components/modal/upload/UploadModal.jsx +++ b/front-end/src/components/modal/upload/UploadModal.jsx @@ -50,22 +50,13 @@ export const UploadModal = ({ setModal, setSuccessMessage}) => { //start loading setLoading(true); - await new Promise((resolve, reject) => { + // TODO: This does not handle uploading multiple, just ensures the last finishes before setting loading to false + await new Promise((resolve) => { for (let i = 0; i < fileLists.length; i++) { - ApiUtil.uploadFile(fileLists[i]).then((res) => { - res.text().then(text => { - if(res.status !== 200) { - alert(JSON.stringify(`${text}, status: ${res.status}`)); - } - if (i===fileLists.length-1) { - resolve(); - } - }); - }).catch(e => { - alert(e); - reject(e); - }); - + ApiUtil.uploadFile(fileLists[i]); + if (i===fileLists.length-1) { + resolve(); + } } }); diff --git a/front-end/src/components/views/Chart/Chart.jsx b/front-end/src/components/views/Chart/Chart.tsx similarity index 78% rename from front-end/src/components/views/Chart/Chart.jsx rename to front-end/src/components/views/Chart/Chart.tsx index 2da3fb4e..807624c2 100644 --- a/front-end/src/components/views/Chart/Chart.jsx +++ b/front-end/src/components/views/Chart/Chart.tsx @@ -1,43 +1,52 @@ import './Chart.css'; -import { defaultChartOptions, getChartConfig, movePlotLineX, movePlotLines } from '@lib/chartOptions.js'; -import { getSeriesData, getTimestamps, LIVE_DATA_INTERVAL, validateChartInformation } from '@lib/chartUtils.js'; +import { defaultChartOptions, getChartConfig, movePlotLineX, movePlotLines } from '@lib/chartOptions'; +import { getSeriesData, getTimestamps, LIVE_DATA_INTERVAL, validateChartInformation } from '@lib/chartUtils'; import { ApiUtil } from '@lib/apiUtils'; -import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { useState, useEffect, useRef, useCallback } from 'react'; import Highcharts from 'highcharts'; import HighchartsReact from 'highcharts-react-official'; import Boost from 'highcharts/modules/boost'; import HighchartsColorAxis from 'highcharts/modules/coloraxis'; -import { computeOffsets, getFileTimestamp, getPointIndex, binarySearchClosest} from '@lib/videoUtils.js'; +import { computeOffsets, getFileTimestamp, getPointIndex, binarySearchClosest} from '@lib/videoUtils'; import { useResizeDetector } from 'react-resize-detector'; import loadingImg from '@assets/loading.gif'; -// TODO: Fix this import (Why is it different?) -// import 'highcharts-multicolor-series'; - +import { FileTimespan, MinMax } from '@lib/apiUtils'; +import { seriesData } from '@lib/chartUtils'; +import { Chart as ChartType } from 'highcharts'; +// TODO: Fix this import (Why is it different?) . Currently no ECMA module Womp Womp // eslint-disable-next-line no-undef -require('highcharts-multicolor-series')(Highcharts); // Keeping this old import until we test the new one +require('highcharts-multicolor-series')(Highcharts); HighchartsColorAxis(Highcharts); Boost(Highcharts); -const Chart = ({ chartInformation, video, videoTimestamp }) => { + +interface ChartProps { + chartInformation: any; // TODO: Define this + video: FileTimespan; + videoTimestamp: number; +} + +const Chart = ({ chartInformation, video, videoTimestamp }: ChartProps) => { const [chartOptions, setChartOptions] = useState(defaultChartOptions); const [loading, setLoading] = useState(false); - const [parsedData, setParsedData] = useState([]); - const [fileNames, setFileNames] = useState([]); + const [parsedData, setParsedData] = useState([]); + const [fileNames, setFileNames] = useState([]); const [offsets, setOffsets] = useState([]); - const [timestamps, setTimestamps] = useState([]); + const [timestamps, setTimestamps] = useState([]); const [lineX, setLineX] = useState(0); const [linePoint, setLinePoint] = useState({x: 0, y: 0}); - const [valueLines, setValueLines] = useState([]); - let minMax = useRef([0, 0]); + const [valueLines, setValueLines] = useState([]); + let minMax = useRef({min: 0, max: 0}); + // TODO: This whole function is a bit of a mess, should be refactored // Fetch the data from the server and format it for the chart const getFileFormat = useCallback(async () => { // Runs through all the series and fetches the data, then updates the graph // This also prevents liveData from adding more data as a separate series - var data = []; - const tempTimestamps = []; + var data: seriesData[] = []; + const tempTimestamps: number[][] = []; for (var i = 0; i < chartInformation.files.length; i++) { // Create a list of all files in order (formatting for backend) let files = chartInformation.files[i].columns.map(column => column.filename); @@ -52,30 +61,34 @@ const Chart = ({ chartInformation, video, videoTimestamp }) => { chartInformation.live ); - const filename = response.headers.get('content-disposition').split('filename=')[1].slice(1, -1); + //TODO: This should be removed and no longer needed I reckon, in place for a better way to return a file / filename + const contentDisposition = response.headers.get('content-disposition'); + if (!contentDisposition) throw new Error('Content-Disposition header is missing'); + const filename = contentDisposition.split('filename=')[1].slice(1, -1); + setFileNames(prevState => [...prevState, filename]); const text = await response.text(); + minMax.current = await ApiUtil.getMinMax(filename, inputColumns[0].header); const seriesData = await getSeriesData( text, - filename, inputColumns, - minMax, + minMax.current, //TODO THIS MIGHT NOT UPDATE FAST ENOUGH chartInformation.type, chartInformation.hasTimestampX, chartInformation.hasGPSTime ); data.push(seriesData); + // TODO: Fix this section for each case tempTimestamps.push( - chartInformation.hasTimestampX ? seriesData.map(item => item[0]) : await getTimestamps(text) + chartInformation.hasTimestampX ? seriesData.map(item => item[0]) as number[] : await getTimestamps(text) ); } setParsedData(data); setTimestamps(tempTimestamps); - }, [chartInformation, setFileNames, setParsedData, setTimestamps]); // Whenever chartInformation is updated (which happens when submit button is pressed), fetch the neccesary data @@ -119,7 +132,7 @@ const Chart = ({ chartInformation, video, videoTimestamp }) => { return () => clearInterval(intervalId); }, [chartInformation, getFileFormat]); - const chartRef = useRef(null); + const chartRef = useRef(null); const { width, height, ref } = useResizeDetector({ onResize: () => { if (chartRef.current) { @@ -158,12 +171,17 @@ const Chart = ({ chartInformation, video, videoTimestamp }) => { // Calculates and updates which value is closest to the video timestamp for each series const lineXUpdate = (videoTimestamp) => { - let fileTimestamp = undefined; + if (chartRef.current === null || chartRef.current.series.length === 0) return; + + // TODO: Find a better base value for fileTimestamp + let fileTimestamp = -Infinity; + const visibleSeries = chartRef.current.series.filter(series => series.visible); if (visibleSeries.length === 0) return; // Gets the first file timestamp that is not undefined visibleSeries.some(series => { + if (chartRef.current === null) return; const seriesIndex = chartRef.current.series.indexOf(series); fileTimestamp = getFileTimestamp(videoTimestamp, offsets[seriesIndex], timestamps[seriesIndex]); return fileTimestamp !== undefined; @@ -194,6 +212,8 @@ const Chart = ({ chartInformation, video, videoTimestamp }) => { // Calculates and updates which point is closest to the video timestamp for each series const linePointUpdate = (videoTimestamp) => { // Finds the matching point index for the first visible series using the video timestamp + if (chartRef.current === null || chartRef.current.series.length === 0) return; + //TODO: Update this null check to be inline and return the right case const visibleSeries = chartRef.current.series.filter(series => series.visible); if (visibleSeries.length === 0) return; const firstVisibleSeries = visibleSeries[0]; @@ -216,6 +236,7 @@ const Chart = ({ chartInformation, video, videoTimestamp }) => { x: firstVisibleSeries.xData[pointIndex], y: firstVisibleSeries.yData[pointIndex] }, ...visibleSeries.slice(1).map(series => { + if (chartRef.current === null || chartRef.current.series.length === 0) throw new Error('Chart is not initialized'); const seriesIndex = chartRef.current.series.indexOf(series); const pointIndex = getPointIndex( series, @@ -230,7 +251,7 @@ const Chart = ({ chartInformation, video, videoTimestamp }) => { ]; // Updates the value box with the found values - const tempValueLines = []; + const tempValueLines: string[] = []; chartRef.current.series.forEach(series => { const value = values.find(value => value.name === series.name); if (value === undefined) return; diff --git a/front-end/src/lib/apiUtils.ts b/front-end/src/lib/apiUtils.ts index edc96654..fa15e6d5 100644 --- a/front-end/src/lib/apiUtils.ts +++ b/front-end/src/lib/apiUtils.ts @@ -1,23 +1,23 @@ // Some defined API types -interface FileInformation { +export interface FileInformation { key: string; fileHeaders: string[]; size: number; } -interface FileTimespan { +export interface FileTimespan { key: string; start: Date; end: Date; } -interface MinMax { +export interface MinMax { min: number; max: number; } -enum AnalyzerType { +export enum AnalyzerType { ACCEL_CURVE = 'ACCEL_CURVE', AVERAGE = 'AVERAGE', CUBIC = 'CUBIC', diff --git a/front-end/src/lib/chartOptions.js b/front-end/src/lib/chartOptions.js index 51be31ff..ead6cc7b 100644 --- a/front-end/src/lib/chartOptions.js +++ b/front-end/src/lib/chartOptions.js @@ -79,7 +79,7 @@ const getDefaultChartConfig = (chartInformation, parsedData, fileNames) => { data: data, colour: colours[index], opacity: 1, - colorAxis: false, + colorAxis: false, findNearestPointBy: 'x', boostThreshold: 1, marker: { enabled: false } diff --git a/front-end/src/lib/chartUtils.js b/front-end/src/lib/chartUtils.ts similarity index 76% rename from front-end/src/lib/chartUtils.js rename to front-end/src/lib/chartUtils.ts index 1a9f354c..9c9278df 100644 --- a/front-end/src/lib/chartUtils.js +++ b/front-end/src/lib/chartUtils.ts @@ -1,10 +1,30 @@ -import { ApiUtil } from './apiUtils'; +import { MinMax } from './apiUtils'; + +//TODO: Move this definition elsewhere +interface column { + header: string; + filename: string; + timespan: { + start: Date; + end: Date; + } +} + // Constants for colour chart const HUE_MIN = 150; const HUE_MAX = 0; export const LIVE_DATA_INTERVAL = 300; +interface colourSeriesData { + x: number; + y: number; + colorValue: number; + segmentColor: string; +} + +export type seriesData = colourSeriesData[] | number[][]; + /** * @description Fetches the data from the server and formats it for the chart. * @param {Object} response - The file response from the server. @@ -16,7 +36,14 @@ export const LIVE_DATA_INTERVAL = 300; * @param {boolean} hasGPSTime - True if all the series have GPS timespans, false otherwise. * @returns {Promise>} A promise that resolves to an array of data objects. */ -export const getSeriesData = async (text, filename, columns, minMax, chartType, hasTimestampX, hasGPSTime) => { +export const getSeriesData = async ( + text: string, + columns: column[], + minMax: MinMax, + chartType: any, // TODO: Update this with highcharts options / enum + hasTimestampX: boolean, + hasGPSTime: boolean +): Promise => { let headers = text.trim().split('\n')[0].split(','); headers[headers.length - 1] = headers[headers.length - 1].replace('\r', ''); @@ -38,15 +65,11 @@ export const getSeriesData = async (text, filename, columns, minMax, chartType, // If colour, return the data in object format to allow for colouring // Make a request to get the maximum and minimum values of the colour value // TODO: Seems to break when giving it a file with 3+ colomns, worth looking into - const minMaxResponse = await ApiUtil.getMinMax(filename, columns[columns.length -1].header); - - let [minval, maxval] = JSON.parse(await minMaxResponse.text()); - minMax.current = [minval, maxval]; return lines.map((line) => { let val = parseFloat(line[headerIndices.colour]); - let hue = HUE_MIN + (HUE_MAX - HUE_MIN) * (val - minval) / (maxval - minval); + let hue = HUE_MIN + (HUE_MAX - HUE_MIN) * (val - minMax.min) / (minMax.max - minMax.min); return { x: parseFloat(line[headerIndices.x]), @@ -59,24 +82,27 @@ export const getSeriesData = async (text, filename, columns, minMax, chartType, // Calculates the offset required to convert the x values to unix timestamps // Adding the timestampOffset results in the x value being a the start time unix millis + millis since first timestamp -const getTimestampOffset = (columns, lines, headerIndices) => { +const getTimestampOffset = (columns: column[], lines: string[][], headerIndices: headersIndex): number => { // Offset is the start time in unix millis minus the first timestamp in the file return new Date(columns[headerIndices.x].timespan.start + 'Z').getTime() - parseFloat(lines[0][headerIndices.x]); }; -export const getTimestamps = async (text) => { +export const getTimestamps = async (text: string) => { const timestampHeaderIndex = text.trim().split('\n')[0].split(',').indexOf('Timestamp (ms)'); return text.trim().split('\n').slice(1).map((line) => parseFloat(line.split(',')[timestampHeaderIndex])); }; +interface headersIndex { + x: number; + y: number; + colour: number; +} /** * @description Matches headers to columns to get the indices of the columns in the headers array. - * @param {string[]} headers - An array of headers. - * @param {string[]} columns - An array of columns. * @returns {Object} An object with the indices of the columns in the headers array. The keys are 'x', 'y', and 'colour' */ -const getHeadersIndex = (headers, columns) => { - let h = {}; +const getHeadersIndex = (headers: string[], columns: column[]): headersIndex => { + let h: headersIndex = { x: -1, y: -1, colour: -1 }; for (let i = 0; i < columns.length; i++) { for (let j = 0; j < headers.length; j++) { if (columns[i].header === headers[j].trim()) { @@ -97,7 +123,7 @@ const getHeadersIndex = (headers, columns) => { * @param {Object} chartInformation - The chart information object. * @returns {Boolean} True if the chart information is full, false otherwise. */ -export const validateChartInformation = (chartInformation) => { +export const validateChartInformation = (chartInformation: any): boolean => { if (!chartInformation) { return false; } From 3ff3990dfc39a9f2cf7196e95e42f279afa317d7 Mon Sep 17 00:00:00 2001 From: Kai Arseneau Date: Wed, 2 Oct 2024 19:06:01 -0400 Subject: [PATCH 05/19] Fix graph request --- front-end/src/lib/apiUtils.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/front-end/src/lib/apiUtils.ts b/front-end/src/lib/apiUtils.ts index fa15e6d5..d72f7182 100644 --- a/front-end/src/lib/apiUtils.ts +++ b/front-end/src/lib/apiUtils.ts @@ -84,19 +84,21 @@ export const ApiUtil = { analyzeFiles: async ( inputFiles: string[], inputColumns: string[], - outputFiles: string[], - type: AnalyzerType, + outputFiles: string[] | null, + type: AnalyzerType | null, analyzerOptions: string[], // This one is weird as its dependent on which analyzer is run live: boolean ): Promise => { const params = new URLSearchParams(); - params.append('inputFiles', inputFiles.join(',')); - params.append('inputColumns', inputColumns.join(',')); - params.append('outputFiles', outputFiles.join(',')); - params.append('type', type); - params.append('analyzerOptions', analyzerOptions.join(',')); - params.append('live', live.toString()); + console.log(inputFiles, inputColumns, outputFiles, type, analyzerOptions, live); + + inputFiles.map(file => params.append('inputFiles', file)); + inputColumns.map(column => params.append('inputColumns', column)); + outputFiles?.map(file => params.append('outputFiles', file)); + type && params.append('type', type); + analyzerOptions.map(option => params.append('analyzerOptions', option)); + live && params.append('live', live.toString()); const response = await fetch(`http://${window.location.hostname}:8080/analyze?` + params.toString(), { method: 'POST' From 433c9e71d9696b6f132d38dd262ebb31f4526899 Mon Sep 17 00:00:00 2001 From: Kai Arseneau Date: Thu, 3 Oct 2024 19:26:32 -0400 Subject: [PATCH 06/19] Defined chartInformation and chartOptions --- front-end/src/components/{App.jsx => App.tsx} | 55 +++++++++---------- .../AnalyzersAndSeries/AnalyzersAndSeries.jsx | 2 +- .../src/components/views/Chart/Chart.tsx | 5 +- .../lib/{chartOptions.js => chartOptions.ts} | 35 +++++++----- 4 files changed, 50 insertions(+), 47 deletions(-) rename front-end/src/components/{App.jsx => App.tsx} (74%) rename front-end/src/lib/{chartOptions.js => chartOptions.ts} (84%) diff --git a/front-end/src/components/App.jsx b/front-end/src/components/App.tsx similarity index 74% rename from front-end/src/components/App.jsx rename to front-end/src/components/App.tsx index 82ec1a74..27028786 100644 --- a/front-end/src/components/App.jsx +++ b/front-end/src/components/App.tsx @@ -3,7 +3,7 @@ import { CreateGraphModal } from './modal/create/CreateGraphModal/CreateGraphMod import { UploadModal } from './modal/upload/UploadModal'; import { HelpModal } from './modal/help/helpModal'; import { DownloadModal } from './modal/download/DownloadModal'; -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; import Topbar from './Topbar/Topbar'; import Views from './views/Views/Views'; @@ -11,6 +11,28 @@ import $ from 'jquery'; import { MAX_VIEWS } from './views/viewsConfig'; import Chart from './views/Chart/Chart'; import MapChart from './map/MapChart/MapChart'; +import { AnalyzerType } from '@lib/apiUtils' + +export interface chartInformation { + files: { + columns: { + header: string, + filename: string, + timespan: { + start: Date, + end: Date + } + }[], + analyze: { + type: AnalyzerType, + analyzerValues: string[] + } + }[], + live: boolean, + type: string //TODO: UPDATE TO ENUM + hasGPSTime: boolean + hasTimestampX: boolean +} const App = () => { @@ -20,33 +42,6 @@ const App = () => { const [videoTimestamp, setVideoTimestamp] = useState(0); const [video, setVideo] = useState({ key: '', start: '', end: '' }); - // sample format for chartInformation: - // { - // files: - // [ - // { - // columns: [ - // {header:"Timestampt", filename:"PRIM_RPM.csv", - // timespan: {start: "18-00-23F--0:00:00", end: "18-00-23F--0:00:00"}}, - // {header:"RPM", filename:"PRIM_RPM.csv", - // timespan: {start: "18-00-23F--0:00:00", end: "18-00-23F--0:00:00"}, - // ], - // analysis: "none" - // }, - // { - // columns: [ - // {header:"RPM", filename:"SEC_RPM.csv", - // timespan: {start: "18-00-23F--0:00:00", end: "18-00-23F--0:00:00"}}, - // {header:"Timestampt", filename:"SEC_RPM.csv", - // timespan: {start: "18-00-23F--0:00:00", end: "18-00-23F--0:00:00"}} - // ], - // analysis: "rollAvg" - // } - // ], - // live: false, - // type: "line" - // } - // State for holding the information for each view const [viewInformation, setViewInformation] = useState( Array.from({ length: MAX_VIEWS }, () => ({ @@ -56,7 +51,7 @@ const App = () => { ); // This is an object so that other updates to it will always call the useEffect, even if the message is the same - const [successMessage, setSuccessMessage] = useState({}); + const [successMessage, setSuccessMessage] = useState(''); const [buttonID, setButtonID] = useState(null); // Catches when success message is updated and displays it after removing old one @@ -72,7 +67,7 @@ const App = () => {
-
{successMessage.message}
+
{successMessage}
{modal === 'Create' ? { return seriesInfo.some((series) => { const isSameColumns = JSON.stringify(series.columns) === JSON.stringify(newSeries.columns); - const isSameAnalyzer = series.analyze.analysis === newSeries.analyze.analysis; + const isSameAnalyzer = series.analyze.type === newSeries.analyze.type; return isSameColumns && isSameAnalyzer; }); }; diff --git a/front-end/src/components/views/Chart/Chart.tsx b/front-end/src/components/views/Chart/Chart.tsx index 807624c2..a4c5a0b8 100644 --- a/front-end/src/components/views/Chart/Chart.tsx +++ b/front-end/src/components/views/Chart/Chart.tsx @@ -13,6 +13,7 @@ import loadingImg from '@assets/loading.gif'; import { FileTimespan, MinMax } from '@lib/apiUtils'; import { seriesData } from '@lib/chartUtils'; import { Chart as ChartType } from 'highcharts'; +import { chartInformation } from '@components/App'; // TODO: Fix this import (Why is it different?) . Currently no ECMA module Womp Womp // eslint-disable-next-line no-undef require('highcharts-multicolor-series')(Highcharts); @@ -22,7 +23,7 @@ Boost(Highcharts); interface ChartProps { - chartInformation: any; // TODO: Define this + chartInformation: chartInformation; video: FileTimespan; videoTimestamp: number; } @@ -56,7 +57,7 @@ const Chart = ({ chartInformation, video, videoTimestamp }: ChartProps) => { files, inputColumns.map(col => col.header), [], - chartInformation.files[i].analyze.analysis, + chartInformation.files[i].analyze.type, chartInformation.files[i].analyze.analyzerValues.filter(e => e), chartInformation.live ); diff --git a/front-end/src/lib/chartOptions.js b/front-end/src/lib/chartOptions.ts similarity index 84% rename from front-end/src/lib/chartOptions.js rename to front-end/src/lib/chartOptions.ts index ead6cc7b..4e8e8db4 100644 --- a/front-end/src/lib/chartOptions.js +++ b/front-end/src/lib/chartOptions.ts @@ -1,11 +1,16 @@ -export const defaultChartOptions = { +import { Options } from 'highcharts'; +import { chartInformation } from '@components/App'; + +export const defaultChartOptions: Options = { chart: { type: 'scatter', - zoomType: 'x', + zooming: { + type: 'x' + }, backgroundColor: '#ffffff' }, title: { - text: 'Template' + text: '' }, subtitle: { text: document.ontouchstart === undefined ? @@ -25,17 +30,19 @@ export const defaultChartOptions = { } }; -const getStandardChartConfig = (chartInformation) => { +const getStandardChartConfig = (chartInformation: chartInformation) => { var chartConfig = defaultChartOptions; - chartConfig.title.text = chartInformation.files[0].columns[1].header + + chartConfig.title = {text: chartInformation.files[0].columns[1].header + ' vs ' + - chartInformation.files[0].columns[0].header; + chartInformation.files[0].columns[0].header}; chartConfig.chart = { type: chartInformation.type, - zoomType: 'x' + zooming: { + type: 'x' + } }; chartConfig.tooltip = { @@ -86,7 +93,7 @@ const getDefaultChartConfig = (chartInformation, parsedData, fileNames) => { }; }); - chartConfig.colorAxis.showInLegend = false; + chartConfig.colorAxis = {showInLegend: false}; return chartConfig; }; @@ -94,21 +101,21 @@ const getDefaultChartConfig = (chartInformation, parsedData, fileNames) => { const getVideoChartConfig = (chartInformation, parsedData, fileNames) => { var chartConfig = getDefaultChartConfig(chartInformation, parsedData, fileNames); - chartConfig.chart.type = 'line'; + chartConfig.chart = {type: 'line'}; - chartConfig.boost.enabled = chartInformation.hasTimestampX; + chartConfig.boost = {enabled: chartInformation.hasTimestampX}; - chartConfig.xAxis.plotLines = [{ + chartConfig.xAxis = {plotLines: [{ color: 'black', width: 2, zIndex: 3, - }]; + }]}; - chartConfig.yAxis.plotLines = [{ + chartConfig.yAxis = {plotLines: [{ color: 'black', width: 2, zIndex: 3, - }]; + }]}; return chartConfig; }; From 4227365203ad4c6fe92cd1e2d15bb7ec2a1e0d73 Mon Sep 17 00:00:00 2001 From: Kai Arseneau Date: Fri, 4 Oct 2024 12:30:43 -0400 Subject: [PATCH 07/19] Refactored chartData to custom hook --- .../src/components/views/Chart/Chart.tsx | 80 ++------------- .../components/views/Chart/useChartData.tsx | 99 +++++++++++++++++++ front-end/src/lib/apiUtils.ts | 11 ++- front-end/src/lib/chartUtils.ts | 8 +- 4 files changed, 118 insertions(+), 80 deletions(-) create mode 100644 front-end/src/components/views/Chart/useChartData.tsx diff --git a/front-end/src/components/views/Chart/Chart.tsx b/front-end/src/components/views/Chart/Chart.tsx index a4c5a0b8..3924dbcf 100644 --- a/front-end/src/components/views/Chart/Chart.tsx +++ b/front-end/src/components/views/Chart/Chart.tsx @@ -1,8 +1,7 @@ import './Chart.css'; import { defaultChartOptions, getChartConfig, movePlotLineX, movePlotLines } from '@lib/chartOptions'; -import { getSeriesData, getTimestamps, LIVE_DATA_INTERVAL, validateChartInformation } from '@lib/chartUtils'; -import { ApiUtil } from '@lib/apiUtils'; -import { useState, useEffect, useRef, useCallback } from 'react'; +import { LIVE_DATA_INTERVAL, validateChartInformation } from '@lib/chartUtils'; +import { useState, useEffect, useRef } from 'react'; import Highcharts from 'highcharts'; import HighchartsReact from 'highcharts-react-official'; import Boost from 'highcharts/modules/boost'; @@ -11,9 +10,9 @@ import { computeOffsets, getFileTimestamp, getPointIndex, binarySearchClosest} f import { useResizeDetector } from 'react-resize-detector'; import loadingImg from '@assets/loading.gif'; import { FileTimespan, MinMax } from '@lib/apiUtils'; -import { seriesData } from '@lib/chartUtils'; import { Chart as ChartType } from 'highcharts'; import { chartInformation } from '@components/App'; +import { useChartData } from './useChartData'; // TODO: Fix this import (Why is it different?) . Currently no ECMA module Womp Womp // eslint-disable-next-line no-undef require('highcharts-multicolor-series')(Highcharts); @@ -30,79 +29,12 @@ interface ChartProps { const Chart = ({ chartInformation, video, videoTimestamp }: ChartProps) => { + const { parsedData, fileNames, timestamps, minMax, loading, refetch } = useChartData(chartInformation); const [chartOptions, setChartOptions] = useState(defaultChartOptions); - const [loading, setLoading] = useState(false); - const [parsedData, setParsedData] = useState([]); - const [fileNames, setFileNames] = useState([]); const [offsets, setOffsets] = useState([]); - const [timestamps, setTimestamps] = useState([]); const [lineX, setLineX] = useState(0); const [linePoint, setLinePoint] = useState({x: 0, y: 0}); const [valueLines, setValueLines] = useState([]); - let minMax = useRef({min: 0, max: 0}); - - // TODO: This whole function is a bit of a mess, should be refactored - // Fetch the data from the server and format it for the chart - const getFileFormat = useCallback(async () => { - // Runs through all the series and fetches the data, then updates the graph - // This also prevents liveData from adding more data as a separate series - var data: seriesData[] = []; - const tempTimestamps: number[][] = []; - for (var i = 0; i < chartInformation.files.length; i++) { - // Create a list of all files in order (formatting for backend) - let files = chartInformation.files[i].columns.map(column => column.filename); - let inputColumns = chartInformation.files[i].columns; - - const response = await ApiUtil.analyzeFiles( - files, - inputColumns.map(col => col.header), - [], - chartInformation.files[i].analyze.type, - chartInformation.files[i].analyze.analyzerValues.filter(e => e), - chartInformation.live - ); - - //TODO: This should be removed and no longer needed I reckon, in place for a better way to return a file / filename - const contentDisposition = response.headers.get('content-disposition'); - if (!contentDisposition) throw new Error('Content-Disposition header is missing'); - const filename = contentDisposition.split('filename=')[1].slice(1, -1); - - setFileNames(prevState => [...prevState, filename]); - - const text = await response.text(); - minMax.current = await ApiUtil.getMinMax(filename, inputColumns[0].header); - - const seriesData = await getSeriesData( - text, - inputColumns, - minMax.current, //TODO THIS MIGHT NOT UPDATE FAST ENOUGH - chartInformation.type, - chartInformation.hasTimestampX, - chartInformation.hasGPSTime - ); - - data.push(seriesData); - // TODO: Fix this section for each case - tempTimestamps.push( - chartInformation.hasTimestampX ? seriesData.map(item => item[0]) as number[] : await getTimestamps(text) - ); - } - setParsedData(data); - setTimestamps(tempTimestamps); - - }, [chartInformation, setFileNames, setParsedData, setTimestamps]); - - // Whenever chartInformation is updated (which happens when submit button is pressed), fetch the neccesary data - useEffect(() => { - if(!validateChartInformation(chartInformation)) return; - - setLoading(true); - setParsedData([]); - setFileNames([]); - - getFileFormat(); - setLoading(false); - }, [chartInformation, getFileFormat]); // Once necessary data is fetched, format it for the chart useEffect(() => { @@ -126,12 +58,12 @@ const Chart = ({ chartInformation, video, videoTimestamp }: ChartProps) => { if (chartInformation.live) { intervalId = setInterval(() => { - getFileFormat(); // TODO: Prevent loading animation or alter it + refetch(); }, LIVE_DATA_INTERVAL); } return () => clearInterval(intervalId); - }, [chartInformation, getFileFormat]); + }, [chartInformation, refetch]); const chartRef = useRef(null); const { width, height, ref } = useResizeDetector({ diff --git a/front-end/src/components/views/Chart/useChartData.tsx b/front-end/src/components/views/Chart/useChartData.tsx new file mode 100644 index 00000000..b94d86ac --- /dev/null +++ b/front-end/src/components/views/Chart/useChartData.tsx @@ -0,0 +1,99 @@ +import { useState, useEffect, useCallback, useRef } from 'react'; +import { ApiUtil, MinMax } from '@lib/apiUtils'; +import { getHeadersIndex, getTimestampOffset, getTimestamps, HUE_MAX, HUE_MIN, validateChartInformation } from '@lib/chartUtils'; +import { seriesData } from '@lib/chartUtils'; +import { chartInformation } from '@components/App'; + +export const useChartData = (chartInformation: chartInformation) => { + const [parsedData, setParsedData] = useState([]); + const [fileNames, setFileNames] = useState([]); + const [timestamps, setTimestamps] = useState([]); + const [loading, setLoading] = useState(false); + let minMax = useRef({ min: 0, max: 0 }); + + const resetData = () => { + setParsedData([]); + setFileNames([]); + setTimestamps([]); // TODO: Maybe not this line + minMax.current = { min: 0, max: 0 }; + }; + + const fetchChartData = useCallback(async () => { + if (!validateChartInformation(chartInformation)) return; + + const { hasGPSTime, hasTimestampX, type } = chartInformation + + for (const file of chartInformation.files) { + const { columns, analyze } = file; + + const { filename, text } = await ApiUtil.analyzeFiles( + columns.map(col => col.filename), + columns.map(col => col.header), + [], + analyze.type, + analyze.analyzerValues.filter(e => e), + chartInformation.live + ); + + setFileNames(prev => [...prev, filename]); + + // TODO: Maybe separate this logic out since its just formatting + let headers = text + .slice(0, text.indexOf('\n')) + .replace('\r', '') + .split(','); + + const headerIndices = getHeadersIndex(headers, columns); + + const lines = text.trim().split('\n').slice(1).map((line) => line.split(',')); + + // TODO: I dont like this handling both cases which are very separate + let seriesData: seriesData; + + if (type !== 'coloredline') { + const timestampOffset = hasTimestampX && hasGPSTime ? getTimestampOffset(columns, lines, headerIndices) : 0; + seriesData = lines.map((line) => { + return [parseFloat(line[headerIndices.x]) + timestampOffset, parseFloat(line[headerIndices.y])]; + }); + } else { + // TODO: Currently only doing this for the first column. Also only should be done if colour mode is enabled + const { min, max } = minMax.current = await ApiUtil.getMinMax(filename, columns[0].header); + + seriesData = lines.map((line) => { + + let val = parseFloat(line[headerIndices.colour]); + let hue = HUE_MIN + (HUE_MAX - HUE_MIN) * (val - minMax.current.min) / (max - min); + + return { + x: parseFloat(line[headerIndices.x]), + y: parseFloat(line[headerIndices.y]), + colorValue: val, + segmentColor: `hsl(${hue}, 100%, 50%)` + }; + }); + } + setParsedData(prev => [...prev, seriesData]); + + // TODO: This while timestamp stuff shouldn't be needed anymore + let timestamps: number[]; + if (hasTimestampX) { + // TODO: Fix this case, which seems to be an overlap of colour and syncing timestamps + timestamps = seriesData.map(item => item[0]) as number[] + } else { + timestamps = await getTimestamps(text); + } + + setTimestamps(prev => [...prev, timestamps]); + } + + }, [chartInformation]); + + useEffect(() => { + setLoading(true); + resetData(); + fetchChartData(); + setLoading(false); + }, [fetchChartData]); + + return { parsedData, fileNames, timestamps, minMax, loading, refetch: fetchChartData }; +} diff --git a/front-end/src/lib/apiUtils.ts b/front-end/src/lib/apiUtils.ts index d72f7182..24359e23 100644 --- a/front-end/src/lib/apiUtils.ts +++ b/front-end/src/lib/apiUtils.ts @@ -88,7 +88,7 @@ export const ApiUtil = { type: AnalyzerType | null, analyzerOptions: string[], // This one is weird as its dependent on which analyzer is run live: boolean - ): Promise => { + ): Promise<{ filename: string, text: string }> => { const params = new URLSearchParams(); console.log(inputFiles, inputColumns, outputFiles, type, analyzerOptions, live); @@ -108,7 +108,14 @@ export const ApiUtil = { alert(`An error has occured!\nCode: ${response.status}\n${await response.text()}`); throw Error(response.statusText); } - return response; + + const contentDisposition = response.headers.get('content-disposition'); + if (!contentDisposition) throw new Error('Content-Disposition header is missing'); + const filename = contentDisposition.split('filename=')[1].slice(1, -1); + + const text = await response.text(); + + return { filename, text }; }, /** diff --git a/front-end/src/lib/chartUtils.ts b/front-end/src/lib/chartUtils.ts index 9c9278df..4554665f 100644 --- a/front-end/src/lib/chartUtils.ts +++ b/front-end/src/lib/chartUtils.ts @@ -12,8 +12,8 @@ interface column { // Constants for colour chart -const HUE_MIN = 150; -const HUE_MAX = 0; +export const HUE_MIN = 150; +export const HUE_MAX = 0; export const LIVE_DATA_INTERVAL = 300; interface colourSeriesData { @@ -82,7 +82,7 @@ export const getSeriesData = async ( // Calculates the offset required to convert the x values to unix timestamps // Adding the timestampOffset results in the x value being a the start time unix millis + millis since first timestamp -const getTimestampOffset = (columns: column[], lines: string[][], headerIndices: headersIndex): number => { +export const getTimestampOffset = (columns: column[], lines: string[][], headerIndices: headersIndex): number => { // Offset is the start time in unix millis minus the first timestamp in the file return new Date(columns[headerIndices.x].timespan.start + 'Z').getTime() - parseFloat(lines[0][headerIndices.x]); }; @@ -101,7 +101,7 @@ interface headersIndex { * @description Matches headers to columns to get the indices of the columns in the headers array. * @returns {Object} An object with the indices of the columns in the headers array. The keys are 'x', 'y', and 'colour' */ -const getHeadersIndex = (headers: string[], columns: column[]): headersIndex => { +export const getHeadersIndex = (headers: string[], columns: column[]): headersIndex => { let h: headersIndex = { x: -1, y: -1, colour: -1 }; for (let i = 0; i < columns.length; i++) { for (let j = 0; j < headers.length; j++) { From ee7c1728b41061c66db66f419da39a5dc9206eb7 Mon Sep 17 00:00:00 2001 From: Kai Arseneau Date: Fri, 4 Oct 2024 12:39:58 -0400 Subject: [PATCH 08/19] Fixing wrong type declaration --- front-end/src/components/App.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/front-end/src/components/App.tsx b/front-end/src/components/App.tsx index 27028786..0c7a2cd2 100644 --- a/front-end/src/components/App.tsx +++ b/front-end/src/components/App.tsx @@ -51,12 +51,12 @@ const App = () => { ); // This is an object so that other updates to it will always call the useEffect, even if the message is the same - const [successMessage, setSuccessMessage] = useState(''); + const [successMessage, setSuccessMessage] = useState<{message: string}>({}); const [buttonID, setButtonID] = useState(null); // Catches when success message is updated and displays it after removing old one useEffect(() => { - if (successMessage === '' || Object.keys(successMessage).length === 0) return; + if (Object.keys(successMessage).length === 0) return; // This could use some work to show that they are different messages more clearly $('div.success').hide().stop(true, false); $('div.success').slideDown(500).delay(2000).slideUp(1000); @@ -67,7 +67,7 @@ const App = () => {
-
{successMessage}
+
{successMessage.message}
{modal === 'Create' ? Date: Fri, 4 Oct 2024 14:28:02 -0400 Subject: [PATCH 09/19] Bug fixing --- front-end/src/components/App.tsx | 26 +----- .../AnalyzersAndSeries/AnalyzersAndSeries.jsx | 2 +- .../src/components/views/Chart/Chart.tsx | 3 +- .../components/views/Chart/useChartData.tsx | 4 +- front-end/src/lib/apiUtils.ts | 2 - front-end/src/lib/chartUtils.ts | 92 +++++-------------- 6 files changed, 30 insertions(+), 99 deletions(-) diff --git a/front-end/src/components/App.tsx b/front-end/src/components/App.tsx index 0c7a2cd2..913dab2b 100644 --- a/front-end/src/components/App.tsx +++ b/front-end/src/components/App.tsx @@ -11,28 +11,6 @@ import $ from 'jquery'; import { MAX_VIEWS } from './views/viewsConfig'; import Chart from './views/Chart/Chart'; import MapChart from './map/MapChart/MapChart'; -import { AnalyzerType } from '@lib/apiUtils' - -export interface chartInformation { - files: { - columns: { - header: string, - filename: string, - timespan: { - start: Date, - end: Date - } - }[], - analyze: { - type: AnalyzerType, - analyzerValues: string[] - } - }[], - live: boolean, - type: string //TODO: UPDATE TO ENUM - hasGPSTime: boolean - hasTimestampX: boolean -} const App = () => { @@ -51,12 +29,12 @@ const App = () => { ); // This is an object so that other updates to it will always call the useEffect, even if the message is the same - const [successMessage, setSuccessMessage] = useState<{message: string}>({}); + const [successMessage, setSuccessMessage] = useState<{message: string}>({message: ''}); const [buttonID, setButtonID] = useState(null); // Catches when success message is updated and displays it after removing old one useEffect(() => { - if (Object.keys(successMessage).length === 0) return; + if (successMessage.message == '') return; // This could use some work to show that they are different messages more clearly $('div.success').hide().stop(true, false); $('div.success').slideDown(500).delay(2000).slideUp(1000); diff --git a/front-end/src/components/modal/create/AnalyzersAndSeries/AnalyzersAndSeries.jsx b/front-end/src/components/modal/create/AnalyzersAndSeries/AnalyzersAndSeries.jsx index 2e473964..81814d3e 100644 --- a/front-end/src/components/modal/create/AnalyzersAndSeries/AnalyzersAndSeries.jsx +++ b/front-end/src/components/modal/create/AnalyzersAndSeries/AnalyzersAndSeries.jsx @@ -84,7 +84,7 @@ const AnalyzersAndSeries = ({ const newSeries = { 'columns': selectColumns, 'analyze': { - 'analysis': checkedAnalyzer.code, + 'type': checkedAnalyzer.code, 'analyzerValues': checkedAnalyzer.parameters.map(param => { const value = document.getElementById(param.name).value; return value === '' ? null : value; diff --git a/front-end/src/components/views/Chart/Chart.tsx b/front-end/src/components/views/Chart/Chart.tsx index 3924dbcf..3f9358dd 100644 --- a/front-end/src/components/views/Chart/Chart.tsx +++ b/front-end/src/components/views/Chart/Chart.tsx @@ -11,7 +11,7 @@ import { useResizeDetector } from 'react-resize-detector'; import loadingImg from '@assets/loading.gif'; import { FileTimespan, MinMax } from '@lib/apiUtils'; import { Chart as ChartType } from 'highcharts'; -import { chartInformation } from '@components/App'; +import { chartInformation } from '@lib/chartUtils'; import { useChartData } from './useChartData'; // TODO: Fix this import (Why is it different?) . Currently no ECMA module Womp Womp // eslint-disable-next-line no-undef @@ -20,7 +20,6 @@ require('highcharts-multicolor-series')(Highcharts); HighchartsColorAxis(Highcharts); Boost(Highcharts); - interface ChartProps { chartInformation: chartInformation; video: FileTimespan; diff --git a/front-end/src/components/views/Chart/useChartData.tsx b/front-end/src/components/views/Chart/useChartData.tsx index b94d86ac..43df4ee1 100644 --- a/front-end/src/components/views/Chart/useChartData.tsx +++ b/front-end/src/components/views/Chart/useChartData.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, useRef } from 'react'; import { ApiUtil, MinMax } from '@lib/apiUtils'; import { getHeadersIndex, getTimestampOffset, getTimestamps, HUE_MAX, HUE_MIN, validateChartInformation } from '@lib/chartUtils'; import { seriesData } from '@lib/chartUtils'; -import { chartInformation } from '@components/App'; +import { chartInformation } from '@lib/chartUtils'; export const useChartData = (chartInformation: chartInformation) => { const [parsedData, setParsedData] = useState([]); @@ -23,6 +23,8 @@ export const useChartData = (chartInformation: chartInformation) => { const { hasGPSTime, hasTimestampX, type } = chartInformation + console.log(chartInformation) + for (const file of chartInformation.files) { const { columns, analyze } = file; diff --git a/front-end/src/lib/apiUtils.ts b/front-end/src/lib/apiUtils.ts index 24359e23..d766da9b 100644 --- a/front-end/src/lib/apiUtils.ts +++ b/front-end/src/lib/apiUtils.ts @@ -91,8 +91,6 @@ export const ApiUtil = { ): Promise<{ filename: string, text: string }> => { const params = new URLSearchParams(); - console.log(inputFiles, inputColumns, outputFiles, type, analyzerOptions, live); - inputFiles.map(file => params.append('inputFiles', file)); inputColumns.map(column => params.append('inputColumns', column)); outputFiles?.map(file => params.append('outputFiles', file)); diff --git a/front-end/src/lib/chartUtils.ts b/front-end/src/lib/chartUtils.ts index 4554665f..b4d9f79d 100644 --- a/front-end/src/lib/chartUtils.ts +++ b/front-end/src/lib/chartUtils.ts @@ -1,6 +1,19 @@ -import { MinMax } from './apiUtils'; +import { AnalyzerType } from './apiUtils'; + +export interface chartInformation { + files: { + columns: column[], + analyze: { + type: AnalyzerType, + analyzerValues: string[] + } + }[], + live: boolean, + type: string //TODO: UPDATE TO ENUM + hasGPSTime: boolean + hasTimestampX: boolean +} -//TODO: Move this definition elsewhere interface column { header: string; filename: string; @@ -10,8 +23,6 @@ interface column { } } - -// Constants for colour chart export const HUE_MIN = 150; export const HUE_MAX = 0; export const LIVE_DATA_INTERVAL = 300; @@ -23,62 +34,13 @@ interface colourSeriesData { segmentColor: string; } -export type seriesData = colourSeriesData[] | number[][]; - -/** - * @description Fetches the data from the server and formats it for the chart. - * @param {Object} response - The file response from the server. - * @param {string} filename - The name of the file. - * @param {Object[]} columns - The columns to be fetched. - * @param {useRef} minMax - The minimum and maximum values of the colour value. - * @param {string} chartType - The type of chart. - * @param {boolean} hasTimestampX - True if the x values are Timestamp (ms), false otherwise. - * @param {boolean} hasGPSTime - True if all the series have GPS timespans, false otherwise. - * @returns {Promise>} A promise that resolves to an array of data objects. - */ -export const getSeriesData = async ( - text: string, - columns: column[], - minMax: MinMax, - chartType: any, // TODO: Update this with highcharts options / enum - hasTimestampX: boolean, - hasGPSTime: boolean -): Promise => { - - let headers = text.trim().split('\n')[0].split(','); - headers[headers.length - 1] = headers[headers.length - 1].replace('\r', ''); - - let headerIndices = getHeadersIndex(headers, columns); - - // Get all the lines of the file, and split them into arrays - const lines = text.trim().split('\n').slice(1).map((line) => line.split(',')); - - // If not colour, return values in array to allow for boost - - if (chartType !== 'coloredline') { - const timestampOffset = hasTimestampX && hasGPSTime ? getTimestampOffset(columns, lines, headerIndices) : 0; - return lines.map((line) => { - return [parseFloat(line[headerIndices.x]) + timestampOffset, parseFloat(line[headerIndices.y])]; - }); - } - - // If colour, return the data in object format to allow for colouring - // Make a request to get the maximum and minimum values of the colour value - // TODO: Seems to break when giving it a file with 3+ colomns, worth looking into - - return lines.map((line) => { - - let val = parseFloat(line[headerIndices.colour]); - let hue = HUE_MIN + (HUE_MAX - HUE_MIN) * (val - minMax.min) / (minMax.max - minMax.min); +interface headersIndex { + x: number; + y: number; + colour: number; +} - return { - x: parseFloat(line[headerIndices.x]), - y: parseFloat(line[headerIndices.y]), - colorValue: val, - segmentColor: `hsl(${hue}, 100%, 50%)` - }; - }); -}; +export type seriesData = colourSeriesData[] | number[][]; // Calculates the offset required to convert the x values to unix timestamps // Adding the timestampOffset results in the x value being a the start time unix millis + millis since first timestamp @@ -92,11 +54,7 @@ export const getTimestamps = async (text: string) => { return text.trim().split('\n').slice(1).map((line) => parseFloat(line.split(',')[timestampHeaderIndex])); }; -interface headersIndex { - x: number; - y: number; - colour: number; -} + /** * @description Matches headers to columns to get the indices of the columns in the headers array. * @returns {Object} An object with the indices of the columns in the headers array. The keys are 'x', 'y', and 'colour' @@ -119,11 +77,7 @@ export const getHeadersIndex = (headers: string[], columns: column[]): headersIn return h; }; -/** - * @param {Object} chartInformation - The chart information object. - * @returns {Boolean} True if the chart information is full, false otherwise. - */ -export const validateChartInformation = (chartInformation: any): boolean => { +export const validateChartInformation = (chartInformation: chartInformation): boolean => { if (!chartInformation) { return false; } From 1ca8f69e2a14bc505e1bb2f30b16fb4bb7946b0a Mon Sep 17 00:00:00 2001 From: Kai Arseneau Date: Fri, 4 Oct 2024 14:54:20 -0400 Subject: [PATCH 10/19] Adding linting --- front-end/.eslintrc.js | 51 -- front-end/eslint.config.mjs | 12 + front-end/package-lock.json | 620 ++++++++++++------ front-end/package.json | 12 +- front-end/src/components/analyzerData.js | 1 - .../src/components/views/Chart/Chart.tsx | 7 +- .../components/views/Chart/useChartData.tsx | 18 +- front-end/src/lib/apiUtils.ts | 8 +- front-end/src/lib/chartOptions.ts | 10 +- front-end/src/lib/chartUtils.ts | 2 +- front-end/src/lib/mapUtils.js | 10 +- 11 files changed, 471 insertions(+), 280 deletions(-) delete mode 100644 front-end/.eslintrc.js create mode 100644 front-end/eslint.config.mjs diff --git a/front-end/.eslintrc.js b/front-end/.eslintrc.js deleted file mode 100644 index 6383a39d..00000000 --- a/front-end/.eslintrc.js +++ /dev/null @@ -1,51 +0,0 @@ -module.exports = { - 'env': { - 'browser': true, - 'es2021': true - }, - 'extends': [ - 'eslint:recommended', - 'plugin:react/recommended' - ], - 'overrides': [ - { - 'env': { - 'node': true - }, - 'files': [ - '.eslintrc.{js,cjs}' - ], - 'parserOptions': { - 'sourceType': 'script' - } - } - ], - 'parserOptions': { - 'ecmaVersion': 'latest', - 'sourceType': 'module' - }, - 'plugins': [ - 'react' - ], - 'rules': { - 'indent': [ - 'error', - 2, - { 'SwitchCase': 1 } - ], - 'linebreak-style': 'off', - 'quotes': [ - 'warn', - 'single' - ], - 'semi': [ - 'warn', - 'always' - ], - 'max-len': [ - 'warn', - {'code': 120} - ], - 'react/prop-types': 'off' // disable react/prop-types rule temporarily, until we figure this out - } -}; diff --git a/front-end/eslint.config.mjs b/front-end/eslint.config.mjs new file mode 100644 index 00000000..fd901bc9 --- /dev/null +++ b/front-end/eslint.config.mjs @@ -0,0 +1,12 @@ +// @ts-check + +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +// TODO: Consider linting with type information +// https://typescript-eslint.io/getting-started/typed-linting +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.strict, + ...tseslint.configs.stylistic, +); \ No newline at end of file diff --git a/front-end/package-lock.json b/front-end/package-lock.json index 0b60734c..f491a231 100644 --- a/front-end/package-lock.json +++ b/front-end/package-lock.json @@ -12,7 +12,6 @@ "@types/react": "^18.3.6", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", - "eslint": "^8.57.0", "eslint-plugin-react": "^7.34.1", "font-awesome": "^4.7.0", "highcharts": "^11.4.8", @@ -30,14 +29,18 @@ "react-player": "^2.15.1", "react-resize-detector": "^10.0.1", "react-router-dom": "^6.22.3", - "typescript": "^5.6.2", "vite": "^5.4.3", "vite-plugin-commonjs": "^0.10.1", "vitest": "^2.1.1" }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", - "@svgr/webpack": "^8.0.0" + "@eslint/js": "^9.11.1", + "@svgr/webpack": "^8.0.0", + "@types/eslint__js": "^8.42.3", + "eslint": "^9.11.1", + "typescript": "^5.6.2", + "typescript-eslint": "^8.8.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -2270,22 +2273,43 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz", + "integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -2293,56 +2317,48 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dependencies": { - "type-fest": "^0.20.2" - }, + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "node_modules/@eslint/js": { + "version": "9.11.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.1.tgz", + "integrity": "sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "node_modules/@eslint/plugin-kit": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz", + "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" + "levn": "^0.4.1" }, "engines": { - "node": ">=10.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@humanwhocodes/module-importer": { @@ -2357,10 +2373,17 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==" + "node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", @@ -2976,11 +2999,35 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint__js": { + "version": "8.42.3", + "resolved": "https://registry.npmjs.org/@types/eslint__js/-/eslint__js-8.42.3.tgz", + "integrity": "sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==", + "dev": true, + "dependencies": { + "@types/eslint": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, "node_modules/@types/node": { "version": "22.5.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz", @@ -3011,10 +3058,223 @@ "@types/react": "*" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.0.tgz", + "integrity": "sha512-wORFWjU30B2WJ/aXBfOm1LX9v9nyt9D3jsSOxC3cCaTQGCW5k4jNpmjFv3U7p/7s4yvdjHzwtv2Sd2dOyhjS0A==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.8.0", + "@typescript-eslint/type-utils": "8.8.0", + "@typescript-eslint/utils": "8.8.0", + "@typescript-eslint/visitor-keys": "8.8.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.0.tgz", + "integrity": "sha512-uEFUsgR+tl8GmzmLjRqz+VrDv4eoaMqMXW7ruXfgThaAShO9JTciKpEsB+TvnfFfbg5IpujgMXVV36gOJRLtZg==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.8.0", + "@typescript-eslint/types": "8.8.0", + "@typescript-eslint/typescript-estree": "8.8.0", + "@typescript-eslint/visitor-keys": "8.8.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.0.tgz", + "integrity": "sha512-EL8eaGC6gx3jDd8GwEFEV091210U97J0jeEHrAYvIYosmEGet4wJ+g0SYmLu+oRiAwbSA5AVrt6DxLHfdd+bUg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.8.0", + "@typescript-eslint/visitor-keys": "8.8.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.0.tgz", + "integrity": "sha512-IKwJSS7bCqyCeG4NVGxnOP6lLT9Okc3Zj8hLO96bpMkJab+10HIfJbMouLrlpyOr3yrQ1cA413YPFiGd1mW9/Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "8.8.0", + "@typescript-eslint/utils": "8.8.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.0.tgz", + "integrity": "sha512-QJwc50hRCgBd/k12sTykOJbESe1RrzmX6COk8Y525C9l7oweZ+1lw9JiU56im7Amm8swlz00DRIlxMYLizr2Vw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.0.tgz", + "integrity": "sha512-ZaMJwc/0ckLz5DaAZ+pNLmHv8AMVGtfWxZe/x2JVEkD5LnmhWiQMMcYT7IY7gkdJuzJ9P14fRy28lUrlDSWYdw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.8.0", + "@typescript-eslint/visitor-keys": "8.8.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.0.tgz", + "integrity": "sha512-QE2MgfOTem00qrlPgyByaCHay9yb1+9BjnMFnSFkUKQfu7adBXDTnCAivURnuPPAG/qiB+kzKkZKmKfaMT0zVg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.8.0", + "@typescript-eslint/types": "8.8.0", + "@typescript-eslint/typescript-estree": "8.8.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.0.tgz", + "integrity": "sha512-8mq51Lx6Hpmd7HnA2fcHQo3YgfX1qbccxQOgZcb4tvasu//zXRaA1j5ZRFeCw/VRAdFi4mRM9DnZw0Nu0Q2d1g==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.8.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } }, "node_modules/@vitejs/plugin-react": { "version": "4.3.1", @@ -3143,9 +3403,9 @@ } }, "node_modules/acorn": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", - "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "bin": { "acorn": "bin/acorn" }, @@ -3851,17 +4111,6 @@ "redux": "^4.1.1" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/domelementtype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", @@ -4119,42 +4368,41 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "9.11.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.11.1.tgz", + "integrity": "sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/core": "^0.6.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.11.1", + "@eslint/plugin-kit": "^0.2.0", "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.0.2", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", @@ -4166,10 +4414,18 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-plugin-react": { @@ -4231,15 +4487,15 @@ } }, "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", + "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -4256,6 +4512,11 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + }, "node_modules/eslint/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -4312,18 +4573,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", - "dependencies": { - "type-fest": "^0.20.2" - }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", "engines": { - "node": ">=8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint/node_modules/has-flag": { @@ -4345,28 +4603,28 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "node_modules/espree": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", + "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.1.0" + }, "engines": { - "node": ">=10" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/eslint" } }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -4468,14 +4726,14 @@ } }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/fill-range": { @@ -4505,22 +4763,21 @@ } }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" }, "node_modules/font-awesome": { "version": "4.7.0", @@ -4538,11 +4795,6 @@ "is-callable": "^1.1.3" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4639,25 +4891,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4705,7 +4938,8 @@ "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true }, "node_modules/has-bigints": { "version": "1.0.2", @@ -4809,9 +5043,9 @@ } }, "node_modules/ignore": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", - "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "engines": { "node": ">= 4" } @@ -4844,15 +5078,6 @@ "node": ">=0.8.19" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -5645,14 +5870,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -5739,14 +5956,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -6210,20 +6419,6 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -6707,6 +6902,18 @@ "node": ">=8.0" } }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -6797,6 +7004,7 @@ "version": "5.6.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6805,6 +7013,29 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.8.0.tgz", + "integrity": "sha512-BjIT/VwJ8+0rVO01ZQ2ZVnjE1svFBiRczcpr1t1Yxt7sT25VSbPfrJtDsQ8uQTy2pilX5nI9gwxhUyLULNentw==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.8.0", + "@typescript-eslint/parser": "8.8.0", + "@typescript-eslint/utils": "8.8.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -7204,11 +7435,6 @@ "node": ">=8" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/front-end/package.json b/front-end/package.json index 57e10bdb..2a61d67b 100644 --- a/front-end/package.json +++ b/front-end/package.json @@ -7,7 +7,6 @@ "@types/react": "^18.3.6", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", - "eslint": "^8.57.0", "eslint-plugin-react": "^7.34.1", "font-awesome": "^4.7.0", "highcharts": "^11.4.8", @@ -25,7 +24,6 @@ "react-player": "^2.15.1", "react-resize-detector": "^10.0.1", "react-router-dom": "^6.22.3", - "typescript": "^5.6.2", "vite": "^5.4.3", "vite-plugin-commonjs": "^0.10.1", "vitest": "^2.1.1" @@ -33,7 +31,8 @@ "scripts": { "start": "vite", "build": "tsc && vite build", - "serve": "vite preview" + "serve": "vite preview", + "lint": "npx eslint . --fix" }, "eslintConfig": { "extends": [ @@ -55,7 +54,12 @@ }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", - "@svgr/webpack": "^8.0.0" + "@eslint/js": "^9.11.1", + "@svgr/webpack": "^8.0.0", + "@types/eslint__js": "^8.42.3", + "eslint": "^9.11.1", + "typescript": "^5.6.2", + "typescript-eslint": "^8.8.0" }, "overrides": { "@svgr/webpack": "$@svgr/webpack" diff --git a/front-end/src/components/analyzerData.js b/front-end/src/components/analyzerData.js index ebbe5a60..cf5f7aff 100644 --- a/front-end/src/components/analyzerData.js +++ b/front-end/src/components/analyzerData.js @@ -1,4 +1,3 @@ -/* eslint-disable max-len */ // const images = {}; // const importAll = r => { // r.keys().forEach(key => images[key] = r(key)); diff --git a/front-end/src/components/views/Chart/Chart.tsx b/front-end/src/components/views/Chart/Chart.tsx index 3f9358dd..90d0e6d2 100644 --- a/front-end/src/components/views/Chart/Chart.tsx +++ b/front-end/src/components/views/Chart/Chart.tsx @@ -9,12 +9,12 @@ import HighchartsColorAxis from 'highcharts/modules/coloraxis'; import { computeOffsets, getFileTimestamp, getPointIndex, binarySearchClosest} from '@lib/videoUtils'; import { useResizeDetector } from 'react-resize-detector'; import loadingImg from '@assets/loading.gif'; -import { FileTimespan, MinMax } from '@lib/apiUtils'; +import { FileTimespan } from '@lib/apiUtils'; import { Chart as ChartType } from 'highcharts'; import { chartInformation } from '@lib/chartUtils'; import { useChartData } from './useChartData'; // TODO: Fix this import (Why is it different?) . Currently no ECMA module Womp Womp -// eslint-disable-next-line no-undef +// eslint-disable-next-line @typescript-eslint/no-require-imports require('highcharts-multicolor-series')(Highcharts); HighchartsColorAxis(Highcharts); @@ -83,7 +83,8 @@ const Chart = ({ chartInformation, video, videoTimestamp }: ChartProps) => { // Handles updating the chart when the video timestamp changes useEffect(() => { if (timestamps.length === 0) return; - chartInformation.hasTimestampX ? lineXUpdate(videoTimestamp) : linePointUpdate(videoTimestamp); + if (chartInformation.hasTimestampX) lineXUpdate(videoTimestamp); + else linePointUpdate(videoTimestamp); }, [videoTimestamp, offsets, timestamps]); useEffect(() => { diff --git a/front-end/src/components/views/Chart/useChartData.tsx b/front-end/src/components/views/Chart/useChartData.tsx index 43df4ee1..22fe7a62 100644 --- a/front-end/src/components/views/Chart/useChartData.tsx +++ b/front-end/src/components/views/Chart/useChartData.tsx @@ -9,7 +9,7 @@ export const useChartData = (chartInformation: chartInformation) => { const [fileNames, setFileNames] = useState([]); const [timestamps, setTimestamps] = useState([]); const [loading, setLoading] = useState(false); - let minMax = useRef({ min: 0, max: 0 }); + const minMax = useRef({ min: 0, max: 0 }); const resetData = () => { setParsedData([]); @@ -29,18 +29,18 @@ export const useChartData = (chartInformation: chartInformation) => { const { columns, analyze } = file; const { filename, text } = await ApiUtil.analyzeFiles( - columns.map(col => col.filename), - columns.map(col => col.header), - [], - analyze.type, - analyze.analyzerValues.filter(e => e), + columns.map(col => col.filename), + columns.map(col => col.header), + [], + analyze.type, + analyze.analyzerValues.filter(e => e), chartInformation.live ); setFileNames(prev => [...prev, filename]); // TODO: Maybe separate this logic out since its just formatting - let headers = text + const headers = text .slice(0, text.indexOf('\n')) .replace('\r', '') .split(','); @@ -63,8 +63,8 @@ export const useChartData = (chartInformation: chartInformation) => { seriesData = lines.map((line) => { - let val = parseFloat(line[headerIndices.colour]); - let hue = HUE_MIN + (HUE_MAX - HUE_MIN) * (val - minMax.current.min) / (max - min); + const val = parseFloat(line[headerIndices.colour]); + const hue = HUE_MIN + (HUE_MAX - HUE_MIN) * (val - minMax.current.min) / (max - min); return { x: parseFloat(line[headerIndices.x]), diff --git a/front-end/src/lib/apiUtils.ts b/front-end/src/lib/apiUtils.ts index d766da9b..cbec431c 100644 --- a/front-end/src/lib/apiUtils.ts +++ b/front-end/src/lib/apiUtils.ts @@ -46,9 +46,9 @@ export const ApiUtil = { /** * @description Fetches a list of files from the server. - * @returns {Promise} A promise that resolves to an array of file names. + * @returns {Promise} A promise that resolves to an array of file names. */ - getFiles: async (): Promise => { + getFiles: async (): Promise => { const response = await fetch(`http://${window.location.hostname}:8080/files`); if (!response.ok) throw Error(response.statusText); @@ -94,9 +94,9 @@ export const ApiUtil = { inputFiles.map(file => params.append('inputFiles', file)); inputColumns.map(column => params.append('inputColumns', column)); outputFiles?.map(file => params.append('outputFiles', file)); - type && params.append('type', type); + if (type) params.append('type', type); analyzerOptions.map(option => params.append('analyzerOptions', option)); - live && params.append('live', live.toString()); + if (live) params.append('live', live.toString()); const response = await fetch(`http://${window.location.hostname}:8080/analyze?` + params.toString(), { method: 'POST' diff --git a/front-end/src/lib/chartOptions.ts b/front-end/src/lib/chartOptions.ts index 4e8e8db4..beb79c7a 100644 --- a/front-end/src/lib/chartOptions.ts +++ b/front-end/src/lib/chartOptions.ts @@ -1,5 +1,5 @@ import { Options } from 'highcharts'; -import { chartInformation } from '@components/App'; +import { chartInformation } from './chartUtils'; export const defaultChartOptions: Options = { chart: { @@ -32,7 +32,7 @@ export const defaultChartOptions: Options = { const getStandardChartConfig = (chartInformation: chartInformation) => { - var chartConfig = defaultChartOptions; + const chartConfig = defaultChartOptions; chartConfig.title = {text: chartInformation.files[0].columns[1].header + ' vs ' + @@ -77,7 +77,7 @@ const getStandardChartConfig = (chartInformation: chartInformation) => { }; const getDefaultChartConfig = (chartInformation, parsedData, fileNames) => { - var chartConfig = getStandardChartConfig(chartInformation); + const chartConfig = getStandardChartConfig(chartInformation); const colours = ['blue', 'red', 'green', 'yellow', 'purple', 'orange', 'pink', 'brown', 'black', 'grey']; chartConfig.series = parsedData.map((data, index) => { @@ -99,7 +99,7 @@ const getDefaultChartConfig = (chartInformation, parsedData, fileNames) => { }; const getVideoChartConfig = (chartInformation, parsedData, fileNames) => { - var chartConfig = getDefaultChartConfig(chartInformation, parsedData, fileNames); + const chartConfig = getDefaultChartConfig(chartInformation, parsedData, fileNames); chartConfig.chart = {type: 'line'}; @@ -121,7 +121,7 @@ const getVideoChartConfig = (chartInformation, parsedData, fileNames) => { }; const getColourChartConfig = (chartInformation, parsedData, fileNames, minMax) => { - var chartConfig = getStandardChartConfig(chartInformation); + const chartConfig = getStandardChartConfig(chartInformation); chartConfig.series = parsedData.map((data, index) => { return { diff --git a/front-end/src/lib/chartUtils.ts b/front-end/src/lib/chartUtils.ts index b4d9f79d..d3fe5a47 100644 --- a/front-end/src/lib/chartUtils.ts +++ b/front-end/src/lib/chartUtils.ts @@ -60,7 +60,7 @@ export const getTimestamps = async (text: string) => { * @returns {Object} An object with the indices of the columns in the headers array. The keys are 'x', 'y', and 'colour' */ export const getHeadersIndex = (headers: string[], columns: column[]): headersIndex => { - let h: headersIndex = { x: -1, y: -1, colour: -1 }; + const h: headersIndex = { x: -1, y: -1, colour: -1 }; for (let i = 0; i < columns.length; i++) { for (let j = 0; j < headers.length; j++) { if (columns[i].header === headers[j].trim()) { diff --git a/front-end/src/lib/mapUtils.js b/front-end/src/lib/mapUtils.js index f479d1b2..39381972 100644 --- a/front-end/src/lib/mapUtils.js +++ b/front-end/src/lib/mapUtils.js @@ -46,17 +46,17 @@ export function pointInRect(point, bounds) { export function findLapTimes(coords, rects) { let inside = false; let events = []; - for (let i = 0; i < coords.length; i++) { + for (const coord of coords) { for (let [index, elem] of rects.entries()) { - if (!inside && pointInRect([coords[i][LAT_INDEX], coords[i][LNG_INDEX]], elem.bounds)) { + if (!inside && pointInRect([coord[LAT_INDEX], coord[LNG_INDEX]], elem.bounds)) { inside = true; - events.push(boundaryEvent(ENTER, index, coords[i][TIME_INDEX])); + events.push(boundaryEvent(ENTER, index, coord[TIME_INDEX])); } else if (inside && events[events.length - 1].rect === index - && !pointInRect([coords[i][LAT_INDEX], coords[i][LNG_INDEX]], elem.bounds)) + && !pointInRect([coord[LAT_INDEX], coord[LNG_INDEX]], elem.bounds)) { inside = false; - events.push(boundaryEvent(EXIT, index, coords[i][TIME_INDEX])); + events.push(boundaryEvent(EXIT, index, coord[TIME_INDEX])); } } } From 5b3e14ff815b871457370e0bdaf8b2faad8c8d29 Mon Sep 17 00:00:00 2001 From: Kai Arseneau Date: Fri, 4 Oct 2024 16:27:27 -0400 Subject: [PATCH 11/19] Separated line syncing --- .../src/components/views/Chart/Chart.tsx | 144 +++--------------- .../views/Chart/useVideoSyncLines.tsx | 136 +++++++++++++++++ 2 files changed, 155 insertions(+), 125 deletions(-) create mode 100644 front-end/src/components/views/Chart/useVideoSyncLines.tsx diff --git a/front-end/src/components/views/Chart/Chart.tsx b/front-end/src/components/views/Chart/Chart.tsx index 90d0e6d2..34a842c6 100644 --- a/front-end/src/components/views/Chart/Chart.tsx +++ b/front-end/src/components/views/Chart/Chart.tsx @@ -13,10 +13,13 @@ import { FileTimespan } from '@lib/apiUtils'; import { Chart as ChartType } from 'highcharts'; import { chartInformation } from '@lib/chartUtils'; import { useChartData } from './useChartData'; +import { useVideoSyncLines } from './useVideoSyncLines'; // TODO: Fix this import (Why is it different?) . Currently no ECMA module Womp Womp // eslint-disable-next-line @typescript-eslint/no-require-imports require('highcharts-multicolor-series')(Highcharts); +import { Series } from 'highcharts'; + HighchartsColorAxis(Highcharts); Boost(Highcharts); @@ -28,12 +31,11 @@ interface ChartProps { const Chart = ({ chartInformation, video, videoTimestamp }: ChartProps) => { - const { parsedData, fileNames, timestamps, minMax, loading, refetch } = useChartData(chartInformation); + const chartRef = useRef(null); const [chartOptions, setChartOptions] = useState(defaultChartOptions); - const [offsets, setOffsets] = useState([]); - const [lineX, setLineX] = useState(0); - const [linePoint, setLinePoint] = useState({x: 0, y: 0}); - const [valueLines, setValueLines] = useState([]); + + const { parsedData, fileNames, timestamps, minMax, loading, refetch } = useChartData(chartInformation); + const { lineX, linePoint, syncedDataPoints } = useVideoSyncLines(chartInformation, chartRef, videoTimestamp, video, timestamps); // Once necessary data is fetched, format it for the chart useEffect(() => { @@ -49,6 +51,16 @@ const Chart = ({ chartInformation, video, videoTimestamp }: ChartProps) => { }, [parsedData, fileNames, chartInformation]); + useEffect(() => { + if (lineX === 0) return; + setChartOptions(movePlotLineX(chartOptions, lineX)); + }, [lineX]); + + useEffect(() => { + if (linePoint.x === 0 && linePoint.y === 0) return; + setChartOptions(movePlotLines(chartOptions, linePoint.x, linePoint.y)); + }, [linePoint]); + // This function loops when live is true, and updates the chart every 500ms useEffect(() => { if(!validateChartInformation(chartInformation)) return; @@ -64,7 +76,7 @@ const Chart = ({ chartInformation, video, videoTimestamp }: ChartProps) => { return () => clearInterval(intervalId); }, [chartInformation, refetch]); - const chartRef = useRef(null); + const { width, height, ref } = useResizeDetector({ onResize: () => { if (chartRef.current) { @@ -75,127 +87,9 @@ const Chart = ({ chartInformation, video, videoTimestamp }: ChartProps) => { refreshRate: 100, }); - useEffect(() => { - if (video.key === '' || chartInformation === undefined) return; - setOffsets(computeOffsets(chartInformation, video)); - }, [chartInformation, video]); - - // Handles updating the chart when the video timestamp changes - useEffect(() => { - if (timestamps.length === 0) return; - if (chartInformation.hasTimestampX) lineXUpdate(videoTimestamp); - else linePointUpdate(videoTimestamp); - }, [videoTimestamp, offsets, timestamps]); - - useEffect(() => { - if (lineX === 0) return; - setChartOptions(movePlotLineX(chartOptions, lineX)); - }, [lineX]); - - useEffect(() => { - if (linePoint.x === 0 && linePoint.y === 0) return; - setChartOptions(movePlotLines(chartOptions, linePoint.x, linePoint.y)); - }, [linePoint]); - - // Reset the value box when chartInformation changes - useEffect(() => { - setValueLines([]); - }, [chartInformation]); - - // Calculates and updates which value is closest to the video timestamp for each series - const lineXUpdate = (videoTimestamp) => { - if (chartRef.current === null || chartRef.current.series.length === 0) return; - - // TODO: Find a better base value for fileTimestamp - let fileTimestamp = -Infinity; - - const visibleSeries = chartRef.current.series.filter(series => series.visible); - if (visibleSeries.length === 0) return; - - // Gets the first file timestamp that is not undefined - visibleSeries.some(series => { - if (chartRef.current === null) return; - const seriesIndex = chartRef.current.series.indexOf(series); - fileTimestamp = getFileTimestamp(videoTimestamp, offsets[seriesIndex], timestamps[seriesIndex]); - return fileTimestamp !== undefined; - }); - - // Updates the lineX value with the new file timestamp - const newLineX = Math.floor(fileTimestamp); - if (fileTimestamp !== undefined) setLineX(newLineX); else return; - - // Finds the closest value to the new lineX value for each series - // Skips the series whose x values do not contain the new lineX value in their domain - const values = visibleSeries.flatMap(series => { - if (newLineX < series.xData[0] || newLineX > series.xData[series.xData.length - 1]) return []; - const closestXIndex = binarySearchClosest(series.xData, newLineX); - return [{ name: series.name, y: series.yData[closestXIndex] }]; - }); - - // Updates the value box with the found values - const tempValueLines = ['Timestamp: ' + new Date(newLineX).toUTCString()]; - chartRef.current.series.forEach(series => { - const value = values.find(value => value.name === series.name); - if (value === undefined) return; - tempValueLines.push(`${series.name}: ${value.y}`); - }); - setValueLines(tempValueLines); - }; - - // Calculates and updates which point is closest to the video timestamp for each series - const linePointUpdate = (videoTimestamp) => { - // Finds the matching point index for the first visible series using the video timestamp - if (chartRef.current === null || chartRef.current.series.length === 0) return; - //TODO: Update this null check to be inline and return the right case - const visibleSeries = chartRef.current.series.filter(series => series.visible); - if (visibleSeries.length === 0) return; - const firstVisibleSeries = visibleSeries[0]; - const seriesIndex = chartRef.current.series.indexOf(firstVisibleSeries); - const pointIndex = getPointIndex( - firstVisibleSeries, - videoTimestamp, - offsets[seriesIndex], - timestamps[seriesIndex] - ); - - if (pointIndex >= 0) { - setLinePoint({x: firstVisibleSeries.xData[pointIndex], y: firstVisibleSeries.yData[pointIndex]}); - } else return; - - // Finds the point index for all the other visible series - const values = [ - { - name: firstVisibleSeries.name, - x: firstVisibleSeries.xData[pointIndex], - y: firstVisibleSeries.yData[pointIndex] - }, ...visibleSeries.slice(1).map(series => { - if (chartRef.current === null || chartRef.current.series.length === 0) throw new Error('Chart is not initialized'); - const seriesIndex = chartRef.current.series.indexOf(series); - const pointIndex = getPointIndex( - series, - videoTimestamp, - offsets[seriesIndex], - timestamps[seriesIndex] - ); - if (pointIndex >= 0) { - return {name: series.name, x: series.xData[pointIndex], y: series.yData[pointIndex]}; - } - }) - ]; - - // Updates the value box with the found values - const tempValueLines: string[] = []; - chartRef.current.series.forEach(series => { - const value = values.find(value => value.name === series.name); - if (value === undefined) return; - tempValueLines.push(`${series.name}: (${value.x.toFixed(5)}, ${value.y.toFixed(5)})`); - }); - setValueLines(tempValueLines); - }; - return (
- {valueLines.length > 0 ? (
{valueLines.join('\n')}
) : null} + {syncedDataPoints.length > 0 ? (
{syncedDataPoints.join('\n')}
) : null}
, + videoTimestamp: number, + videoTimespan: FileTimespan, + timestamps: number[][] +) => { + const [offsets, setOffsets] = useState([]); + const [lineX, setLineX] = useState(0); + const [linePoint, setLinePoint] = useState({x: 0, y: 0}); + const [syncedDataPoints, setSyncedDataPoints] = useState([]); + + const resetData = () => { + setOffsets([]); + setLineX(0); + setLinePoint({x: 0, y: 0}); + setSyncedDataPoints([]); + } + + // Do initial calculation when timespan or chartInformation change + useEffect(() => { + resetData(); + if (videoTimespan === undefined || !validateChartInformation(chartInformation)) return; + setOffsets(computeOffsets(chartInformation, videoTimespan)); + }, [videoTimespan, chartInformation]); + + // Updates when video plays + useEffect(() => { + if (timestamps.length === 0) return; + if (chartInformation.hasTimestampX) { + lineXUpdate(videoTimestamp); + } else { + linePointUpdate(videoTimestamp); + } + }, [videoTimestamp]); + + // Calculates the next vertical line + const lineXUpdate = (videoTimestamp: number) => { + if (chartRef.current === null || chartRef.current.series.length === 0) return; + + // TODO: Find a better base value for fileTimestamp + let fileTimestamp = -Infinity; + + const visibleSeries = chartRef.current.series.filter(series => series.visible); + if (visibleSeries.length === 0) return; + + // Gets the first file timestamp that is not undefined + visibleSeries.some(series => { + if (chartRef.current === null) return; + const seriesIndex = chartRef.current.series.indexOf(series); + fileTimestamp = getFileTimestamp(videoTimestamp, offsets[seriesIndex], timestamps[seriesIndex]); + return fileTimestamp !== undefined; + }); + + // Updates the lineX value with the new file timestamp + const newLineX = Math.floor(fileTimestamp); + if (fileTimestamp !== undefined) setLineX(newLineX); else return; + + // Finds the closest value to the new lineX value for each series + // Skips the series whose x values do not contain the new lineX value in their domain + const values = visibleSeries.flatMap(series => { + if (newLineX < series.xData[0] || newLineX > series.xData[series.xData.length - 1]) return []; + const closestXIndex = binarySearchClosest(series.xData, newLineX); + return [{ name: series.name, y: series.yData[closestXIndex] }]; + }); + + // Updates the value box with the found values + const tempValueLines = ['Timestamp: ' + new Date(newLineX).toUTCString()]; + chartRef.current.series.forEach(series => { + const value = values.find(value => value.name === series.name); + if (value === undefined) return; + tempValueLines.push(`${series.name}: ${value.y}`); + }); + setSyncedDataPoints(tempValueLines); + }; + + // Calculates the next vertical and horizontal lines + const linePointUpdate = (videoTimestamp: number) => { + // Finds the matching point index for the first visible series using the video timestamp + if (chartRef.current === null || chartRef.current.series.length === 0) return; + //TODO: Update this null check to be inline and return the right case + const visibleSeries = chartRef.current.series.filter(series => series.visible); + if (visibleSeries.length === 0) return; + const firstVisibleSeries = visibleSeries[0]; + const seriesIndex = chartRef.current.series.indexOf(firstVisibleSeries); + const pointIndex = getPointIndex( + firstVisibleSeries, + videoTimestamp, + offsets[seriesIndex], + timestamps[seriesIndex] + ); + + if (pointIndex >= 0) { + setLinePoint({x: firstVisibleSeries.xData[pointIndex], y: firstVisibleSeries.yData[pointIndex]}); + } else return; + + // Finds the point index for all the other visible series + const values = [ + { + name: firstVisibleSeries.name, + x: firstVisibleSeries.xData[pointIndex], + y: firstVisibleSeries.yData[pointIndex] + }, ...visibleSeries.slice(1).map(series => { + if (chartRef.current === null || chartRef.current.series.length === 0) throw new Error('Chart is not initialized'); + const seriesIndex = chartRef.current.series.indexOf(series); + const pointIndex = getPointIndex( + series, + videoTimestamp, + offsets[seriesIndex], + timestamps[seriesIndex] + ); + if (pointIndex >= 0) { + return {name: series.name, x: series.xData[pointIndex], y: series.yData[pointIndex]}; + } + }) + ]; + + // Updates the value box with the found values + const tempValueLines: string[] = []; + chartRef.current.series.forEach(series => { + const value = values.find(value => value.name === series.name); + if (value === undefined) return; + tempValueLines.push(`${series.name}: (${value.x.toFixed(5)}, ${value.y.toFixed(5)})`); + }); + setSyncedDataPoints(tempValueLines); + }; + + return { lineX, linePoint, syncedDataPoints }; + +} \ No newline at end of file From a1140b60a113ac94c5af9186f8949a1a8afc63b4 Mon Sep 17 00:00:00 2001 From: Kai Arseneau Date: Fri, 4 Oct 2024 16:32:16 -0400 Subject: [PATCH 12/19] Cleanup --- front-end/src/components/modal/download/DownloadModal.jsx | 3 +-- front-end/src/components/views/Chart/Chart.tsx | 3 --- front-end/src/components/views/Chart/useChartData.tsx | 2 -- front-end/src/components/views/Chart/useVideoSyncLines.tsx | 1 - front-end/src/components/views/VideoPlayer/VideoPlayer.jsx | 1 - 5 files changed, 1 insertion(+), 9 deletions(-) diff --git a/front-end/src/components/modal/download/DownloadModal.jsx b/front-end/src/components/modal/download/DownloadModal.jsx index ffb3459c..c14f7ed6 100644 --- a/front-end/src/components/modal/download/DownloadModal.jsx +++ b/front-end/src/components/modal/download/DownloadModal.jsx @@ -38,8 +38,7 @@ export const DownloadModal = ({ setModal }) => { // Add each selected file to the zip archive for (const file of selectedFiles) { - const response = await ApiUtil.getFile(file.key); - const blob = await response.blob(); + const blob = await ApiUtil.getFile(file.key); // Add the file to the zip archive with the file name as the key zip.file(file.key, blob); diff --git a/front-end/src/components/views/Chart/Chart.tsx b/front-end/src/components/views/Chart/Chart.tsx index 34a842c6..96e56ca9 100644 --- a/front-end/src/components/views/Chart/Chart.tsx +++ b/front-end/src/components/views/Chart/Chart.tsx @@ -6,7 +6,6 @@ import Highcharts from 'highcharts'; import HighchartsReact from 'highcharts-react-official'; import Boost from 'highcharts/modules/boost'; import HighchartsColorAxis from 'highcharts/modules/coloraxis'; -import { computeOffsets, getFileTimestamp, getPointIndex, binarySearchClosest} from '@lib/videoUtils'; import { useResizeDetector } from 'react-resize-detector'; import loadingImg from '@assets/loading.gif'; import { FileTimespan } from '@lib/apiUtils'; @@ -18,8 +17,6 @@ import { useVideoSyncLines } from './useVideoSyncLines'; // eslint-disable-next-line @typescript-eslint/no-require-imports require('highcharts-multicolor-series')(Highcharts); -import { Series } from 'highcharts'; - HighchartsColorAxis(Highcharts); Boost(Highcharts); diff --git a/front-end/src/components/views/Chart/useChartData.tsx b/front-end/src/components/views/Chart/useChartData.tsx index 22fe7a62..59391609 100644 --- a/front-end/src/components/views/Chart/useChartData.tsx +++ b/front-end/src/components/views/Chart/useChartData.tsx @@ -23,8 +23,6 @@ export const useChartData = (chartInformation: chartInformation) => { const { hasGPSTime, hasTimestampX, type } = chartInformation - console.log(chartInformation) - for (const file of chartInformation.files) { const { columns, analyze } = file; diff --git a/front-end/src/components/views/Chart/useVideoSyncLines.tsx b/front-end/src/components/views/Chart/useVideoSyncLines.tsx index b1e6dbcd..7c04ac10 100644 --- a/front-end/src/components/views/Chart/useVideoSyncLines.tsx +++ b/front-end/src/components/views/Chart/useVideoSyncLines.tsx @@ -132,5 +132,4 @@ export const useVideoSyncLines = ( }; return { lineX, linePoint, syncedDataPoints }; - } \ No newline at end of file diff --git a/front-end/src/components/views/VideoPlayer/VideoPlayer.jsx b/front-end/src/components/views/VideoPlayer/VideoPlayer.jsx index 20944ce7..8b4c7e23 100644 --- a/front-end/src/components/views/VideoPlayer/VideoPlayer.jsx +++ b/front-end/src/components/views/VideoPlayer/VideoPlayer.jsx @@ -10,7 +10,6 @@ const VideoPlayer = ({ video, setVideoTimestamp }) => { useEffect(() => { // Fetch data when the component mounts ApiUtil.getFile(video.key) - .then((response) => response.blob()) .then((blob) => { const url = URL.createObjectURL(blob); setVideoURL(url); From 936e6e674cd8e8e3ee9493110400572335c4ec89 Mon Sep 17 00:00:00 2001 From: Kai Arseneau Date: Fri, 4 Oct 2024 16:49:08 -0400 Subject: [PATCH 13/19] Converted videoUtils --- .../src/components/views/Chart/Chart.tsx | 8 +--- .../components/views/Chart/useChartData.tsx | 4 +- .../views/Chart/useVideoSyncLines.tsx | 4 +- front-end/src/lib/chartOptions.ts | 4 +- front-end/src/lib/chartUtils.ts | 42 +++++++++---------- .../src/lib/{videoUtils.js => videoUtils.ts} | 32 ++++++++------ 6 files changed, 48 insertions(+), 46 deletions(-) rename front-end/src/lib/{videoUtils.js => videoUtils.ts} (70%) diff --git a/front-end/src/components/views/Chart/Chart.tsx b/front-end/src/components/views/Chart/Chart.tsx index 96e56ca9..573b37f0 100644 --- a/front-end/src/components/views/Chart/Chart.tsx +++ b/front-end/src/components/views/Chart/Chart.tsx @@ -10,7 +10,7 @@ import { useResizeDetector } from 'react-resize-detector'; import loadingImg from '@assets/loading.gif'; import { FileTimespan } from '@lib/apiUtils'; import { Chart as ChartType } from 'highcharts'; -import { chartInformation } from '@lib/chartUtils'; +import { ChartInformation } from '@lib/chartUtils'; import { useChartData } from './useChartData'; import { useVideoSyncLines } from './useVideoSyncLines'; // TODO: Fix this import (Why is it different?) . Currently no ECMA module Womp Womp @@ -21,24 +21,20 @@ HighchartsColorAxis(Highcharts); Boost(Highcharts); interface ChartProps { - chartInformation: chartInformation; + chartInformation: ChartInformation; video: FileTimespan; videoTimestamp: number; } const Chart = ({ chartInformation, video, videoTimestamp }: ChartProps) => { - const chartRef = useRef(null); const [chartOptions, setChartOptions] = useState(defaultChartOptions); - const { parsedData, fileNames, timestamps, minMax, loading, refetch } = useChartData(chartInformation); const { lineX, linePoint, syncedDataPoints } = useVideoSyncLines(chartInformation, chartRef, videoTimestamp, video, timestamps); - // Once necessary data is fetched, format it for the chart useEffect(() => { if(!validateChartInformation(chartInformation)) return; - // Update the chart options with the new data setChartOptions((prevState) => { return { ...prevState, diff --git a/front-end/src/components/views/Chart/useChartData.tsx b/front-end/src/components/views/Chart/useChartData.tsx index 59391609..f3487c9a 100644 --- a/front-end/src/components/views/Chart/useChartData.tsx +++ b/front-end/src/components/views/Chart/useChartData.tsx @@ -2,9 +2,9 @@ import { useState, useEffect, useCallback, useRef } from 'react'; import { ApiUtil, MinMax } from '@lib/apiUtils'; import { getHeadersIndex, getTimestampOffset, getTimestamps, HUE_MAX, HUE_MIN, validateChartInformation } from '@lib/chartUtils'; import { seriesData } from '@lib/chartUtils'; -import { chartInformation } from '@lib/chartUtils'; +import { ChartInformation } from '@lib/chartUtils'; -export const useChartData = (chartInformation: chartInformation) => { +export const useChartData = (chartInformation: ChartInformation) => { const [parsedData, setParsedData] = useState([]); const [fileNames, setFileNames] = useState([]); const [timestamps, setTimestamps] = useState([]); diff --git a/front-end/src/components/views/Chart/useVideoSyncLines.tsx b/front-end/src/components/views/Chart/useVideoSyncLines.tsx index 7c04ac10..fa11a14f 100644 --- a/front-end/src/components/views/Chart/useVideoSyncLines.tsx +++ b/front-end/src/components/views/Chart/useVideoSyncLines.tsx @@ -1,11 +1,11 @@ -import { chartInformation, validateChartInformation } from '@lib/chartUtils'; +import { ChartInformation, validateChartInformation } from '@lib/chartUtils'; import { useState, useEffect } from 'react'; import { Chart } from 'highcharts'; import { FileTimespan } from '@lib/apiUtils'; import { computeOffsets, getFileTimestamp, getPointIndex, binarySearchClosest} from '@lib/videoUtils'; export const useVideoSyncLines = ( - chartInformation: chartInformation, + chartInformation: ChartInformation, chartRef: React.RefObject, videoTimestamp: number, videoTimespan: FileTimespan, diff --git a/front-end/src/lib/chartOptions.ts b/front-end/src/lib/chartOptions.ts index beb79c7a..3a86a5ea 100644 --- a/front-end/src/lib/chartOptions.ts +++ b/front-end/src/lib/chartOptions.ts @@ -1,5 +1,5 @@ import { Options } from 'highcharts'; -import { chartInformation } from './chartUtils'; +import { ChartInformation } from './chartUtils'; export const defaultChartOptions: Options = { chart: { @@ -30,7 +30,7 @@ export const defaultChartOptions: Options = { } }; -const getStandardChartConfig = (chartInformation: chartInformation) => { +const getStandardChartConfig = (chartInformation: ChartInformation) => { const chartConfig = defaultChartOptions; diff --git a/front-end/src/lib/chartUtils.ts b/front-end/src/lib/chartUtils.ts index d3fe5a47..340a00c3 100644 --- a/front-end/src/lib/chartUtils.ts +++ b/front-end/src/lib/chartUtils.ts @@ -1,20 +1,20 @@ import { AnalyzerType } from './apiUtils'; -export interface chartInformation { +export interface ChartInformation { files: { - columns: column[], + columns: Column[]; analyze: { - type: AnalyzerType, - analyzerValues: string[] + type: AnalyzerType; + analyzerValues: string[]; } - }[], - live: boolean, - type: string //TODO: UPDATE TO ENUM - hasGPSTime: boolean - hasTimestampX: boolean + }[]; + live: boolean; + type: string; + hasGPSTime: boolean; + hasTimestampX: boolean; } -interface column { +interface Column { header: string; filename: string; timespan: { @@ -23,28 +23,28 @@ interface column { } } -export const HUE_MIN = 150; -export const HUE_MAX = 0; -export const LIVE_DATA_INTERVAL = 300; - -interface colourSeriesData { +interface ColourSeriesData { x: number; y: number; colorValue: number; segmentColor: string; } -interface headersIndex { +interface HeadersIndex { x: number; y: number; colour: number; } -export type seriesData = colourSeriesData[] | number[][]; +export type seriesData = ColourSeriesData[] | number[][]; + +export const HUE_MIN = 150; +export const HUE_MAX = 0; +export const LIVE_DATA_INTERVAL = 300; // Calculates the offset required to convert the x values to unix timestamps // Adding the timestampOffset results in the x value being a the start time unix millis + millis since first timestamp -export const getTimestampOffset = (columns: column[], lines: string[][], headerIndices: headersIndex): number => { +export const getTimestampOffset = (columns: Column[], lines: string[][], headerIndices: HeadersIndex): number => { // Offset is the start time in unix millis minus the first timestamp in the file return new Date(columns[headerIndices.x].timespan.start + 'Z').getTime() - parseFloat(lines[0][headerIndices.x]); }; @@ -59,8 +59,8 @@ export const getTimestamps = async (text: string) => { * @description Matches headers to columns to get the indices of the columns in the headers array. * @returns {Object} An object with the indices of the columns in the headers array. The keys are 'x', 'y', and 'colour' */ -export const getHeadersIndex = (headers: string[], columns: column[]): headersIndex => { - const h: headersIndex = { x: -1, y: -1, colour: -1 }; +export const getHeadersIndex = (headers: string[], columns: Column[]): HeadersIndex => { + const h: HeadersIndex = { x: -1, y: -1, colour: -1 }; for (let i = 0; i < columns.length; i++) { for (let j = 0; j < headers.length; j++) { if (columns[i].header === headers[j].trim()) { @@ -77,7 +77,7 @@ export const getHeadersIndex = (headers: string[], columns: column[]): headersIn return h; }; -export const validateChartInformation = (chartInformation: chartInformation): boolean => { +export const validateChartInformation = (chartInformation: ChartInformation): boolean => { if (!chartInformation) { return false; } diff --git a/front-end/src/lib/videoUtils.js b/front-end/src/lib/videoUtils.ts similarity index 70% rename from front-end/src/lib/videoUtils.js rename to front-end/src/lib/videoUtils.ts index 08faec3d..3476fec1 100644 --- a/front-end/src/lib/videoUtils.js +++ b/front-end/src/lib/videoUtils.ts @@ -1,8 +1,12 @@ +import { FileInformation, FileTimespan } from "./apiUtils"; +import { ChartInformation } from "./chartUtils"; +import { Series } from "highcharts"; + // Computs the offsets between the videoStart and the fileStart for all series -export const computeOffsets = (chartInformation, video) => { - const videoStart = new Date(video.start).getTime(); - - const tempOffsets = []; +export const computeOffsets = (chartInformation: ChartInformation, videoTimespan: FileTimespan) => { + const videoStart = new Date(videoTimespan.start).getTime(); + + const tempOffsets: number[] = []; chartInformation.files.forEach(file => { const fileStart = new Date(file.columns[0].timespan.start).getTime(); // Unix date of first timestamp in file tempOffsets.push(videoStart - fileStart); @@ -10,7 +14,7 @@ export const computeOffsets = (chartInformation, video) => { return tempOffsets; }; -export const getPointIndex = (series, videoTimestamp, offset, timestamps) => { +export const getPointIndex = (series: Series, videoTimestamp: number, offset: number, timestamps: number[]) => { const fileTimestamp = getFileTimestamp(videoTimestamp, offset, timestamps); if (fileTimestamp === undefined) return; const timestampIndex = findClosestTimestamp(fileTimestamp, timestamps); @@ -18,17 +22,18 @@ export const getPointIndex = (series, videoTimestamp, offset, timestamps) => { return pointIndex; }; -export const getFileTimestamp = (videoTimestamp, offset, timestamps) => { +export const getFileTimestamp = (videoTimestamp: number, offset: number, timestamps: number[]) => { const fileTimestamp = videoTimestamp + offset + timestamps[0]; if (fileTimestamp < timestamps[0] || fileTimestamp > timestamps[timestamps.length - 1]) return; return fileTimestamp; }; // Filters the given list of files to only include those that have timespans that overlap with the video -export const filterFiles = (video, files, fileTimespans) => { - const videoSyncFiles = []; - const videoStart = new Date (video.start); - const videoEnd = new Date (video.end); +// TODO: Check types on these +export const filterFiles = (videoTimespan: FileTimespan, files: FileInformation[], fileTimespans: FileTimespan[]) => { + const videoSyncFiles: FileInformation[] = []; + const videoStart = new Date (videoTimespan.start); + const videoEnd = new Date (videoTimespan.end); files.forEach(file => { const fileTimespan = fileTimespans.find(timespan => timespan.key === file.key); if (fileTimespan === undefined) return; @@ -39,7 +44,7 @@ export const filterFiles = (video, files, fileTimespans) => { return videoSyncFiles; }; -export const binarySearchClosest = (arr, target) => { +export const binarySearchClosest = (arr: number[], target: number) => { let left = 0; let right = arr.length - 1; @@ -65,7 +70,7 @@ export const binarySearchClosest = (arr, target) => { }; // Finds the index of the timestamp in array that is closest to the timestamp provided -const findClosestTimestamp = (targetTimestamp, timestampArray) => { +const findClosestTimestamp = (targetTimestamp: number, timestampArray: number[]) => { const closestTimestamp = timestampArray.reduce((prev, curr) => { return Math.abs(curr - targetTimestamp) < Math.abs(prev - targetTimestamp) ? curr : prev; }); @@ -74,8 +79,9 @@ const findClosestTimestamp = (targetTimestamp, timestampArray) => { // Finds the index of the point of the on screen series // that matches with the point at the same index as the one provided -const findPointIndex = (timestampIndex, series) => { +const findPointIndex = (timestampIndex: number, series: Series) => { const timestampPoint = {x: series.xData[timestampIndex], y: series.yData[timestampIndex]}; const point = series.points.find(point => point.x === timestampPoint.x && point.y === timestampPoint.y); + if (point === undefined) return -1; return series.points.indexOf(point); }; \ No newline at end of file From 72c053b3c7cb23ec32e8c46598ea2b770bd9390f Mon Sep 17 00:00:00 2001 From: Kai Arseneau Date: Fri, 4 Oct 2024 16:52:40 -0400 Subject: [PATCH 14/19] Adding missed types --- front-end/src/components/views/Chart/useVideoSyncLines.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/front-end/src/components/views/Chart/useVideoSyncLines.tsx b/front-end/src/components/views/Chart/useVideoSyncLines.tsx index fa11a14f..6344d9bb 100644 --- a/front-end/src/components/views/Chart/useVideoSyncLines.tsx +++ b/front-end/src/components/views/Chart/useVideoSyncLines.tsx @@ -11,9 +11,9 @@ export const useVideoSyncLines = ( videoTimespan: FileTimespan, timestamps: number[][] ) => { - const [offsets, setOffsets] = useState([]); - const [lineX, setLineX] = useState(0); - const [linePoint, setLinePoint] = useState({x: 0, y: 0}); + const [offsets, setOffsets] = useState([]); + const [lineX, setLineX] = useState(0); + const [linePoint, setLinePoint] = useState<{x: number, y: number}>({x: 0, y: 0}); const [syncedDataPoints, setSyncedDataPoints] = useState([]); const resetData = () => { From c7c97f58e4b4293e49dc80e400e431ef25dd3518 Mon Sep 17 00:00:00 2001 From: Kai Arseneau Date: Sun, 13 Oct 2024 16:55:00 -0400 Subject: [PATCH 15/19] Fixing ts errors (the stupid way) --- front-end/src/components/views/Chart/Chart.tsx | 7 +++++++ .../views/Chart/useVideoSyncLines.tsx | 18 ++++++++++++------ front-end/src/lib/videoUtils.ts | 9 +++++---- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/front-end/src/components/views/Chart/Chart.tsx b/front-end/src/components/views/Chart/Chart.tsx index 573b37f0..ce1631d5 100644 --- a/front-end/src/components/views/Chart/Chart.tsx +++ b/front-end/src/components/views/Chart/Chart.tsx @@ -80,6 +80,13 @@ const Chart = ({ chartInformation, video, videoTimestamp }: ChartProps) => { refreshRate: 100, }); + useEffect(() => { + // chartRef.current?.axes[0].series[0].points gives only visible points + // chartRef.current?.axes[0].series[0].options.data is not typescript valid + console.log(chartRef.current?.series[0].points); + + }, [chartOptions]); + return (
{syncedDataPoints.length > 0 ? (
{syncedDataPoints.join('\n')}
) : null} diff --git a/front-end/src/components/views/Chart/useVideoSyncLines.tsx b/front-end/src/components/views/Chart/useVideoSyncLines.tsx index 6344d9bb..4fe8a1db 100644 --- a/front-end/src/components/views/Chart/useVideoSyncLines.tsx +++ b/front-end/src/components/views/Chart/useVideoSyncLines.tsx @@ -1,9 +1,14 @@ import { ChartInformation, validateChartInformation } from '@lib/chartUtils'; import { useState, useEffect } from 'react'; -import { Chart } from 'highcharts'; +import { Chart, Series } from 'highcharts'; import { FileTimespan } from '@lib/apiUtils'; import { computeOffsets, getFileTimestamp, getPointIndex, binarySearchClosest} from '@lib/videoUtils'; +export interface ExtSeries extends Series { + xData: number[]; + yData: number[]; +} + export const useVideoSyncLines = ( chartInformation: ChartInformation, chartRef: React.RefObject, @@ -47,7 +52,8 @@ export const useVideoSyncLines = ( // TODO: Find a better base value for fileTimestamp let fileTimestamp = -Infinity; - const visibleSeries = chartRef.current.series.filter(series => series.visible); + // TODO: This ExtSeries is yucky + const visibleSeries = chartRef.current.series.filter(series => series.visible) as ExtSeries[]; if (visibleSeries.length === 0) return; // Gets the first file timestamp that is not undefined @@ -85,7 +91,7 @@ export const useVideoSyncLines = ( // Finds the matching point index for the first visible series using the video timestamp if (chartRef.current === null || chartRef.current.series.length === 0) return; //TODO: Update this null check to be inline and return the right case - const visibleSeries = chartRef.current.series.filter(series => series.visible); + const visibleSeries = chartRef.current.series.filter(series => series.visible) as ExtSeries[]; if (visibleSeries.length === 0) return; const firstVisibleSeries = visibleSeries[0]; const seriesIndex = chartRef.current.series.indexOf(firstVisibleSeries); @@ -96,7 +102,7 @@ export const useVideoSyncLines = ( timestamps[seriesIndex] ); - if (pointIndex >= 0) { + if (pointIndex && pointIndex >= 0) { setLinePoint({x: firstVisibleSeries.xData[pointIndex], y: firstVisibleSeries.yData[pointIndex]}); } else return; @@ -115,7 +121,7 @@ export const useVideoSyncLines = ( offsets[seriesIndex], timestamps[seriesIndex] ); - if (pointIndex >= 0) { + if (pointIndex && pointIndex >= 0) { return {name: series.name, x: series.xData[pointIndex], y: series.yData[pointIndex]}; } }) @@ -124,7 +130,7 @@ export const useVideoSyncLines = ( // Updates the value box with the found values const tempValueLines: string[] = []; chartRef.current.series.forEach(series => { - const value = values.find(value => value.name === series.name); + const value = values.find(value => value?.name === series.name); if (value === undefined) return; tempValueLines.push(`${series.name}: (${value.x.toFixed(5)}, ${value.y.toFixed(5)})`); }); diff --git a/front-end/src/lib/videoUtils.ts b/front-end/src/lib/videoUtils.ts index 3476fec1..566dadd5 100644 --- a/front-end/src/lib/videoUtils.ts +++ b/front-end/src/lib/videoUtils.ts @@ -1,6 +1,6 @@ +import { ExtSeries } from "@components/views/Chart/useVideoSyncLines"; import { FileInformation, FileTimespan } from "./apiUtils"; import { ChartInformation } from "./chartUtils"; -import { Series } from "highcharts"; // Computs the offsets between the videoStart and the fileStart for all series export const computeOffsets = (chartInformation: ChartInformation, videoTimespan: FileTimespan) => { @@ -14,7 +14,7 @@ export const computeOffsets = (chartInformation: ChartInformation, videoTimespan return tempOffsets; }; -export const getPointIndex = (series: Series, videoTimestamp: number, offset: number, timestamps: number[]) => { +export const getPointIndex = (series: ExtSeries, videoTimestamp: number, offset: number, timestamps: number[]) => { const fileTimestamp = getFileTimestamp(videoTimestamp, offset, timestamps); if (fileTimestamp === undefined) return; const timestampIndex = findClosestTimestamp(fileTimestamp, timestamps); @@ -22,9 +22,10 @@ export const getPointIndex = (series: Series, videoTimestamp: number, offset: nu return pointIndex; }; +// TODO: Error handling instead of null return here? export const getFileTimestamp = (videoTimestamp: number, offset: number, timestamps: number[]) => { const fileTimestamp = videoTimestamp + offset + timestamps[0]; - if (fileTimestamp < timestamps[0] || fileTimestamp > timestamps[timestamps.length - 1]) return; + if (fileTimestamp < timestamps[0] || fileTimestamp > timestamps[timestamps.length - 1]) throw new Error('Timestamp out of bounds'); return fileTimestamp; }; @@ -79,7 +80,7 @@ const findClosestTimestamp = (targetTimestamp: number, timestampArray: number[]) // Finds the index of the point of the on screen series // that matches with the point at the same index as the one provided -const findPointIndex = (timestampIndex: number, series: Series) => { +const findPointIndex = (timestampIndex: number, series: ExtSeries) => { const timestampPoint = {x: series.xData[timestampIndex], y: series.yData[timestampIndex]}; const point = series.points.find(point => point.x === timestampPoint.x && point.y === timestampPoint.y); if (point === undefined) return -1; From bc9943e13797c1422d95c7ffb4750c07cde7a6c4 Mon Sep 17 00:00:00 2001 From: Kai Arseneau Date: Sun, 13 Oct 2024 16:57:39 -0400 Subject: [PATCH 16/19] Linting --- .../com/mcmasterbaja/services/DefaultFileMetadataService.java | 1 - .../java/com/mcmasterbaja/services/FileMetadataService.java | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/src/main/java/com/mcmasterbaja/services/DefaultFileMetadataService.java b/backend/src/main/java/com/mcmasterbaja/services/DefaultFileMetadataService.java index da8fc23b..aa88edc5 100644 --- a/backend/src/main/java/com/mcmasterbaja/services/DefaultFileMetadataService.java +++ b/backend/src/main/java/com/mcmasterbaja/services/DefaultFileMetadataService.java @@ -7,7 +7,6 @@ import com.mcmasterbaja.exceptions.MalformedCsvException; import com.mcmasterbaja.exceptions.StorageException; import com.mcmasterbaja.model.MinMax; - import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import java.io.BufferedReader; diff --git a/backend/src/main/java/com/mcmasterbaja/services/FileMetadataService.java b/backend/src/main/java/com/mcmasterbaja/services/FileMetadataService.java index cd0aa312..9f839275 100644 --- a/backend/src/main/java/com/mcmasterbaja/services/FileMetadataService.java +++ b/backend/src/main/java/com/mcmasterbaja/services/FileMetadataService.java @@ -1,10 +1,9 @@ package com.mcmasterbaja.services; +import com.mcmasterbaja.model.MinMax; import java.nio.file.Path; import java.time.LocalDateTime; -import com.mcmasterbaja.model.MinMax; - public interface FileMetadataService { /** From c49dfa0aa0bfc0055babba1d92f64fb7e0a399fc Mon Sep 17 00:00:00 2001 From: Kai Arseneau Date: Sun, 13 Oct 2024 17:23:13 -0400 Subject: [PATCH 17/19] Centralize types --- .../src/components/views/Chart/Chart.tsx | 3 +- .../components/views/Chart/useChartData.tsx | 5 +- .../views/Chart/useVideoSyncLines.tsx | 11 ++--- front-end/src/lib/apiUtils.ts | 32 +------------ front-end/src/lib/chartOptions.ts | 2 +- front-end/src/lib/chartUtils.ts | 40 +--------------- front-end/src/lib/videoUtils.ts | 4 +- front-end/src/types/ApiTypes.ts | 30 ++++++++++++ front-end/src/types/ChartInformation.ts | 47 +++++++++++++++++++ front-end/src/types/index.ts | 2 + front-end/tsconfig.json | 3 +- front-end/vite.config.ts | 1 + 12 files changed, 92 insertions(+), 88 deletions(-) create mode 100644 front-end/src/types/ApiTypes.ts create mode 100644 front-end/src/types/ChartInformation.ts create mode 100644 front-end/src/types/index.ts diff --git a/front-end/src/components/views/Chart/Chart.tsx b/front-end/src/components/views/Chart/Chart.tsx index ce1631d5..8a0cd7c5 100644 --- a/front-end/src/components/views/Chart/Chart.tsx +++ b/front-end/src/components/views/Chart/Chart.tsx @@ -8,9 +8,8 @@ import Boost from 'highcharts/modules/boost'; import HighchartsColorAxis from 'highcharts/modules/coloraxis'; import { useResizeDetector } from 'react-resize-detector'; import loadingImg from '@assets/loading.gif'; -import { FileTimespan } from '@lib/apiUtils'; +import { FileTimespan, ChartInformation } from '@types'; import { Chart as ChartType } from 'highcharts'; -import { ChartInformation } from '@lib/chartUtils'; import { useChartData } from './useChartData'; import { useVideoSyncLines } from './useVideoSyncLines'; // TODO: Fix this import (Why is it different?) . Currently no ECMA module Womp Womp diff --git a/front-end/src/components/views/Chart/useChartData.tsx b/front-end/src/components/views/Chart/useChartData.tsx index f3487c9a..25e8904a 100644 --- a/front-end/src/components/views/Chart/useChartData.tsx +++ b/front-end/src/components/views/Chart/useChartData.tsx @@ -1,8 +1,7 @@ import { useState, useEffect, useCallback, useRef } from 'react'; -import { ApiUtil, MinMax } from '@lib/apiUtils'; +import { ApiUtil } from '@lib/apiUtils'; import { getHeadersIndex, getTimestampOffset, getTimestamps, HUE_MAX, HUE_MIN, validateChartInformation } from '@lib/chartUtils'; -import { seriesData } from '@lib/chartUtils'; -import { ChartInformation } from '@lib/chartUtils'; +import { ChartInformation, seriesData, MinMax } from '@types'; export const useChartData = (chartInformation: ChartInformation) => { const [parsedData, setParsedData] = useState([]); diff --git a/front-end/src/components/views/Chart/useVideoSyncLines.tsx b/front-end/src/components/views/Chart/useVideoSyncLines.tsx index 4fe8a1db..e871bed6 100644 --- a/front-end/src/components/views/Chart/useVideoSyncLines.tsx +++ b/front-end/src/components/views/Chart/useVideoSyncLines.tsx @@ -1,14 +1,9 @@ -import { ChartInformation, validateChartInformation } from '@lib/chartUtils'; +import { validateChartInformation } from '@lib/chartUtils'; import { useState, useEffect } from 'react'; -import { Chart, Series } from 'highcharts'; -import { FileTimespan } from '@lib/apiUtils'; +import { Chart } from 'highcharts'; +import { FileTimespan, ChartInformation, ExtSeries } from '@types'; import { computeOffsets, getFileTimestamp, getPointIndex, binarySearchClosest} from '@lib/videoUtils'; -export interface ExtSeries extends Series { - xData: number[]; - yData: number[]; -} - export const useVideoSyncLines = ( chartInformation: ChartInformation, chartRef: React.RefObject, diff --git a/front-end/src/lib/apiUtils.ts b/front-end/src/lib/apiUtils.ts index cbec431c..f7d29da7 100644 --- a/front-end/src/lib/apiUtils.ts +++ b/front-end/src/lib/apiUtils.ts @@ -1,34 +1,4 @@ -// Some defined API types - -export interface FileInformation { - key: string; - fileHeaders: string[]; - size: number; -} - -export interface FileTimespan { - key: string; - start: Date; - end: Date; -} - -export interface MinMax { - min: number; - max: number; -} - -export enum AnalyzerType { - ACCEL_CURVE = 'ACCEL_CURVE', - AVERAGE = 'AVERAGE', - CUBIC = 'CUBIC', - LINEAR_INTERPOLATE = 'LINEAR_INTERPOLATE', - LINEAR_MULTIPLY = 'LINEAR_MULTIPLY', - INTERPOLATER_PRO = 'INTERPOLATER_PRO', - RDP_COMPRESSION = 'RDP_COMPRESSION', - ROLL_AVG = 'ROLL_AVG', - SGOLAY = 'SGOLAY', - SPLIT = 'SPLIT' -} +import { AnalyzerType, FileInformation, FileTimespan, MinMax } from "@types"; export const ApiUtil = { diff --git a/front-end/src/lib/chartOptions.ts b/front-end/src/lib/chartOptions.ts index 3a86a5ea..c6e64295 100644 --- a/front-end/src/lib/chartOptions.ts +++ b/front-end/src/lib/chartOptions.ts @@ -1,5 +1,5 @@ import { Options } from 'highcharts'; -import { ChartInformation } from './chartUtils'; +import { ChartInformation } from '@types'; export const defaultChartOptions: Options = { chart: { diff --git a/front-end/src/lib/chartUtils.ts b/front-end/src/lib/chartUtils.ts index 340a00c3..3f4843fc 100644 --- a/front-end/src/lib/chartUtils.ts +++ b/front-end/src/lib/chartUtils.ts @@ -1,42 +1,4 @@ -import { AnalyzerType } from './apiUtils'; - -export interface ChartInformation { - files: { - columns: Column[]; - analyze: { - type: AnalyzerType; - analyzerValues: string[]; - } - }[]; - live: boolean; - type: string; - hasGPSTime: boolean; - hasTimestampX: boolean; -} - -interface Column { - header: string; - filename: string; - timespan: { - start: Date; - end: Date; - } -} - -interface ColourSeriesData { - x: number; - y: number; - colorValue: number; - segmentColor: string; -} - -interface HeadersIndex { - x: number; - y: number; - colour: number; -} - -export type seriesData = ColourSeriesData[] | number[][]; +import { ChartInformation, Column, HeadersIndex } from "@types"; export const HUE_MIN = 150; export const HUE_MAX = 0; diff --git a/front-end/src/lib/videoUtils.ts b/front-end/src/lib/videoUtils.ts index 566dadd5..867b205c 100644 --- a/front-end/src/lib/videoUtils.ts +++ b/front-end/src/lib/videoUtils.ts @@ -1,6 +1,4 @@ -import { ExtSeries } from "@components/views/Chart/useVideoSyncLines"; -import { FileInformation, FileTimespan } from "./apiUtils"; -import { ChartInformation } from "./chartUtils"; +import { FileInformation, FileTimespan, ChartInformation, ExtSeries } from "@types"; // Computs the offsets between the videoStart and the fileStart for all series export const computeOffsets = (chartInformation: ChartInformation, videoTimespan: FileTimespan) => { diff --git a/front-end/src/types/ApiTypes.ts b/front-end/src/types/ApiTypes.ts new file mode 100644 index 00000000..66863da4 --- /dev/null +++ b/front-end/src/types/ApiTypes.ts @@ -0,0 +1,30 @@ + +export interface FileInformation { + key: string; + fileHeaders: string[]; + size: number; +} + +export interface FileTimespan { + key: string; + start: Date; + end: Date; +} + +export interface MinMax { + min: number; + max: number; +} + +export enum AnalyzerType { + ACCEL_CURVE = 'ACCEL_CURVE', + AVERAGE = 'AVERAGE', + CUBIC = 'CUBIC', + LINEAR_INTERPOLATE = 'LINEAR_INTERPOLATE', + LINEAR_MULTIPLY = 'LINEAR_MULTIPLY', + INTERPOLATER_PRO = 'INTERPOLATER_PRO', + RDP_COMPRESSION = 'RDP_COMPRESSION', + ROLL_AVG = 'ROLL_AVG', + SGOLAY = 'SGOLAY', + SPLIT = 'SPLIT' +} \ No newline at end of file diff --git a/front-end/src/types/ChartInformation.ts b/front-end/src/types/ChartInformation.ts new file mode 100644 index 00000000..f370e852 --- /dev/null +++ b/front-end/src/types/ChartInformation.ts @@ -0,0 +1,47 @@ +import { AnalyzerType } from "@types"; +import { Series } from "highcharts"; + +export interface ChartInformation { + files: { + columns: Column[]; + analyze: { + type: AnalyzerType; + analyzerValues: string[]; + } + }[]; + live: boolean; + type: string; + hasGPSTime: boolean; + hasTimestampX: boolean; +} + +export interface Column { + header: string; + filename: string; + timespan: { + start: Date; + end: Date; + } +} + +export interface ColourSeriesData { + x: number; + y: number; + colorValue: number; + segmentColor: string; +} + +export interface HeadersIndex { + x: number; + y: number; + colour: number; +} + +export type seriesData = ColourSeriesData[] | number[][]; + +// Some highcharts bs here +// See https://www.highcharts.com/forum/viewtopic.php?t=52926 +export interface ExtSeries extends Series { + xData: number[]; + yData: number[]; +} \ No newline at end of file diff --git a/front-end/src/types/index.ts b/front-end/src/types/index.ts new file mode 100644 index 00000000..89573f86 --- /dev/null +++ b/front-end/src/types/index.ts @@ -0,0 +1,2 @@ +export * from './ApiTypes'; +export * from './ChartInformation'; \ No newline at end of file diff --git a/front-end/tsconfig.json b/front-end/tsconfig.json index d2b5871d..fdcf6d46 100644 --- a/front-end/tsconfig.json +++ b/front-end/tsconfig.json @@ -21,7 +21,8 @@ "@assets/*": ["assets/*"], "@components/*": ["components/*"], "@lib/*": ["lib/*"], - "@styles/*": ["styles/*"] + "@styles/*": ["styles/*"], + "@types": ["types/index.ts"], }, }, "include": ["src"], diff --git a/front-end/vite.config.ts b/front-end/vite.config.ts index 93ffaa88..bb594a39 100644 --- a/front-end/vite.config.ts +++ b/front-end/vite.config.ts @@ -23,6 +23,7 @@ export default defineConfig({ '@components': resolve(root, 'components'), '@lib': resolve(root, 'lib'), '@styles': resolve(root, 'styles'), + '@types': resolve(root, 'types/index.ts'), }, }, }); From eac21e6668b5deabf887bdeed3cbde8fa956e1bc Mon Sep 17 00:00:00 2001 From: Kai Arseneau Date: Sun, 13 Oct 2024 23:50:40 -0400 Subject: [PATCH 18/19] Cleaning up + adding linting rules --- front-end/eslint.config.mjs | 11 +++++++++++ front-end/src/components/Topbar/Topbar.tsx | 4 ++-- front-end/src/components/analyzerData.js | 1 + front-end/src/components/views/Chart/Chart.tsx | 15 +++++++-------- .../src/components/views/Chart/useChartData.tsx | 15 +++++++++++---- .../components/views/Chart/useVideoSyncLines.tsx | 8 +++++--- front-end/src/lib/apiUtils.ts | 2 +- front-end/src/lib/chartUtils.ts | 2 +- front-end/src/lib/videoUtils.ts | 7 ++++--- front-end/src/types/ChartInformation.ts | 4 ++-- 10 files changed, 45 insertions(+), 24 deletions(-) diff --git a/front-end/eslint.config.mjs b/front-end/eslint.config.mjs index fd901bc9..1849ca35 100644 --- a/front-end/eslint.config.mjs +++ b/front-end/eslint.config.mjs @@ -9,4 +9,15 @@ export default tseslint.config( eslint.configs.recommended, ...tseslint.configs.strict, ...tseslint.configs.stylistic, + {rules: { + 'semi': ['warn', 'always'], + 'max-len': ['warn', {code: 120}], + 'quotes': ['warn', 'single'], + 'no-console': 'warn', + 'indent': [ + 'error', + 2, + { 'SwitchCase': 1 } + ] + }} ); \ No newline at end of file diff --git a/front-end/src/components/Topbar/Topbar.tsx b/front-end/src/components/Topbar/Topbar.tsx index a8b242a2..6ddc9d05 100644 --- a/front-end/src/components/Topbar/Topbar.tsx +++ b/front-end/src/components/Topbar/Topbar.tsx @@ -19,9 +19,9 @@ const Topbar = ({ setModal, numViews, setNumViews }: TopbarProps) => { const beginLiveData = () => { ApiUtil.toggleLiveData('COM2').then((res) => { - console.log(res); + alert(res); }).catch((err) => { - console.log(err); + alert(err); }); if (liveStatus === false) { diff --git a/front-end/src/components/analyzerData.js b/front-end/src/components/analyzerData.js index cf5f7aff..ebbe5a60 100644 --- a/front-end/src/components/analyzerData.js +++ b/front-end/src/components/analyzerData.js @@ -1,3 +1,4 @@ +/* eslint-disable max-len */ // const images = {}; // const importAll = r => { // r.keys().forEach(key => images[key] = r(key)); diff --git a/front-end/src/components/views/Chart/Chart.tsx b/front-end/src/components/views/Chart/Chart.tsx index 8a0cd7c5..67ba083d 100644 --- a/front-end/src/components/views/Chart/Chart.tsx +++ b/front-end/src/components/views/Chart/Chart.tsx @@ -29,7 +29,13 @@ const Chart = ({ chartInformation, video, videoTimestamp }: ChartProps) => { const chartRef = useRef(null); const [chartOptions, setChartOptions] = useState(defaultChartOptions); const { parsedData, fileNames, timestamps, minMax, loading, refetch } = useChartData(chartInformation); - const { lineX, linePoint, syncedDataPoints } = useVideoSyncLines(chartInformation, chartRef, videoTimestamp, video, timestamps); + const { lineX, linePoint, syncedDataPoints } = useVideoSyncLines( + chartInformation, + chartRef, + videoTimestamp, + video, + timestamps + ); useEffect(() => { if(!validateChartInformation(chartInformation)) return; @@ -79,13 +85,6 @@ const Chart = ({ chartInformation, video, videoTimestamp }: ChartProps) => { refreshRate: 100, }); - useEffect(() => { - // chartRef.current?.axes[0].series[0].points gives only visible points - // chartRef.current?.axes[0].series[0].options.data is not typescript valid - console.log(chartRef.current?.series[0].points); - - }, [chartOptions]); - return (
{syncedDataPoints.length > 0 ? (
{syncedDataPoints.join('\n')}
) : null} diff --git a/front-end/src/components/views/Chart/useChartData.tsx b/front-end/src/components/views/Chart/useChartData.tsx index 25e8904a..02d2ceba 100644 --- a/front-end/src/components/views/Chart/useChartData.tsx +++ b/front-end/src/components/views/Chart/useChartData.tsx @@ -1,6 +1,13 @@ import { useState, useEffect, useCallback, useRef } from 'react'; import { ApiUtil } from '@lib/apiUtils'; -import { getHeadersIndex, getTimestampOffset, getTimestamps, HUE_MAX, HUE_MIN, validateChartInformation } from '@lib/chartUtils'; +import { + getHeadersIndex, + getTimestampOffset, + getTimestamps, + HUE_MAX, + HUE_MIN, + validateChartInformation +} from '@lib/chartUtils'; import { ChartInformation, seriesData, MinMax } from '@types'; export const useChartData = (chartInformation: ChartInformation) => { @@ -20,7 +27,7 @@ export const useChartData = (chartInformation: ChartInformation) => { const fetchChartData = useCallback(async () => { if (!validateChartInformation(chartInformation)) return; - const { hasGPSTime, hasTimestampX, type } = chartInformation + const { hasGPSTime, hasTimestampX, type } = chartInformation; for (const file of chartInformation.files) { const { columns, analyze } = file; @@ -77,7 +84,7 @@ export const useChartData = (chartInformation: ChartInformation) => { let timestamps: number[]; if (hasTimestampX) { // TODO: Fix this case, which seems to be an overlap of colour and syncing timestamps - timestamps = seriesData.map(item => item[0]) as number[] + timestamps = seriesData.map(item => item[0]) as number[]; } else { timestamps = await getTimestamps(text); } @@ -95,4 +102,4 @@ export const useChartData = (chartInformation: ChartInformation) => { }, [fetchChartData]); return { parsedData, fileNames, timestamps, minMax, loading, refetch: fetchChartData }; -} +}; diff --git a/front-end/src/components/views/Chart/useVideoSyncLines.tsx b/front-end/src/components/views/Chart/useVideoSyncLines.tsx index e871bed6..579ddc44 100644 --- a/front-end/src/components/views/Chart/useVideoSyncLines.tsx +++ b/front-end/src/components/views/Chart/useVideoSyncLines.tsx @@ -21,7 +21,7 @@ export const useVideoSyncLines = ( setLineX(0); setLinePoint({x: 0, y: 0}); setSyncedDataPoints([]); - } + }; // Do initial calculation when timespan or chartInformation change useEffect(() => { @@ -108,7 +108,9 @@ export const useVideoSyncLines = ( x: firstVisibleSeries.xData[pointIndex], y: firstVisibleSeries.yData[pointIndex] }, ...visibleSeries.slice(1).map(series => { - if (chartRef.current === null || chartRef.current.series.length === 0) throw new Error('Chart is not initialized'); + if (chartRef.current === null || chartRef.current.series.length === 0) { + throw new Error('Chart is not initialized'); + } const seriesIndex = chartRef.current.series.indexOf(series); const pointIndex = getPointIndex( series, @@ -133,4 +135,4 @@ export const useVideoSyncLines = ( }; return { lineX, linePoint, syncedDataPoints }; -} \ No newline at end of file +}; \ No newline at end of file diff --git a/front-end/src/lib/apiUtils.ts b/front-end/src/lib/apiUtils.ts index f7d29da7..bd3ff7bd 100644 --- a/front-end/src/lib/apiUtils.ts +++ b/front-end/src/lib/apiUtils.ts @@ -1,4 +1,4 @@ -import { AnalyzerType, FileInformation, FileTimespan, MinMax } from "@types"; +import { AnalyzerType, FileInformation, FileTimespan, MinMax } from '@types'; export const ApiUtil = { diff --git a/front-end/src/lib/chartUtils.ts b/front-end/src/lib/chartUtils.ts index 3f4843fc..3959c768 100644 --- a/front-end/src/lib/chartUtils.ts +++ b/front-end/src/lib/chartUtils.ts @@ -1,4 +1,4 @@ -import { ChartInformation, Column, HeadersIndex } from "@types"; +import { ChartInformation, Column, HeadersIndex } from '@types'; export const HUE_MIN = 150; export const HUE_MAX = 0; diff --git a/front-end/src/lib/videoUtils.ts b/front-end/src/lib/videoUtils.ts index 867b205c..d4f5a0c3 100644 --- a/front-end/src/lib/videoUtils.ts +++ b/front-end/src/lib/videoUtils.ts @@ -1,4 +1,4 @@ -import { FileInformation, FileTimespan, ChartInformation, ExtSeries } from "@types"; +import { FileInformation, FileTimespan, ChartInformation, ExtSeries } from '@types'; // Computs the offsets between the videoStart and the fileStart for all series export const computeOffsets = (chartInformation: ChartInformation, videoTimespan: FileTimespan) => { @@ -23,12 +23,13 @@ export const getPointIndex = (series: ExtSeries, videoTimestamp: number, offset: // TODO: Error handling instead of null return here? export const getFileTimestamp = (videoTimestamp: number, offset: number, timestamps: number[]) => { const fileTimestamp = videoTimestamp + offset + timestamps[0]; - if (fileTimestamp < timestamps[0] || fileTimestamp > timestamps[timestamps.length - 1]) throw new Error('Timestamp out of bounds'); + if (fileTimestamp < timestamps[0] || fileTimestamp > timestamps[timestamps.length - 1]) { + throw new Error('Timestamp out of bounds'); + } return fileTimestamp; }; // Filters the given list of files to only include those that have timespans that overlap with the video -// TODO: Check types on these export const filterFiles = (videoTimespan: FileTimespan, files: FileInformation[], fileTimespans: FileTimespan[]) => { const videoSyncFiles: FileInformation[] = []; const videoStart = new Date (videoTimespan.start); diff --git a/front-end/src/types/ChartInformation.ts b/front-end/src/types/ChartInformation.ts index f370e852..abae2540 100644 --- a/front-end/src/types/ChartInformation.ts +++ b/front-end/src/types/ChartInformation.ts @@ -1,5 +1,5 @@ -import { AnalyzerType } from "@types"; -import { Series } from "highcharts"; +import { AnalyzerType } from '@types'; +import { Series } from 'highcharts'; export interface ChartInformation { files: { From 26dcc05b31b96fabe7d9814877fe6b2524f0bdad Mon Sep 17 00:00:00 2001 From: Kai Arseneau Date: Mon, 14 Oct 2024 22:51:57 -0400 Subject: [PATCH 19/19] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9108f094..b0769b32 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ For an in-depth user guide, consult the McMaster Baja Wiki. ## How to run ### Required tools: -NodeJS and NPM -JDK 21+ +- NodeJS and NPM +- JDK 21+ ### To setup: