diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e076b2ae..132edb28 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,18 +15,32 @@ "@types/node": "^16.18.46", "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", + "axios": "^0.24.0", + "chart.js": "^3.8.0", + "d3": "^7.4.3", + "leaflet": "^1.9.4", "react": "^18.2.0", + "react-chartjs-2": "^4.2.0", "react-dom": "^18.2.0", + "react-gradient-hook": "^1.5.3", + "react-leaflet": "^4.2.1", + "react-leaflet-hotline": "^1.4.2", + "react-router-dom": "^6.15.0", "react-scripts": "5.0.1", + "react-slider": "^2.0.6", "typescript": "^4.9.5", "web-vitals": "^2.1.4" }, "devDependencies": { + "@types/d3": "^7.1.0", + "@types/leaflet": "^1.7.5", + "@types/react": "^17.0.24", + "@types/react-slider": "^1.3.1", "@typescript-eslint/parser": "^5.62.0", "eslint": "^8.48.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", - "prettier": "^3.0.3" + "prettier": "3.0.3" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -2400,6 +2414,14 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" }, + "node_modules/@icons/material": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz", + "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==", + "peerDependencies": { + "react": "*" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -3279,6 +3301,24 @@ } } }, + "node_modules/@react-leaflet/core": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz", + "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@remix-run/router": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.8.0.tgz", + "integrity": "sha512-mrfKqIHnSZRyIzBcanNJmVQELTnX+qagEDlcKO90RgRBVOZGSGvZKeDihTRfWcqoDn5N/NkUcwWTccnpN18Tfg==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -3991,6 +4031,259 @@ "@types/node": "*" } }, + "node_modules/@types/d3": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.0.tgz", + "integrity": "sha512-jIfNVK0ZlxcuRDKtRS/SypEyOQ6UHaFQBKv032X45VvxSJ6Yi5G9behy9h6tNTHTDGh5Vq+KbmBjUWLgY4meCA==", + "dev": true, + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.7.tgz", + "integrity": "sha512-4/Q0FckQ8TBjsB0VdGFemJOG8BLXUB2KKlL0VmZ+eOYeOnTb/wDRQqYWpBmQ6IlvWkXwkYiot+n9Px2aTJ7zGQ==", + "dev": true + }, + "node_modules/@types/d3-axis": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.3.tgz", + "integrity": "sha512-SE3x/pLO/+GIHH17mvs1uUVPkZ3bHquGzvZpPAh4yadRy71J93MJBpgK/xY8l9gT28yTN1g9v3HfGSFeBMmwZw==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.3.tgz", + "integrity": "sha512-MQ1/M/B5ifTScHSe5koNkhxn2mhUPqXjGuKjjVYckplAPjP9t2I2sZafb/YVHDwhoXWZoSav+Q726eIbN3qprA==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.3.tgz", + "integrity": "sha512-keuSRwO02c7PBV3JMWuctIfdeJrVFI7RpzouehvBWL4/GGUB3PBNg/9ZKPZAgJphzmS2v2+7vr7BGDQw1CAulw==", + "dev": true + }, + "node_modules/@types/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==", + "dev": true + }, + "node_modules/@types/d3-contour": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.3.tgz", + "integrity": "sha512-x7G/tdDZt4m09XZnG2SutbIuQqmkNYqR9uhDMdPlpJbcwepkEjEWG29euFcgVA1k6cn92CHdDL9Z+fOnxnbVQw==", + "dev": true, + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz", + "integrity": "sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==", + "dev": true + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.3.tgz", + "integrity": "sha512-Df7KW3Re7G6cIpIhQtqHin8yUxUHYAqiE41ffopbmU5+FifYUNV7RVyTg8rQdkEagg83m14QtS8InvNb95Zqug==", + "dev": true + }, + "node_modules/@types/d3-drag": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.3.tgz", + "integrity": "sha512-82AuQMpBQjuXeIX4tjCYfWjpm3g7aGCfx6dFlxX2JlRaiME/QWcHzBsINl7gbHCODA2anPYlL31/Trj/UnjK9A==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.2.tgz", + "integrity": "sha512-DooW5AOkj4AGmseVvbwHvwM/Ltu0Ks0WrhG6r5FG9riHT5oUUTHz6xHsHqJSVU8ZmPkOqlUEY2obS5C9oCIi2g==", + "dev": true + }, + "node_modules/@types/d3-ease": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.0.tgz", + "integrity": "sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==", + "dev": true + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.3.tgz", + "integrity": "sha512-/EsDKRiQkby3Z/8/AiZq8bsuLDo/tYHnNIZkUpSeEHWV7fHUl6QFBjvMPbhkKGk9jZutzfOkGygCV7eR/MkcXA==", + "dev": true, + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.5.tgz", + "integrity": "sha512-EGG+IWx93ESSXBwfh/5uPuR9Hp8M6o6qEGU7bBQslxCvrdUBQZha/EFpu/VMdLU4B0y4Oe4h175nSm7p9uqFug==", + "dev": true + }, + "node_modules/@types/d3-format": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz", + "integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==", + "dev": true + }, + "node_modules/@types/d3-geo": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.0.4.tgz", + "integrity": "sha512-kmUK8rVVIBPKJ1/v36bk2aSgwRj2N/ZkjDT+FkMT5pgedZoPlyhaG62J+9EgNIgUXE6IIL0b7bkLxCzhE6U4VQ==", + "dev": true, + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.3.tgz", + "integrity": "sha512-GpSK308Xj+HeLvogfEc7QsCOcIxkDwLhFYnOoohosEzOqv7/agxwvJER1v/kTC+CY1nfazR0F7gnHo7GE41/fw==", + "dev": true + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==", + "dev": true, + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz", + "integrity": "sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==", + "dev": true + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.0.tgz", + "integrity": "sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw==", + "dev": true + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz", + "integrity": "sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw==", + "dev": true + }, + "node_modules/@types/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ==", + "dev": true + }, + "node_modules/@types/d3-scale": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.4.tgz", + "integrity": "sha512-eq1ZeTj0yr72L8MQk6N6heP603ubnywSDRfNpi5enouR112HzGLS6RIvExCzZTraFF4HdzNpJMwA/zGiMoHUUw==", + "dev": true, + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw==", + "dev": true + }, + "node_modules/@types/d3-selection": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.6.tgz", + "integrity": "sha512-2ACr96USZVjXR9KMD9IWi1Epo4rSDKnUtYn6q2SPhYxykvXTw9vR77lkFNruXVg4i1tzQtBxeDMx0oNvJWbF1w==", + "dev": true + }, + "node_modules/@types/d3-shape": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.2.tgz", + "integrity": "sha512-NN4CXr3qeOUNyK5WasVUV8NCSAx/CRVcwcb0BuuS1PiTqwIm6ABi1SyasLZ/vsVCFDArF+W4QiGzSry1eKYQ7w==", + "dev": true, + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz", + "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==", + "dev": true + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.0.tgz", + "integrity": "sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw==", + "dev": true + }, + "node_modules/@types/d3-timer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz", + "integrity": "sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==", + "dev": true + }, + "node_modules/@types/d3-transition": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.4.tgz", + "integrity": "sha512-512a4uCOjUzsebydItSXsHrPeQblCVk8IKjqCUmrlvBWkkVh3donTTxmURDo1YPwIVDh5YVwCAO6gR4sgimCPQ==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.4.tgz", + "integrity": "sha512-cqkuY1ah9ZQre2POqjSLcM8g40UVya/qwEUrNYP2/rCVljbmqKCVcv+ebvwhlI5azIbSEL7m+os6n+WlYA43aA==", + "dev": true, + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/eslint": { "version": "8.44.2", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz", @@ -4036,6 +4329,12 @@ "@types/send": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.10", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", + "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==", + "dev": true + }, "node_modules/@types/graceful-fs": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", @@ -4102,6 +4401,15 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, + "node_modules/@types/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-kfwgQf4eOxoe/tD9CaKQrBKHbc7VpyfJOG5sxsQtkH+ML9xYa8hUC3UMa0wU1pKfciJtO0pU9g9XbWhPo7iBCA==", + "dev": true, + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -4143,9 +4451,9 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" }, "node_modules/@types/react": { - "version": "18.2.21", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.21.tgz", - "integrity": "sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==", + "version": "17.0.65", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.65.tgz", + "integrity": "sha512-oxur785xZYHvnI7TRS61dXbkIhDPnGfsXKv0cNXR/0ml4SipRIFpSMzA7HMEfOywFwJ5AOnPrXYTEiTRUQeGlQ==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -4160,6 +4468,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-slider": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@types/react-slider/-/react-slider-1.3.1.tgz", + "integrity": "sha512-4X2yK7RyCIy643YCFL+bc6XNmcnBtt8n88uuyihvcn5G7Lut23eNQU3q3KmwF7MWIfKfsW5NxCjw0SeDZRtgaA==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -5095,6 +5412,14 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", + "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", + "dependencies": { + "follow-redirects": "^1.14.4" + } + }, "node_modules/axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -5730,6 +6055,11 @@ "node": ">=10" } }, + "node_modules/chart.js": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz", + "integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==" + }, "node_modules/check-types": { "version": "11.2.2", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.2.tgz", @@ -5828,6 +6158,14 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -6484,6 +6822,384 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, + "node_modules/d3": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz", + "integrity": "sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -6747,6 +7463,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz", + "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==", + "dependencies": { + "robust-predicates": "^3.0.0" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -9306,6 +10030,14 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/ipaddr.js": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", @@ -12062,6 +12794,11 @@ "shell-quote": "^1.7.3" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -12135,6 +12872,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -12233,6 +12975,11 @@ "tmpl": "1.0.5" } }, + "node_modules/material-colors": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", + "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" + }, "node_modules/mdn-data": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", @@ -14598,6 +15345,32 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, + "node_modules/react-chartjs-2": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-4.3.1.tgz", + "integrity": "sha512-5i3mjP6tU7QSn0jvb8I4hudTzHJqS8l00ORJnVwI2sYu0ihpj83Lv2YzfxunfxTZkscKvZu2F2w9LkwNBhj6xA==", + "peerDependencies": { + "chart.js": "^3.5.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-color": { + "version": "2.19.3", + "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz", + "integrity": "sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==", + "dependencies": { + "@icons/material": "^0.2.4", + "lodash": "^4.17.15", + "lodash-es": "^4.17.15", + "material-colors": "^1.2.1", + "prop-types": "^15.5.10", + "reactcss": "^1.2.0", + "tinycolor2": "^1.4.1" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -14727,16 +15500,69 @@ "react": "^18.2.0" } }, + "node_modules/react-draggable": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.5.tgz", + "integrity": "sha512-OMHzJdyJbYTZo4uQE393fHcqqPYsEtkjfMgvCHr6rejT+Ezn4OZbNyGH50vv+SunC1RMvwOTSWkEODQLzw1M9g==", + "dependencies": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, "node_modules/react-error-overlay": { "version": "6.0.11", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "node_modules/react-gradient-hook": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/react-gradient-hook/-/react-gradient-hook-1.5.3.tgz", + "integrity": "sha512-PYmzwMu5usQ4w1tMOvUS0QvEgmZ6rVFi3HpmsEJI7BgzU5P033wEs4bblTgnPXIcBBFL7EAFmIfjL98vz3umHA==", + "dependencies": { + "react-color": "^2.19.3", + "react-draggable": "^4.4.5", + "react-icons": "^4.4.0" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/react-icons": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.11.0.tgz", + "integrity": "sha512-V+4khzYcE5EBk/BvcuYRq6V/osf11ODUM2J8hg2FDSswRrGvqiYUYPRy4OdrWaQOBj4NcpJfmHZLNaD+VH0TyA==", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-leaflet": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz", + "integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==", + "dependencies": { + "@react-leaflet/core": "^2.1.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/react-leaflet-hotline": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/react-leaflet-hotline/-/react-leaflet-hotline-1.4.12.tgz", + "integrity": "sha512-Pb3yl01g5OizMvdlWhTvMrzvN5E+Y7YyeMsvpXX4SiIl5o8q6TqnQy9Z+3NeO6UlM0c0z3geBTbQgsdq3phLwA==" + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -14745,6 +15571,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.15.0.tgz", + "integrity": "sha512-NIytlzvzLwJkCQj2HLefmeakxxWHWAP+02EGqWEZy+DgfHHKQMUoBBjUQLOtFInBMhWtb3hiUy6MfFgwLjXhqg==", + "dependencies": { + "@remix-run/router": "1.8.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.15.0.tgz", + "integrity": "sha512-aR42t0fs7brintwBGAv2+mGlCtgtFQeOzK0BM1/OiqEzRejOZtpMZepvgkscpMUnKb8YO84G7s3LsHnnDNonbQ==", + "dependencies": { + "@remix-run/router": "1.8.0", + "react-router": "6.15.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -14817,6 +15673,25 @@ } } }, + "node_modules/react-slider": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/react-slider/-/react-slider-2.0.6.tgz", + "integrity": "sha512-gJxG1HwmuMTJ+oWIRCmVWvgwotNCbByTwRkFZC6U4MBsHqJBmxwbYRJUmxy4Tke1ef8r9jfXjgkmY/uHOCEvbA==", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18" + } + }, + "node_modules/reactcss": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", + "integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==", + "dependencies": { + "lodash": "^4.0.1" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -15147,6 +16022,11 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, "node_modules/rollup": { "version": "2.79.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", @@ -15253,6 +16133,11 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, "node_modules/safe-array-concat": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz", @@ -16395,6 +17280,11 @@ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==" + }, "node_modules/titleize": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8eb05552..70aaeb0e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,9 +10,19 @@ "@types/node": "^16.18.46", "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", + "axios": "^0.24.0", + "chart.js": "^3.8.0", + "d3": "^7.4.3", + "leaflet": "^1.9.4", "react": "^18.2.0", + "react-chartjs-2": "^4.2.0", "react-dom": "^18.2.0", + "react-gradient-hook": "^1.5.3", + "react-leaflet": "^4.2.1", + "react-leaflet-hotline": "^1.4.2", + "react-router-dom": "^6.15.0", "react-scripts": "5.0.1", + "react-slider": "^2.0.6", "typescript": "^4.9.5", "web-vitals": "^2.1.4" }, @@ -41,10 +51,14 @@ ] }, "devDependencies": { + "@types/d3": "^7.1.0", + "@types/leaflet": "^1.7.5", + "@types/react": "^17.0.24", + "@types/react-slider": "^1.3.1", "@typescript-eslint/parser": "^5.62.0", "eslint": "^8.48.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", - "prettier": "^3.0.3" + "prettier": "3.0.3" } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a53698aa..90ad2fb5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,26 +1,15 @@ -import React from 'react'; -import logo from './logo.svg'; +import { FC } from 'react'; +// import { BrowserRouter as Router, Route } from 'react-router-dom'; +import Conditions from './pages/Conditions'; + import './App.css'; -function App() { +const App: FC = () => { return (
-
- logo -

