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 (
);
-}
+};
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