- Edit src/App.tsx and save to reload. -

- - Learn React - -
+
); -} +}; export default App; diff --git a/frontend/src/Components/Conditions/ConditionsMap.tsx b/frontend/src/Components/Conditions/ConditionsMap.tsx new file mode 100644 index 00000000..be4df760 --- /dev/null +++ b/frontend/src/Components/Conditions/ConditionsMap.tsx @@ -0,0 +1,525 @@ +import { useEffect, useRef, useState } from 'react'; +import ReactSlider from 'react-slider'; + +import { MapContainer, TileLayer, ScaleControl, GeoJSON } from 'react-leaflet'; +import { Layer, PathOptions } from 'leaflet'; + +import { Feature, FeatureCollection } from 'geojson'; + +import Zoom from '../Map/Zoom'; +import { MAP_OPTIONS } from './constants'; + +import { getConditions } from '../../queries/fetchConditions'; + +import '../../css/slider.css'; + +const ALL = 'ALL'; +const KPI = 'KPI'; +const DI = 'DI'; +const IRI = 'IRI'; +const IRInew = 'IRI_new'; +const Mu = 'Mu'; +const Enrg = 'E_norm'; + +const conditionTypes = [ + ALL, + KPI, + DI, + IRI, // IRInew, + Mu, + Enrg, +]; + +interface YearMonth { + year: number; + month: number; +} + +interface DateRange { + start?: YearMonth; + end?: YearMonth; +} + +const lessOrEqualThan = ( + yearMonth1: YearMonth, + yearMonth2: YearMonth, +): boolean => { + if (yearMonth1.year < yearMonth2.year) { + return true; + } else if (yearMonth1.year > yearMonth2.year) { + return false; + } else { + return yearMonth1.month <= yearMonth2.month; + } +}; + +const noMonth = (dateRange: DateRange): number => { + if (dateRange.start === undefined || dateRange.end === undefined) { + return NaN; + } else { + if (!lessOrEqualThan(dateRange.start, dateRange.end)) { + return NaN; + } else { + return ( + (dateRange.end.year - dateRange.start.year) * 12 + + dateRange.end.month - + dateRange.start.month + ); + } + } +}; + +const noToYearMonth = ( + no: number, + dateRange: DateRange, +): YearMonth | undefined => { + if (dateRange.start !== undefined) { + const month = dateRange.start.month + no; + const normalizedMonth = ((month - 1) % 12) + 1; + const years = Math.floor((month - 1) / 12); + return { year: dateRange.start.year + years, month: normalizedMonth }; + } +}; + +const yearMonthtoText = (yearMonth: YearMonth | undefined): string => { + if (yearMonth === undefined) { + return '??/??'; + } + return ( + yearMonth.month.toString() + '/' + yearMonth.year.toString().substring(2) + ); +}; + +const getTypeColor = (type: string): string => { + switch (type) { + case KPI: + return 'red'; + + case DI: + return 'green'; + + case IRI: + case IRInew: + return 'yellow'; + + case Mu: + return 'cyan'; + + case Enrg: + return 'magenta'; + + default: + return 'grey'; + } +}; +const green = '#09BD09'; //"#00FF00" +const greenyellow = '#02FC02'; // "#BFFF00" +const yellow = '#FFFF00'; +const orange = '#FFBF00'; +const red = '#FF0000'; + +const getConditionColor = (properties: GeoJSON.GeoJsonProperties): string => { + if (properties !== null) { + const type = properties.type; + const value = properties.value; + // const motorway = properties.motorway; + + if (type !== undefined && type !== 'ALL') { + // if (motorway === undefined || !motorway) { + // gradient for municpality roads + switch (type) { + case KPI: + return value <= 4.0 + ? green + : value <= 6.0 + ? greenyellow + : value <= 7.0 + ? yellow + : value <= 8.0 + ? orange + : red; + case DI: + return value <= 1.2 + ? green + : value <= 1.5 + ? greenyellow + : value <= 2.0 + ? yellow + : value <= 2.5 + ? orange + : red; + case IRI: + case IRInew: + return value <= 1.5 ? green : value <= 2.5 ? yellow : red; + case Mu: + return value >= 0.8 + ? green + : value >= 0.5 + ? greenyellow + : value >= 0.3 + ? yellow + : value >= 0.2 + ? orange + : red; + case Enrg: + return value <= 0.05 + ? green + : value <= 0.1 + ? greenyellow + : value <= 0.15 + ? yellow + : value <= 0.25 + ? orange + : red; + } + /* } else { + // gradient for motorways + switch (type) { + case KPI: + return value <= 2.0 ? green : (value <= 4.5 ? yellow : red) + case DI: + return value <= 1.0 ? green : (value <= 3.0 ? yellow : red) + case IRI: + return value <= 1.0 ? green : (value <= 2.0 ? yellow : red) + } + } */ + } + } + return 'grey'; +}; + +const ConditionsMap = (props: any) => { + const { children } = props; + + const { center, zoom, minZoom, maxZoom, scaleWidth } = MAP_OPTIONS; + + const geoJsonRef = useRef(); + + const [dataAll, setDataAll] = useState(); + + const [rangeAll, setRangeAll] = useState({}); + + const [rangeSelected, setRangeSelected] = useState({}); + + const [mode, setMode] = useState('ALL'); + + const inputChange = ({ target }: any) => { + setMode(target.value); + }; + + const rangeChange = (values: number[]) => { + if (values.length === 2) { + const newSelectedRange: DateRange = { + start: noToYearMonth(values[0], rangeAll), + end: noToYearMonth(values[1], rangeAll), + }; + setRangeSelected(newSelectedRange); + } + }; + + useEffect(() => { + getConditions((data) => { + const range: DateRange = {}; + data.features.forEach((f) => { + if (f.properties !== null && f.properties.valid_time !== undefined) { + const date = new Date(f.properties.valid_time); + const yearMonth: YearMonth = { + year: date.getFullYear(), + month: date.getMonth() + 1, + }; + f.properties.valid_yearmonth = yearMonth; + if (range.start === undefined) { + range.start = yearMonth; + } else if (!lessOrEqualThan(range.start, yearMonth)) { + range.start = yearMonth; + } + if (range.end === undefined) { + range.end = yearMonth; + } else if (!lessOrEqualThan(yearMonth, range.end)) { + range.end = yearMonth; + } + } + }); + setRangeAll(range); + setDataAll(data); + }); + }, []); + + useEffect(() => { + const setConditions = (data: FeatureCollection) => { + if (geoJsonRef !== undefined && geoJsonRef.current !== undefined) { + geoJsonRef.current.clearLayers(); + geoJsonRef.current.addData(data); + geoJsonRef.current.setStyle(style); + } + }; + + const style = ( + feature: + | GeoJSON.Feature + | undefined, + ): PathOptions => { + const mapStyle: PathOptions = { + weight: 4, + opacity: 1, + color: 'grey', + // fillcolor: 'red', + // fillOpacity: 0.7 + }; + + if ( + feature !== undefined && + feature.properties !== null && + feature.properties.type !== undefined + ) { + if (mode === 'ALL') { + mapStyle.color = getTypeColor(feature.properties.type); + mapStyle.opacity = 0.5; + switch (feature.properties.type) { + case KPI: + mapStyle.dashArray = '12 12'; + break; + case DI: + mapStyle.dashArray = '20 20'; + break; + case IRI: + case IRInew: + mapStyle.dashArray = '28 28'; + break; + case Mu: + mapStyle.dashArray = '15 15'; + break; + case Enrg: + mapStyle.dashArray = '22 22'; + } + } else if (feature.properties.value !== undefined) { + mapStyle.color = getConditionColor(feature.properties); + } + } + + return mapStyle; + }; + + if (mode === 'ALL') { + if (dataAll !== undefined) { + const featureCollection: FeatureCollection = { + type: 'FeatureCollection', + features: + dataAll.features !== undefined + ? dataAll.features.filter( + (f) => + f.properties !== null && + (f.properties.valid_yearmonth === undefined || + ((rangeSelected.start === undefined || + lessOrEqualThan( + rangeSelected.start, + f.properties.valid_yearmonth, + )) && + (rangeSelected.end === undefined || + lessOrEqualThan( + f.properties.valid_yearmonth, + rangeSelected.end, + )))), + ) + : [], + }; + setConditions(featureCollection); + } + } else { + const featureCollection: FeatureCollection = { + type: 'FeatureCollection', + features: + dataAll !== undefined + ? dataAll.features !== undefined + ? dataAll.features.filter( + (f) => + f.properties !== null && + f.properties.type === mode && + (f.properties.valid_yearmonth === undefined || + ((rangeSelected.start === undefined || + lessOrEqualThan( + rangeSelected.start, + f.properties.valid_yearmonth, + )) && + (rangeSelected.end === undefined || + lessOrEqualThan( + f.properties.valid_yearmonth, + rangeSelected.end, + )))), + ) + : [] + : [], + }; + setConditions(featureCollection); + } + }, [dataAll, mode, rangeAll, rangeSelected]); + + const onEachFeature = (feature: Feature, layer: Layer) => { + if (layer.on !== undefined) { + layer.on({ + // mouseover: ... , + // mouseout: ... , + click: (e) => { + console.log(e.target); + console.log(feature); + }, + }); + } + if ( + feature !== undefined && + feature.properties !== null && + feature.properties.type !== undefined && + feature.properties.value !== undefined && + feature.properties.value !== null + ) { + layer.bindPopup( + 'Condition type: ' + + feature.properties.type + + '
' + + 'Value: ' + + feature.properties.value.toPrecision(3) + + '
' + + (feature.properties.std !== undefined && + feature.properties.std !== null + ? 'σ: ' + feature.properties.std.toPrecision(3) + '
' + : '') + + 'Valid for ' + + feature.properties.valid_time + + '
' + + 'Computed on ' + + feature.properties.compute_time + + '
' + + (feature.properties.motorway !== undefined && + feature.properties.motorway + ? 'Motorway: yes
' + : '') + + 'Trip (task id): ' + + feature.properties.task_id + + '
' + + 'Condition id: ' + + feature.properties.id, + ); + } + }; + + return ( +
+
+ + + + {dataAll !== undefined && ( + + )} + + + + + {children} +
+
+ + + {rangeAll !== undefined && + rangeAll.start !== undefined && + rangeAll.end !== undefined && ( + ( +
+ {yearMonthtoText(noToYearMonth(props.key, rangeAll))} +
+ )} + defaultValue={[0, noMonth(rangeAll)]} + ariaLabel={['Lower thumb', 'Upper thumb']} + ariaValuetext={(state) => + `Thumb value ${yearMonthtoText( + noToYearMonth(state.valueNow, rangeAll), + )}` + } + renderThumb={(props, state) => ( +
+ {yearMonthtoText(noToYearMonth(state.valueNow, rangeAll))} +
+ )} + pearling + minDistance={0} + onChange={(value) => rangeChange(value)} + /> + )} +
+ {mode === 'ALL' && ( +
+ Colors indicate condition types (not their values):  {' '} +
KPI
    +
DI
    +
IRI
    +
μ
    +
E
    +
+ )} + {mode !== 'ALL' && ( +
+ Colors indicate condition values from   + green (good)  + over  + yellow (medium)  + to  + red (bad)! +
+ )} +
+ ); +}; + +export default ConditionsMap; diff --git a/frontend/src/Components/Conditions/constants.ts b/frontend/src/Components/Conditions/constants.ts new file mode 100644 index 00000000..58d30b1f --- /dev/null +++ b/frontend/src/Components/Conditions/constants.ts @@ -0,0 +1,10 @@ +import { LatLng } from 'leaflet'; + +// Options for conditions map +export const MAP_OPTIONS = { + center: new LatLng(55.672, 12.458), + zoom: 12, + minZoom: 3, + maxZoom: 19, + scaleWidth: 100, +}; diff --git a/frontend/src/Components/Map/Hooks/useMapBounds.tsx b/frontend/src/Components/Map/Hooks/useMapBounds.tsx new file mode 100644 index 00000000..b0b808ac --- /dev/null +++ b/frontend/src/Components/Map/Hooks/useMapBounds.tsx @@ -0,0 +1,15 @@ +import { LatLngBounds, Map } from 'leaflet'; +import { useState } from 'react'; +import { useMapEvents } from 'react-leaflet'; + +const useMapBounds = (): LatLngBounds => { + const map = useMapEvents({ + moveend: () => setBounds(map.getBounds()), + }); + + const [bounds, setBounds] = useState(map.getBounds()); + + return bounds; +}; + +export default useMapBounds; diff --git a/frontend/src/Components/Map/Hooks/useZoom.tsx b/frontend/src/Components/Map/Hooks/useZoom.tsx new file mode 100644 index 00000000..da33392c --- /dev/null +++ b/frontend/src/Components/Map/Hooks/useZoom.tsx @@ -0,0 +1,18 @@ +import { useEffect, useState } from 'react'; +import { useMapEvents } from 'react-leaflet'; + +const useZoom = () => { + const [zoom, setZoom] = useState(); + + const update = () => setZoom(map.getZoom()); + + const map = useMapEvents({ + zoom: update, + }); + + useEffect(update, []); + + return zoom; +}; + +export default useZoom; diff --git a/frontend/src/Components/Map/MapWrapper.tsx b/frontend/src/Components/Map/MapWrapper.tsx new file mode 100644 index 00000000..22f889d9 --- /dev/null +++ b/frontend/src/Components/Map/MapWrapper.tsx @@ -0,0 +1,40 @@ +import { MapContainer, TileLayer, ScaleControl } from 'react-leaflet'; + +import Zoom from './Zoom'; + +import '../../css/map.css'; +import { MAP_OPTIONS } from './constants'; + +const MapWrapper = (props: any) => { + const { children } = props; + + const { center, zoom, minZoom, maxZoom, scaleWidth } = MAP_OPTIONS; + + return ( + + + + + {children} + + ); +}; + +export default MapWrapper; diff --git a/frontend/src/Components/Map/Renderers/DistHotline.tsx b/frontend/src/Components/Map/Renderers/DistHotline.tsx new file mode 100644 index 00000000..3241aeae --- /dev/null +++ b/frontend/src/Components/Map/Renderers/DistHotline.tsx @@ -0,0 +1,95 @@ +import { FC, useEffect, useMemo, useState } from 'react'; +import { LeafletEvent, Polyline } from 'leaflet'; +import { HotlineOptions, useCustomHotline } from 'react-leaflet-hotline'; + +import { useGraph } from '../../../context/GraphContext'; + +import { Condition, Node, WayId } from '../../../models/path'; + +import DistRenderer from '../../../assets/hotline/DistRenderer'; +import { DistData } from '../../../assets/hotline/hotline'; +import HoverHotPolyline from '../../../assets/hotline/HoverHotPolyline'; +import { + HotlineEventFn, + HotlineEventHandlers, +} from 'react-leaflet-hotline/dist/types/types'; +import useZoom from '../Hooks/useZoom'; +import { useHoverContext } from '../../../context/GraphHoverContext'; + +const getLat = (n: Node) => n.lat; +const getLng = (n: Node) => n.lng; +const getVal = (n: Node) => n.way_dist; +const getWeight = (z: number | undefined) => + z === undefined ? 0 : Math.max(z > 8 ? z - 6 : z - 5, 2); + +interface IDistHotline { + way_ids: WayId[]; + geometry: Node[][]; + conditions: Condition[][]; + options?: HotlineOptions; + eventHandlers?: HotlineEventHandlers; +} + +const handler = ( + eventHandlers: HotlineEventHandlers | undefined, + event: keyof HotlineEventHandlers, + opacity: number, +) => { + return (e: LeafletEvent, i: number, p: Polyline) => { + p.setStyle({ opacity }); + if (eventHandlers && eventHandlers[event] !== undefined) + (eventHandlers[event] as HotlineEventFn)(e, i, p); + }; +}; + +const DistHotline: FC = ({ + way_ids, + geometry, + conditions, + options, + eventHandlers, +}) => { + const { dotHover } = useHoverContext(); + const zoom = useZoom(); + + const opts = useMemo( + () => ({ + ...options, + weight: getWeight(zoom), + }), + [options, zoom], + ); + + const handlers: HotlineEventHandlers = useMemo( + () => ({ + ...eventHandlers, + mouseover: handler(eventHandlers, 'mouseover', 0.5), + mouseout: handler(eventHandlers, 'mouseout', 0), + }), + [eventHandlers], + ); + + const { hotline } = useCustomHotline( + DistRenderer, + HoverHotPolyline, + { + data: geometry, + getLat, + getLng, + getVal, + options: opts, + eventHandlers: handlers, + }, + way_ids, + conditions, + ); + + useEffect(() => { + if (hotline === undefined) return; + (hotline as HoverHotPolyline).setHover(dotHover); + }, [dotHover]); + + return null; +}; + +export default DistHotline; diff --git a/frontend/src/Components/Map/Zoom.tsx b/frontend/src/Components/Map/Zoom.tsx new file mode 100644 index 00000000..7dc72e96 --- /dev/null +++ b/frontend/src/Components/Map/Zoom.tsx @@ -0,0 +1,15 @@ +import { ZoomControl } from 'react-leaflet'; +import useZoom from './Hooks/useZoom'; + +const Zoom = () => { + const zoom = useZoom(); + + return ( + <> +
{zoom}
+ + + ); +}; + +export default Zoom; diff --git a/frontend/src/Components/Map/constants.ts b/frontend/src/Components/Map/constants.ts new file mode 100644 index 00000000..06861973 --- /dev/null +++ b/frontend/src/Components/Map/constants.ts @@ -0,0 +1,80 @@ +import { LatLng } from 'leaflet'; +import { Palette } from 'react-leaflet-hotline'; +import { + ActiveMeasProperties, + RendererOptions, + XAxisType, +} from '../../models/properties'; +import { RendererName } from '../../models/renderers'; + +// Map +export const MAP_OPTIONS = { + center: new LatLng(55.672, 12.458), + zoom: 12, + minZoom: 5, + maxZoom: 18, + scaleWidth: 100, +}; + +// Renderer +export const RENDERER_WIDTH = 4; +export const RENDERER_WEIGHT = 4; +export const RENDERER_COLOR = 'red'; +export const RENDERER_OPACITY = 1.0; + +// TODO, ekki@dtu.dk: this was a quick fix; this was a constant used in +// different places in the software. But someone changed the components, +// in order for this not to happen, I made this a function, which makes +// a new copy. Eventually we should track down where the change of the +// constant is made and fix this. +export function RENDERER_PALETTE(): Palette { + return [ + { r: 0, g: 160, b: 0, t: 0 }, + { r: 255, g: 255, b: 0, t: 0.5 }, + { r: 255, g: 0, b: 0, t: 1 }, + ]; +} + +export function RENDERER_OPTIONS(): Required { + return { + rendererName: 'hotline' as RendererName, + dilatationFactor: 1, + arrowHead: 0, + min: 0, + max: 10, + width: RENDERER_WIDTH, + weight: RENDERER_WEIGHT, + color: RENDERER_COLOR, + opacity: RENDERER_OPACITY, + palette: RENDERER_PALETTE(), + }; +} + +export function RENDERER_MEAS_PROPERTIES(): Required { + return { + ...RENDERER_OPTIONS(), + dbName: '', + name: '', + hasValue: true, + xAxisType: XAxisType.distance, + isActive: false, + }; +} + +// Heatmap +// ekki@dtu.dk (precaussion: see comment on PALLETTE +export function HEATMAP_PALETTE(): Palette { + return [ + { r: 0, g: 0, b: 255, t: 0 }, + { r: 255, g: 255, b: 255, t: 0.5 }, + { r: 255, g: 0, b: 0, t: 1 }, + ]; +} + +export function HEATMAP_OPTIONS() { + return { + max: 10, + radius: 10, + palette: HEATMAP_PALETTE(), + }; +} diff --git a/frontend/src/Components/Palette/PaletteEditor.tsx b/frontend/src/Components/Palette/PaletteEditor.tsx new file mode 100644 index 00000000..3833f305 --- /dev/null +++ b/frontend/src/Components/Palette/PaletteEditor.tsx @@ -0,0 +1,47 @@ +import { FC, MouseEvent, useState } from 'react'; +import { Gradient } from 'react-gradient-hook'; +import { CursorOptions } from 'react-gradient-hook/lib/types'; +import { useMap } from 'react-leaflet'; +import { Palette } from 'react-leaflet-hotline'; + +import '../../css/palette.css'; + +interface IPaletteEditor { + width: number | undefined; + defaultPalette?: Palette; + cursorOptions?: CursorOptions; + onChange?: (palette: Palette) => void; +} + +const PaletteEditor: FC = ({ + width, + defaultPalette, + cursorOptions, + onChange, +}) => { + const [show, setShow] = useState(false); + + const toggleAppear = () => setShow((prev) => !prev); + + if (width === undefined || width === 0) return null; + + return ( +
+
+ +
+
+ 🎨 +
+
+ ); +}; + +export default PaletteEditor; diff --git a/frontend/src/Components/RoadConditions/ConditionsGraph.tsx b/frontend/src/Components/RoadConditions/ConditionsGraph.tsx new file mode 100644 index 00000000..6f1dec46 --- /dev/null +++ b/frontend/src/Components/RoadConditions/ConditionsGraph.tsx @@ -0,0 +1,133 @@ +import { FC, useCallback, useEffect, useMemo, useRef } from 'react'; +import { + ChartData, + Chart, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, + ActiveElement, + ChartEvent, + ChartOptions, + ChartTypeRegistry, + Plugin, +} from 'chart.js'; +import { Color, Palette } from 'react-leaflet-hotline'; +import { Line } from 'react-chartjs-2'; + +import { ConditionType } from '../../models/graph'; + +Chart.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, +); + +const options = ({ name, min, max }: ConditionType): ChartOptions<'line'> => ({ + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'top' as const, + labels: { color: 'white' }, + }, + }, + scales: { + x: { + title: { + display: true, + text: 'distance (m)', + }, + ticks: { + maxTicksLimit: 30, + stepSize: 200, + callback: (tick: string | number) => + Math.round(parseFloat(tick.toString())), + }, + }, + y: { + title: { + display: true, + text: name, + }, + min: min, + max: max, + }, + }, +}); + +interface Props { + type: ConditionType; + data: ChartData<'line', number[], number> | undefined; + palette: Palette; +} + +const ConditionsGraph: FC = ({ type, data, palette }) => { + const ref = useRef>(null); + + const addPaletteChart = + (palette: Palette) => + (chart: Chart) => { + const dataset = chart.data.datasets[0]; + const gradient = chart.ctx.createLinearGradient( + 0, + chart.chartArea.bottom, + 0, + 0, + ); + console.log(...palette); + palette.forEach((c: Color) => { + gradient.addColorStop(c.t, `rgb(${c.r}, ${c.g}, ${c.b})`); + }); + dataset.borderColor = gradient; + dataset.backgroundColor = gradient; + }; + + useEffect(() => { + if (ref.current === null) return; + const chart = ref.current; + addPaletteChart(palette)(chart); + chart.update(); + }, [ref, data, palette]); + + // attach events to the graph options + const graphOptions: ChartOptions<'line'> = useMemo( + () => ({ + ...options(type), + onClick: ( + event: ChartEvent, + elts: ActiveElement[], + chart: Chart, + ) => { + if (elts.length === 0) return; + const elt = elts[0]; // doesnt work if multiple datasets + const pointIndex = elt.index; + console.log(pointIndex, event, elts); + }, + }), + [], + ); + + const plugins: Plugin<'line'>[] = [ + { + id: 'id', + }, + ]; + + return ( +
+ {data && ( + + )} +
+ ); +}; + +export default ConditionsGraph; diff --git a/frontend/src/Components/RoadConditions/ConditionsMap.tsx b/frontend/src/Components/RoadConditions/ConditionsMap.tsx new file mode 100644 index 00000000..3a7253dc --- /dev/null +++ b/frontend/src/Components/RoadConditions/ConditionsMap.tsx @@ -0,0 +1,75 @@ +import { FC, useCallback, useRef } from 'react'; +import { ChartData } from 'chart.js'; +import { Palette } from 'react-leaflet-hotline'; + +import MapWrapper from '../Map/MapWrapper'; +import { RENDERER_PALETTE } from '../Map/constants'; +import PaletteEditor from '../Palette/PaletteEditor'; +import Ways from './Ways'; + +import useSize from '../../hooks/useSize'; + +import { ConditionType } from '../../models/graph'; +import { Condition } from '../../models/path'; + +import { getConditions } from '../../queries/conditions'; + +interface Props { + type: ConditionType; + palette: Palette; + setPalette: React.Dispatch>; + setWayData: React.Dispatch< + React.SetStateAction | undefined> + >; +} + +const ConditionsMap: FC = ({ + type, + palette, + setPalette, + setWayData, +}) => { + const { name, max, grid, samples } = type; + + const ref = useRef(null); + const [width, _] = useSize(ref); + + const onClick = useCallback((way_id: string, way_length: number) => { + console.log('onclick called'); + + getConditions(way_id, name, (wc: Condition[]) => { + console.log('getConditions called'); + setWayData({ + labels: wc.map((p) => p.way_dist * way_length), + datasets: [ + { + type: 'line' as const, + label: way_id, + borderColor: 'rgb(255, 99, 132)', + borderWidth: 2, + fill: false, + tension: 0.1, + data: wc.map((p) => p.value), + }, + ], + }); + }); + }, []); + + return ( +
+ + + + + +
+ ); +}; + +export default ConditionsMap; diff --git a/frontend/src/Components/RoadConditions/Ways.tsx b/frontend/src/Components/RoadConditions/Ways.tsx new file mode 100644 index 00000000..31cab719 --- /dev/null +++ b/frontend/src/Components/RoadConditions/Ways.tsx @@ -0,0 +1,65 @@ +import { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { TRGB } from 'react-gradient-hook/lib/types'; +import { HotlineOptions } from 'react-leaflet-hotline'; +import { HotlineEventHandlers } from 'react-leaflet-hotline/dist/types/types'; +import { useGraph } from '../../context/GraphContext'; +import { WaysConditions } from '../../models/path'; +import { getWaysConditions } from '../../queries/conditions'; +import useZoom from '../Map/Hooks/useZoom'; +import DistHotline from '../Map/Renderers/DistHotline'; + +interface IWays { + palette: TRGB[]; + type: string; + onClick?: (way_id: string, way_length: number) => void; +} + +const Ways: FC = ({ palette, type, onClick }) => { + const zoom = useZoom(); + const { minY, maxY } = useGraph(); + + const [ways, setWays] = useState(); + + const options = useMemo( + () => ({ + palette, + min: minY, + max: maxY, + }), + [palette, minY, maxY], + ); + + const handlers = useMemo( + () => ({ + click: (_, i) => { + if (ways && onClick) onClick(ways.way_ids[i], ways.way_lengths[i]); + }, + }), + [ways], + ); + + useEffect(() => { + if (zoom === undefined) return; + const z = Math.max(0, zoom - 12); + getWaysConditions(type, z, (data: WaysConditions) => { + console.log(data); + setWays(data); + }); + }, [zoom]); + + return ( + <> + {ways ? ( + + ) : null} + + ); +}; + +export default Ways; diff --git a/frontend/src/assets/graph/types.ts b/frontend/src/assets/graph/types.ts new file mode 100644 index 00000000..c66dbc8f --- /dev/null +++ b/frontend/src/assets/graph/types.ts @@ -0,0 +1,72 @@ +import { Selection } from 'd3'; +import { FC } from 'react'; +import { Color } from 'react-leaflet-hotline'; +import { Bounds } from '../../models/path'; + +// SVG +export type SVG = d3.Selection; +export type SVGLayer = d3.Selection; + +// Axis +export interface IAxis { + svg: SVG; + axis: Axis | undefined; + width: number; + height: number; + zoom: number; + absolute?: boolean; + time?: boolean; +} +export type ReactAxis = FC; +export type Axis = d3.ScaleLinear; +export type GraphAxis = [Axis, Axis]; + +// Data format +export type GraphPoint = { + x: number; + y: number; + lat: number; + lng: number; +}; // [number, number] + +export type GraphData = GraphPoint[]; + +export interface Plot { + data: GraphData; + bounds?: Bounds; + label: string; +} + +// Events +export interface DotHover { + label: string; + point: GraphPoint; +} + +// Options +export interface PathOptions { + stroke?: string; + strokeWidth?: number; +} + +export interface DotsOptions { + radius?: number; + opacity?: number; + fill?: string; +} + +// Palette - Gradient +export type Gradient = Selection< + SVGStopElement, + Color, + SVGLinearGradientElement, + unknown +>; + +// MinMax +export type MinMax = [number, number]; +export type AddMinMaxFunc = (label: string, bounds: Required) => void; +export type RemMinMaxFunc = (label: string) => void; + +// Callback +export type D3Callback = (event: any, d: GraphPoint) => void; diff --git a/frontend/src/assets/hotline/DistRenderer.ts b/frontend/src/assets/hotline/DistRenderer.ts new file mode 100644 index 00000000..8b4fddba --- /dev/null +++ b/frontend/src/assets/hotline/DistRenderer.ts @@ -0,0 +1,196 @@ +import { LatLng, Map } from 'leaflet'; +import { HotlineOptions, Renderer } from 'react-leaflet-hotline'; + +import { Condition, Node, WayId } from '../../models/path'; +import { DotHover } from '../graph/types'; +import { DistData, DistPoint } from './hotline'; +import Edge from './Edge'; + +export default class DistRenderer extends Renderer { + way_ids: string[]; + conditions: Condition[][]; + edgess: Edge[][]; + dotHover: DotHover | undefined; + + constructor(options?: HotlineOptions, ...args: any[]) { + super({ ...options }); + this.way_ids = args[0][0]; + this.conditions = args[0][1]; + this.edgess = []; + this.dotHover = undefined; + } + + projectLatLngs( + _map: Map, + latlngs: LatLng[], + result: any, + projectedBounds: any, + ) { + const len = latlngs.length; + const ring: DistData = []; + for (let i = 0; i < len; i++) { + ring[i] = _map.latLngToLayerPoint(latlngs[i]) as any; + ring[i].i = i; + ring[i].way_dist = latlngs[i].alt || 0; + projectedBounds.extend(ring[i]); + } + result.push(ring); + } + + onProjected(): number { + this.updateEdges(); + return 0; + } + + _addWayColorGradient( + gradient: CanvasGradient, + edge: Edge, + dist: number, + way_id: string, + ): void { + const opacity = + this.dotHover !== undefined && this.dotHover.label !== way_id ? 0.3 : 1; + try { + gradient.addColorStop(dist, `rgba(${edge.get().join(',')},${opacity})`); + } catch {} + } + + /** + * Find the closest conditions around each edge (node for ways) and interpolate the color + */ + private updateEdges() { + let i = 0; + + const calcValue = (a: Condition, b: Condition, cur: DistPoint) => { + const A = 1 - (cur.way_dist - a.way_dist); + const B = 1 - (cur.way_dist - b.way_dist); + return (A * a.value + B * b.value) / (A + B); + }; + + const getValue = (d: DistPoint, conditions: Condition[]): number => { + if (d.way_dist <= 0) return conditions[0].value; + else if (d.way_dist >= 1 || i >= conditions.length) + return conditions[conditions.length - 1].value; + + while (conditions[i].way_dist <= d.way_dist && ++i < conditions.length) {} + + if (i === 0) return conditions[0].value; + else if (i >= conditions.length - 1) + return conditions[conditions.length - 1].value; + + return calcValue(conditions[i - 1], conditions[i], d); + }; + + this.edgess = this.projectedData.map((data, j) => { + i = 0; + return data.map((d) => { + const value = getValue(d, this.conditions[j]); + const rgb = this.getRGBForValue(value); + return new Edge(...rgb); + }); + }); + } + + setWayIds(way_ids: WayId[]) { + this.way_ids = way_ids; + } + + setConditions(conditions: Condition[][]) { + this.conditions = conditions; + this.updateEdges(); + } + + _drawHotline(): void { + const ctx = this._ctx; + if (ctx === undefined) return; + + const dataLength = this._data.length; + + for (let i = 0; i < dataLength; i++) { + const path = this._data[i]; + const edges = this.edgess[i]; + + const way_id = this.way_ids[i]; + const conditions = this.conditions[i]; + + for (let j = 1; j < path.length; j++) { + const start = path[j - 1]; + const end = path[j]; + + const gradient = this._addGradient(ctx, start, end, conditions, way_id); + + this._addWayColorGradient(gradient, edges[start.i], 0, way_id); + this._addWayColorGradient(gradient, edges[end.i], 1, way_id); + + this.drawGradient(ctx, gradient, way_id, start, end); + } + } + } + + drawGradient( + ctx: CanvasRenderingContext2D, + gradient: CanvasGradient, + way_id: string, + pointStart: DistPoint, + pointEnd: DistPoint, + ) { + ctx.beginPath(); + const hoverWeight = + this.dotHover !== undefined && + this.dotHover.label === way_id && + pointStart.way_dist <= this.dotHover.point.x && + pointEnd.way_dist >= this.dotHover.point.x + ? 10 + : 0; + + ctx.lineWidth = this._options.weight + hoverWeight; + ctx.strokeStyle = gradient; + ctx.moveTo(pointStart.x, pointStart.y); + ctx.lineTo(pointEnd.x, pointEnd.y); + ctx.stroke(); + ctx.closePath(); + } + + _addGradient( + ctx: CanvasRenderingContext2D, + start: DistPoint, + end: DistPoint, + conditions: Condition[], + way_id: string, + ): CanvasGradient { + const gradient: CanvasGradient = ctx.createLinearGradient( + start.x, + start.y, + end.x, + end.y, + ); + this.computeGradient(gradient, start, end, conditions, way_id); + return gradient; + } + + computeGradient( + gradient: CanvasGradient, + pointStart: DistPoint, + pointEnd: DistPoint, + conditions: Condition[], + way_id: string, + ) { + const start_dist = pointStart.way_dist; + const end_dist = pointEnd.way_dist; + + if (start_dist === end_dist) return; + + for (let i = 0; i < conditions.length; i++) { + // const { dist: way_dist, value } = conditions[i] as any + const { way_dist, value } = conditions[i]; + + if (way_dist < start_dist) continue; + else if (way_dist > end_dist) return; + + const rgb = this.getRGBForValue(value); + const dist = (way_dist - start_dist) / (end_dist - start_dist); + + this._addWayColorGradient(gradient, new Edge(...rgb), dist, way_id); + } + } +} diff --git a/frontend/src/assets/hotline/Edge.ts b/frontend/src/assets/hotline/Edge.ts new file mode 100644 index 00000000..51ddaa3f --- /dev/null +++ b/frontend/src/assets/hotline/Edge.ts @@ -0,0 +1,27 @@ +export default class Edge { + r: number; + g: number; + b: number; + + constructor(r: number, g: number, b: number) { + this.r = r; + this.g = g; + this.b = b; + } + + add(o: Edge): Edge { + return new Edge(this.r + o.r, this.g + o.g, this.b + o.b); + } + + sub(o: Edge): Edge { + return new Edge(this.r - o.r, this.g - o.g, this.b - o.b); + } + + mul(fac: number): Edge { + return new Edge(this.r * fac, this.g * fac, this.b * fac); + } + + get(): [number, number, number] { + return [this.r, this.g, this.b]; + } +} diff --git a/frontend/src/assets/hotline/HoverHotPolyline.ts b/frontend/src/assets/hotline/HoverHotPolyline.ts new file mode 100644 index 00000000..18f0bc74 --- /dev/null +++ b/frontend/src/assets/hotline/HoverHotPolyline.ts @@ -0,0 +1,11 @@ +import { HotPolyline } from 'react-leaflet-hotline'; +import { DotHover } from '../graph/types'; + +export default class HoverHotPolyline extends HotPolyline { + setHover(dotHover: DotHover | undefined) { + if (this._canvas._hotline === undefined) return; + (this._canvas._hotline as any).dotHover = dotHover; + this._canvas._update(); + this.redraw(); + } +} diff --git a/frontend/src/assets/hotline/hotline.d.ts b/frontend/src/assets/hotline/hotline.d.ts new file mode 100644 index 00000000..f9c20498 --- /dev/null +++ b/frontend/src/assets/hotline/hotline.d.ts @@ -0,0 +1,8 @@ +// DistHotline +export interface DistPoint { + x: number; + y: number; + i: number; + way_dist: number; +} +export type DistData = DistPoint[]; diff --git a/frontend/src/context/GraphContext.tsx b/frontend/src/context/GraphContext.tsx new file mode 100644 index 00000000..6f8e3674 --- /dev/null +++ b/frontend/src/context/GraphContext.tsx @@ -0,0 +1,41 @@ +import { createContext, useContext } from 'react'; + +import useMinMaxAxis from '../hooks/useMinMaxAxis'; +import { AddMinMaxFunc, RemMinMaxFunc } from '../assets/graph/types'; + +interface ContextProps { + minX: number; + maxX: number; + minY: number; + maxY: number; + + addBounds: AddMinMaxFunc; + remBounds: RemMinMaxFunc; +} + +const GraphContext = createContext({} as ContextProps); + +// TODO: remove bounds / refactor? -> is it needed really? +// TODO: generalize DotHover into an "Event State" (to support for more events at once) +export const GraphProvider = ({ children }: any) => { + const { bounds, addBounds, remBounds } = useMinMaxAxis(); + + const { minX, maxX, minY, maxY } = bounds; + + return ( + + {children} + + ); +}; + +export const useGraph = () => useContext(GraphContext); diff --git a/frontend/src/context/GraphHoverContext.tsx b/frontend/src/context/GraphHoverContext.tsx new file mode 100644 index 00000000..df24ff21 --- /dev/null +++ b/frontend/src/context/GraphHoverContext.tsx @@ -0,0 +1,41 @@ +import { + createContext, + Dispatch, + SetStateAction, + useContext, + useState, +} from 'react'; + +import { Map } from 'leaflet'; + +import useMinMaxAxis from '../hooks/useMinMaxAxis'; +import { AddMinMaxFunc, DotHover, RemMinMaxFunc } from '../assets/graph/types'; + +interface HoverContextProps { + dotHover: DotHover | undefined; + setDotHover: Dispatch>; + map: Map | undefined; + setMap: Dispatch>; +} + +const HoverContext = createContext({} as HoverContextProps); + +export const HoverProvider = ({ children }: any) => { + const [dotHover, setDotHover] = useState(); + const [map, setMap] = useState(); + + return ( + + {children} + + ); +}; + +export const useHoverContext = () => useContext(HoverContext); diff --git a/frontend/src/css/slider.css b/frontend/src/css/slider.css new file mode 100644 index 00000000..f10d38c8 --- /dev/null +++ b/frontend/src/css/slider.css @@ -0,0 +1,83 @@ +.horizontal-slider { + width: 100%; + /* max-width: 500px; */ + height: 40px; + /* border: 1px solid grey; */ +} + +/* +.vertical-slider { + height: 380px; + width: 50px; + border: 1px solid grey; +} + + */ + +.example-thumb { + font-size: 0.9em; + text-align: center; + background-color: black; + color: white; + cursor: pointer; + border: 5px solid gray; + border-radius: 15px; + box-sizing: border-box; + top: 4px; + width: 50px; + height: 44px; + line-height: 34px; +} + +.example-thumb.active { + background-color: darkgrey; +} + +.example-track { + top: 24px; + height: 4px; + position: relative; + background: #ddd; +} + +.example-mark { + cursor: pointer; + bottom: 0; + width: 1.5px; + height: 8px; + background-color: #ddd; +} + +.example-track.example-track-1 { + background: blue; +} + +.example-track.example-track-2 { + background: #ddd; +} + +/* +.horizontal-slider .example-track { + top: 20px; + height: 10px; +} + +.horizontal-slider .example-thumb { + top: 1px; + width: 50px; + height: 48px; + line-height: 38px; +} +*/ + +.vertical-slider .example-thumb { + left: 1px; + width: 48px; + line-height: 40px; + height: 50px; +} + +.vertical-slider .example-track { + left: 20px; + width: 10px; +} diff --git a/frontend/src/hooks/useMinMax.tsx b/frontend/src/hooks/useMinMax.tsx new file mode 100644 index 00000000..5b1b85f8 --- /dev/null +++ b/frontend/src/hooks/useMinMax.tsx @@ -0,0 +1,42 @@ +import { useEffect, useState } from 'react'; +import { MinMax } from '../assets/graph/types'; + +type History = { [key: string]: [number, number] }; + +const resetMinMax = [ + Number.MAX_SAFE_INTEGER, + Number.MIN_SAFE_INTEGER, +] as MinMax; + +const useMinMax = (defaultInterval: MinMax) => { + const [bounds, setBounds] = useState(defaultInterval); + const [history, setHistory] = useState({}); + + const addInterval = (id: string, [nmin, nmax]: MinMax) => { + setHistory((prev) => ({ ...prev, [id]: [nmin, nmax] })); + }; + + const remInterval = (id: string) => { + setHistory((prev) => { + const temp = { ...prev }; + delete temp[id]; + return temp; + }); + }; + + useEffect(() => { + if (Object.keys(history).length === 0) return setBounds(defaultInterval); + + const newMinMax = Object.values(history).reduce( + ([accMin, accMax], [curMin, curMax]) => + [Math.min(accMin, curMin), Math.max(accMax, curMax)] as MinMax, + resetMinMax, + ); + + setBounds(newMinMax); + }, [history]); + + return { bounds, addInterval, remInterval }; +}; + +export default useMinMax; diff --git a/frontend/src/hooks/useMinMaxAxis.tsx b/frontend/src/hooks/useMinMaxAxis.tsx new file mode 100644 index 00000000..3c351941 --- /dev/null +++ b/frontend/src/hooks/useMinMaxAxis.tsx @@ -0,0 +1,40 @@ +import { MinMax } from '../assets/graph/types'; +import { Bounds } from '../models/path'; +import useMinMax from './useMinMax'; + +const defaultMinMax = [0, 10] as MinMax; + +const useMinMaxAxis = () => { + const { + bounds: boundsX, + addInterval: addIntervalX, + remInterval: remIntervalX, + } = useMinMax(defaultMinMax); + const { + bounds: boundsY, + addInterval: addIntervalY, + remInterval: remIntervalY, + } = useMinMax(defaultMinMax); + + const addBounds = (id: string, bounds: Required) => { + const { minX, maxX, minY, maxY } = bounds; + addIntervalX(id, [minX, maxX]); + addIntervalY(id, [minY, maxY]); + }; + + const remBounds = (id: string) => { + remIntervalX(id); + remIntervalY(id); + }; + + const bounds = { + minX: boundsX[0], + maxX: boundsX[1], + minY: boundsY[0], + maxY: boundsY[1], + }; + + return { bounds, addBounds, remBounds }; +}; + +export default useMinMaxAxis; diff --git a/frontend/src/hooks/useSize.tsx b/frontend/src/hooks/useSize.tsx new file mode 100644 index 00000000..9674d561 --- /dev/null +++ b/frontend/src/hooks/useSize.tsx @@ -0,0 +1,25 @@ +import { useEffect, useState } from 'react'; + +const useSize = (ref: React.MutableRefObject): [number, number] => { + const [width, setWidth] = useState(0); + const [height, setHeight] = useState(0); + + useEffect(() => { + if (ref.current === undefined) return; + + const updateSize = () => { + const { width, height } = (ref.current as any).getBoundingClientRect(); + setWidth(width); + setHeight(height); + }; + + updateSize(); + + window.addEventListener('resize', updateSize); + return () => window.removeEventListener('resize', updateSize); + }, [ref]); + + return [width, height]; +}; + +export default useSize; diff --git a/frontend/src/models/graph.ts b/frontend/src/models/graph.ts new file mode 100644 index 00000000..5517a20b --- /dev/null +++ b/frontend/src/models/graph.ts @@ -0,0 +1,9 @@ +// model and properties for conditions +// used in Condition(ML) page +export interface ConditionType { + name: string; + min: number; + max: number; + grid: boolean; + samples?: number; +} diff --git a/frontend/src/models/map.ts b/frontend/src/models/map.ts new file mode 100644 index 00000000..234ae1c0 --- /dev/null +++ b/frontend/src/models/map.ts @@ -0,0 +1,6 @@ +export interface MapBounds { + minLat: number; + maxLat: number; + minLng: number; + maxLng: number; +} diff --git a/frontend/src/models/models.ts b/frontend/src/models/models.ts new file mode 100644 index 00000000..f54e8d29 --- /dev/null +++ b/frontend/src/models/models.ts @@ -0,0 +1,38 @@ +// RideMeta data (props) + +export interface RideMeta { + TripId: string; + TaskId: number; + StartTimeUtc: string; // "2021-04-27T18:11:02.223Z" + EndTimeUtc: string; // "2021-04-27T18:57:18.551Z" + StartPositionLat: string; // "55.683240" + StartPositionLng: string; // "12.584890" + StartPositionDisplay: string; // "{\"ntk_geocode_time\":48…2,\"type\":\"geocode\"}" + EndPositionLat: string; // "55.711580" + EndPositionLng: string; // "12.570990" + EndPositionDisplay: string; // "{\"ntk_geocode_time\":31…"house_number\":\"37\"}" + Duration: string; // "2021-07-30T00:46:16.327Z" + DistanceKm: number; // 86.0269352289332 + FK_Device: string; // "d25574dd-e9a4-4296-ae00-7dcef3aa8278" + Created_Date: string; // "2021-07-30T07:52:47.969Z" + Updated_Date: string; // "0001-01-01T00:00:00.000Z" +} + +// export interface MeasurementData { +// T: string, +// lat: number, +// lon: number, +// message: string +// } + +export interface LatLng { + lat: number; + lng: number; +} + +export interface TripsOptions { + search: string; + startDate: Date; + endDate: Date; + reversed: boolean; +} diff --git a/frontend/src/models/path.ts b/frontend/src/models/path.ts new file mode 100644 index 00000000..7da6d71b --- /dev/null +++ b/frontend/src/models/path.ts @@ -0,0 +1,73 @@ +// Represents a point containing (lat, lng) coordinates, +import { LatLng } from './models'; +import { MeasProperties, PathProperties } from './properties'; +import { PathEventHandler } from './renderers'; + +// rendering properties, and optionally, a value and some metadata (like timestamp) +export interface PointData extends LatLng { + value?: number; + metadata?: any; +} + +// A Path is a collection of points +export type Path = PointData[]; + +export type Metadata = { [key: string]: any }; + +export interface Bounds { + minX?: number; + maxX?: number; + minY?: number; + maxY?: number; +} + +// measurement name -> trip task id -> bounded path (used in Rides) +export type MeasMetaPath = { [key: string]: { [key: number]: BoundedPath } }; + +// Props passed to the Path and EventPath components +export interface PathProps { + path: Path; + bounds?: Bounds; + properties: PathProperties; + metadata?: Metadata; + onClick?: PathEventHandler; +} + +// used for queries +export interface BoundedPath { + path: Path; + bounds?: Bounds; +} + +// This interface is used as a type for server's response +// for instance, JSON files follow this format +export interface JSONProps extends BoundedPath { + properties: MeasProperties; + metadata?: Metadata; +} + +export interface Node { + lat: number; + lng: number; + way_dist: number; +} + +export type Ways = { [key: string]: Node[] }; + +export interface ValueLatLng extends LatLng { + value: number; +} + +export interface Condition { + way_dist: number; + value: number; +} + +export type WayId = string; + +export interface WaysConditions { + way_lengths: number[]; + way_ids: WayId[]; + geometry: Node[][]; + conditions: Condition[][]; +} diff --git a/frontend/src/models/properties.ts b/frontend/src/models/properties.ts new file mode 100644 index 00000000..62e69b04 --- /dev/null +++ b/frontend/src/models/properties.ts @@ -0,0 +1,48 @@ +import { Palette } from 'react-leaflet-hotline'; +import { RendererName } from './renderers'; + +// Rendering properties of an entire Path +export interface PathProperties { + // Color of a path if a palette is not used + color?: string; + // Radius of a point or a line + width?: number; + // Weight (boldness) of a point or the entire path + weight?: number; + // Opacity (between 0 and 1) of the path + opacity?: number; + // The name of the renderer to use - see ./renderers for the list of names + rendererName: RendererName; + // Weight can be multiplied by the dilatationFactor + // < 1 -> shrinks ; > 1 -> grows ; == 1 -> stays the same + dilatationFactor?: number; + // Palette used for coloring the path and graph + palette?: Palette; + // Whether to show the arrow head or not, 0 no arrowhead, 1 = first dir, 2 = other dir, 3 = both dir + arrowHead?: number; +} + +export interface RendererOptions extends PathProperties { + min?: number; + max?: number; +} + +export interface MeasProperties extends PathProperties { + // measurement as it is in the database + dbName: string; + // human friendly name of the measurement + name: string; + xAxisType: XAxisType; + // Needs to be specified if the points have a value attached to them + hasValue?: boolean; +} + +export enum XAxisType { + distance = 'Dist [km]', + timeSec = 'Time [s]', + timeHMS = 'Time [h:m:s]', +} + +export interface ActiveMeasProperties extends MeasProperties { + isActive: boolean; // true if measurement is displayed, false otherwise +} diff --git a/frontend/src/models/renderers.ts b/frontend/src/models/renderers.ts new file mode 100644 index 00000000..8a584c59 --- /dev/null +++ b/frontend/src/models/renderers.ts @@ -0,0 +1,55 @@ +import { FC, ReactElement, ReactNode } from 'react'; +import { Hotline, Palette } from 'react-leaflet-hotline'; +import { LeafletEvent, LeafletEventHandlerFnMap } from 'leaflet'; + +import { LatLng } from './models'; +import { Path, Bounds } from './path'; +import { PathProperties, RendererOptions } from './properties'; +import { HEATMAP_OPTIONS, RENDERER_PALETTE } from '../Components/Map/constants'; + +export enum RendererName { + circles = 'circles', + rectangles = 'rectangles', + line = 'line', + hotline = 'hotline', + hotcircles = 'hotcircles', + heatmap = 'heatmap', +} + +export interface RendererType { + usePalette: boolean; + defaultPalette?: Palette; +} + +export const rendererTypes: { [key in RendererName]: RendererType } = { + circles: { usePalette: false }, + rectangles: { usePalette: false }, + line: { usePalette: false }, + hotline: { usePalette: true, defaultPalette: RENDERER_PALETTE() }, + hotcircles: { usePalette: true, defaultPalette: RENDERER_PALETTE() }, + heatmap: { usePalette: true, defaultPalette: HEATMAP_OPTIONS().palette }, +}; + +export type PathEventHandler = (i: number) => (e: LeafletEvent) => void; + +export type EventHandlers = { + [key in keyof LeafletEventHandlerFnMap]: PathEventHandler; +}; + +export interface IRenderer { + data: T[]; + getLat: (t: T, i: number) => number; + getLng: (t: T, i: number) => number; + getVal: (t: T, i: number) => number; + options: Required; + eventHandlers?: EventHandlers; +} + +export type Renderer = FC>; + +export interface PathRenderer { + path: Path; + properties: PathProperties; + bounds?: Bounds; + onClick?: PathEventHandler; +} diff --git a/frontend/src/pages/Conditions.tsx b/frontend/src/pages/Conditions.tsx new file mode 100644 index 00000000..c5981b93 --- /dev/null +++ b/frontend/src/pages/Conditions.tsx @@ -0,0 +1,13 @@ +import ConditionsMap from '../Components/Conditions/ConditionsMap'; + +const Conditions = () => { + return ( +
+ + {/* we could place some other stuff in the bottom, if need should be */} + +
+ ); +}; + +export default Conditions; diff --git a/frontend/src/pages/RoadConditions.tsx b/frontend/src/pages/RoadConditions.tsx new file mode 100644 index 00000000..ac71037e --- /dev/null +++ b/frontend/src/pages/RoadConditions.tsx @@ -0,0 +1,41 @@ +import { useState } from 'react'; +import { Palette } from 'react-leaflet-hotline'; +import { ChartData } from 'chart.js'; + +import ConditionsMap from '../Components/RoadConditions/ConditionsMap'; +import ConditionsGraph from '../Components/RoadConditions/ConditionsGraph'; + +import { ConditionType } from '../models/graph'; + +import { GraphProvider } from '../context/GraphContext'; + +import '../css/road_conditions.css'; + +const RoadConditions = () => { + const [palette, setPalette] = useState([]); + const [wayData, setWayData] = useState>(); + + const type: ConditionType = { + name: 'IRI', + min: 0, + max: 10, + grid: true, + samples: 40, + }; + + return ( + +
+ + +
+
+ ); +}; + +export default RoadConditions; diff --git a/frontend/src/queries/conditions.ts b/frontend/src/queries/conditions.ts new file mode 100644 index 00000000..78b2414a --- /dev/null +++ b/frontend/src/queries/conditions.ts @@ -0,0 +1,32 @@ +import { MapBounds } from '../models/map'; +import { Condition, WaysConditions } from '../models/path'; +import { asyncPost, post } from './fetch'; + +export const getWaysConditions = ( + type: string, + zoom: number, + setWays: (data: WaysConditions) => void, +) => { + post('/conditions/ways', { type, zoom }, setWays); +}; + +export const getConditions = ( + wayId: string, + type: string, + setConditions: (data: Condition[]) => void, +) => { + post('/conditions/way', { wayId, type }, setConditions); +}; + +export const getBoundedWaysConditions = async ( + bounds: MapBounds, + type: string, + zoom: number, +) => { + console.log(bounds); + return await asyncPost('/conditions/bounded/ways', { + ...bounds, + type, + zoom, + }); +}; diff --git a/frontend/src/queries/fetch.tsx b/frontend/src/queries/fetch.tsx new file mode 100644 index 00000000..d7197d94 --- /dev/null +++ b/frontend/src/queries/fetch.tsx @@ -0,0 +1,44 @@ +import axios, { AxiosResponse } from 'axios'; + +const development = + !process.env.NODE_ENV || process.env.NODE_ENV === 'development'; + +const devURL = process.env.REACT_APP_BACKEND_URL_DEV; +const prodURL = process.env.REACT_APP_BACKEND_URL_PROD; + +const getPath = (p: string) => (development ? devURL : prodURL) + p; + +export async function asyncPost( + path: string, + obj: object, +): Promise> { + return axios.get(getPath(path), { + params: obj, + paramsSerializer: (params) => + Object.keys(params) + .map((key: any) => new URLSearchParams(`${key}=${params[key]}`)) + .join('&'), + }); +} + +export function get(path: string, callback: (data: T) => void): void { + fetch(getPath(path)) + .then((res) => res.json()) + .then((data) => callback(data)); +} + +export function post( + path: string, + obj: object, + callback: (data: T) => void, +): void { + asyncPost(path, obj).then((res) => callback(res.data)); +} + +export const put = (path: string, obj: object): void => { + axios.put(getPath(path), obj); +}; + +export const deleteReq = (path: string): void => { + axios.delete(getPath(path)); +}; diff --git a/frontend/src/queries/fetchConditions.ts b/frontend/src/queries/fetchConditions.ts new file mode 100644 index 00000000..7b2f4d6e --- /dev/null +++ b/frontend/src/queries/fetchConditions.ts @@ -0,0 +1,17 @@ +// TODO ekki@dtu.dk: This should eventually be aligned with and reused from fetch + +import { FeatureCollection } from 'geojson'; + +const development = + !process.env.NODE_ENV || process.env.NODE_ENV === 'development'; + +const devURL = process.env.REACT_APP_BACKEND_URL_DEV; +const prodURL = process.env.REACT_APP_BACKEND_URL_PROD; + +const getPath = (p: string) => (development ? devURL : prodURL) + p; + +export function getConditions(callback: (data: FeatureCollection) => void) { + fetch(getPath('/conditions')).then((res) => + res.json().then((json) => callback(json)), + ); +}