diff --git a/package-lock.json b/package-lock.json index f10420e..2ca4730 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@mui/material": "^6.1.6", "@mui/styles": "^6.1.6", "@mui/x-date-pickers": "^7.26.0", + "@react-pdf/renderer": "^4.3.0", "@sentry/react": "^8.38.0", "@turf/area": "^7.2.0", "@turf/turf": "^7.2.0", @@ -3216,6 +3217,180 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "node_modules/@react-pdf/fns": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@react-pdf/fns/-/fns-3.1.2.tgz", + "integrity": "sha512-qTKGUf0iAMGg2+OsUcp9ffKnKi41RukM/zYIWMDJ4hRVYSr89Q7e3wSDW/Koqx3ea3Uy/z3h2y3wPX6Bdfxk6g==", + "license": "MIT" + }, + "node_modules/@react-pdf/font": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-pdf/font/-/font-4.0.2.tgz", + "integrity": "sha512-/dAWu7Y2RD1RxarDZ9SkYPHgBYOhmcDnet4W/qN/m8k+A2Hr3ja54GymSR7GGxWBtxjKtNauVKrTa9LS1n8WUw==", + "license": "MIT", + "dependencies": { + "@react-pdf/pdfkit": "^4.0.3", + "@react-pdf/types": "^2.9.0", + "fontkit": "^2.0.2", + "is-url": "^1.2.4" + } + }, + "node_modules/@react-pdf/image": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@react-pdf/image/-/image-3.0.3.tgz", + "integrity": "sha512-lvP5ryzYM3wpbO9bvqLZYwEr5XBDX9jcaRICvtnoRqdJOo7PRrMnmB4MMScyb+Xw10mGeIubZAAomNAG5ONQZQ==", + "license": "MIT", + "dependencies": { + "@react-pdf/png-js": "^3.0.0", + "jay-peg": "^1.1.1" + } + }, + "node_modules/@react-pdf/layout": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@react-pdf/layout/-/layout-4.4.0.tgz", + "integrity": "sha512-Aq+Cc6JYausWLoks2FvHe3PwK9cTuvksB2uJ0AnkKJEUtQbvCq8eCRb1bjbbwIji9OzFRTTzZij7LzkpKHjIeA==", + "license": "MIT", + "dependencies": { + "@react-pdf/fns": "3.1.2", + "@react-pdf/image": "^3.0.3", + "@react-pdf/primitives": "^4.1.1", + "@react-pdf/stylesheet": "^6.1.0", + "@react-pdf/textkit": "^6.0.0", + "@react-pdf/types": "^2.9.0", + "emoji-regex": "^10.3.0", + "queue": "^6.0.1", + "yoga-layout": "^3.2.1" + } + }, + "node_modules/@react-pdf/pdfkit": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@react-pdf/pdfkit/-/pdfkit-4.0.3.tgz", + "integrity": "sha512-k+Lsuq8vTwWsCqTp+CCB4+2N+sOTFrzwGA7aw3H9ix/PDWR9QksbmNg0YkzGbLAPI6CeawmiLHcf4trZ5ecLPQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@react-pdf/png-js": "^3.0.0", + "browserify-zlib": "^0.2.0", + "crypto-js": "^4.2.0", + "fontkit": "^2.0.2", + "jay-peg": "^1.1.1", + "linebreak": "^1.1.0", + "vite-compatible-readable-stream": "^3.6.1" + } + }, + "node_modules/@react-pdf/png-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-pdf/png-js/-/png-js-3.0.0.tgz", + "integrity": "sha512-eSJnEItZ37WPt6Qv5pncQDxLJRK15eaRwPT+gZoujP548CodenOVp49GST8XJvKMFt9YqIBzGBV/j9AgrOQzVA==", + "license": "MIT", + "dependencies": { + "browserify-zlib": "^0.2.0" + } + }, + "node_modules/@react-pdf/primitives": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@react-pdf/primitives/-/primitives-4.1.1.tgz", + "integrity": "sha512-IuhxYls1luJb7NUWy6q5avb1XrNaVj9bTNI40U9qGRuS6n7Hje/8H8Qi99Z9UKFV74bBP3DOf3L1wV2qZVgVrQ==", + "license": "MIT" + }, + "node_modules/@react-pdf/reconciler": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@react-pdf/reconciler/-/reconciler-1.1.4.tgz", + "integrity": "sha512-oTQDiR/t4Z/Guxac88IavpU2UgN7eR0RMI9DRKvKnvPz2DUasGjXfChAdMqDNmJJxxV26mMy9xQOUV2UU5/okg==", + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.1", + "scheduler": "0.25.0-rc-603e6108-20241029" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-pdf/reconciler/node_modules/scheduler": { + "version": "0.25.0-rc-603e6108-20241029", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-rc-603e6108-20241029.tgz", + "integrity": "sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==", + "license": "MIT" + }, + "node_modules/@react-pdf/render": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@react-pdf/render/-/render-4.3.0.tgz", + "integrity": "sha512-MdWfWaqO6d7SZD75TZ2z5L35V+cHpyA43YNRlJNG0RJ7/MeVGDQv12y/BXOJgonZKkeEGdzM3EpAt9/g4E22WA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@react-pdf/fns": "3.1.2", + "@react-pdf/primitives": "^4.1.1", + "@react-pdf/textkit": "^6.0.0", + "@react-pdf/types": "^2.9.0", + "abs-svg-path": "^0.1.1", + "color-string": "^1.9.1", + "normalize-svg-path": "^1.1.0", + "parse-svg-path": "^0.1.2", + "svg-arc-to-cubic-bezier": "^3.2.0" + } + }, + "node_modules/@react-pdf/renderer": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@react-pdf/renderer/-/renderer-4.3.0.tgz", + "integrity": "sha512-28gpA69fU9ZQrDzmd5xMJa1bDf8t0PT3ApUKBl2PUpoE/x4JlvCB5X66nMXrfFrgF2EZrA72zWQAkvbg7TE8zw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@react-pdf/fns": "3.1.2", + "@react-pdf/font": "^4.0.2", + "@react-pdf/layout": "^4.4.0", + "@react-pdf/pdfkit": "^4.0.3", + "@react-pdf/primitives": "^4.1.1", + "@react-pdf/reconciler": "^1.1.4", + "@react-pdf/render": "^4.3.0", + "@react-pdf/types": "^2.9.0", + "events": "^3.3.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "queue": "^6.0.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-pdf/stylesheet": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@react-pdf/stylesheet/-/stylesheet-6.1.0.tgz", + "integrity": "sha512-BGZ2sYNUp38VJUegjva/jsri3iiRGnVNjWI+G9dTwAvLNOmwFvSJzqaCsEnqQ/DW5mrTBk/577FhDY7pv6AidA==", + "license": "MIT", + "dependencies": { + "@react-pdf/fns": "3.1.2", + "@react-pdf/types": "^2.9.0", + "color-string": "^1.9.1", + "hsl-to-hex": "^1.0.0", + "media-engine": "^1.0.3", + "postcss-value-parser": "^4.1.0" + } + }, + "node_modules/@react-pdf/textkit": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@react-pdf/textkit/-/textkit-6.0.0.tgz", + "integrity": "sha512-fDt19KWaJRK/n2AaFoVm31hgGmpygmTV7LsHGJNGZkgzXcFyLsx+XUl63DTDPH3iqxj3xUX128t104GtOz8tTw==", + "license": "MIT", + "dependencies": { + "@react-pdf/fns": "3.1.2", + "bidi-js": "^1.0.2", + "hyphen": "^1.6.4", + "unicode-properties": "^1.4.1" + } + }, + "node_modules/@react-pdf/types": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@react-pdf/types/-/types-2.9.0.tgz", + "integrity": "sha512-ckj80vZLlvl9oYrQ4tovEaqKWP3O06Eb1D48/jQWbdwz1Yh7Y9v1cEmwlP8ET+a1Whp8xfdM0xduMexkuPANCQ==", + "license": "MIT", + "dependencies": { + "@react-pdf/font": "^4.0.2", + "@react-pdf/primitives": "^4.1.1", + "@react-pdf/stylesheet": "^6.1.0" + } + }, "node_modules/@remix-run/router": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.21.0.tgz", @@ -3735,6 +3910,15 @@ "sourcemap-codec": "^1.4.8" } }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", @@ -6597,6 +6781,12 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/abs-svg-path": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz", + "integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==", + "license": "MIT" + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -6858,6 +7048,35 @@ "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/bignumber.js": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", @@ -6889,6 +7108,24 @@ "node": ">=8" } }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "license": "MIT", + "dependencies": { + "pako": "~1.0.5" + } + }, "node_modules/browserslist": { "version": "4.24.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", @@ -7153,6 +7390,15 @@ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/clone-response": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", @@ -7190,9 +7436,18 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -7302,6 +7557,12 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, "node_modules/crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -7759,6 +8020,12 @@ "node": ">=0.4.0" } }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, "node_modules/doc-path": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/doc-path/-/doc-path-4.1.1.tgz", @@ -7820,6 +8087,12 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "license": "MIT" + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -8443,6 +8716,23 @@ } } }, + "node_modules/fontkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", + "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -9012,6 +9302,21 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" }, + "node_modules/hsl-to-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsl-to-hex/-/hsl-to-hex-1.0.0.tgz", + "integrity": "sha512-K6GVpucS5wFf44X0h2bLVRDsycgJmf9FF2elg+CrqD8GcFU8c6vYhgXn8NjUkFCwj+xDFb70qgLbTUm6sxwPmA==", + "license": "MIT", + "dependencies": { + "hsl-to-rgb-for-reals": "^1.1.0" + } + }, + "node_modules/hsl-to-rgb-for-reals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/hsl-to-rgb-for-reals/-/hsl-to-rgb-for-reals-1.1.1.tgz", + "integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==", + "license": "ISC" + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -9031,6 +9336,12 @@ "node": ">=10.19.0" } }, + "node_modules/hyphen": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/hyphen/-/hyphen-1.10.6.tgz", + "integrity": "sha512-fXHXcGFTXOvZTSkPJuGOQf5Lv5T/R2itiiCVPg9LxAje5D00O0pP83yJShFq5V89Ly//Gt6acj7z8pbBr34stw==", + "license": "ISC" + }, "node_modules/hyphenate-style-name": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", @@ -9121,7 +9432,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/internal-slot": { @@ -9561,6 +9871,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "license": "MIT" + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -9648,6 +9964,15 @@ "node": ">=10" } }, + "node_modules/jay-peg": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jay-peg/-/jay-peg-1.1.1.tgz", + "integrity": "sha512-D62KEuBxz/ip2gQKOEhk/mx14o7eiFRaU+VNNSP4MOiIkwb/D6B3G1Mfas7C/Fit8EsSV2/IWjZElx/Gs6A4ww==", + "license": "MIT", + "dependencies": { + "restructure": "^3.0.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9907,6 +10232,25 @@ "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.15.tgz", "integrity": "sha512-M7+rtYi9l5RvMmHyjyoF3BHHUpXTYdJ0PezZGHNs0GyW1lO+K7jxlXpbdIb7a56h0nqLYdjIw+E+z0ciGaJP7g==" }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -10112,6 +10456,12 @@ "node": ">= 0.4" } }, + "node_modules/media-engine": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/media-engine/-/media-engine-1.0.3.tgz", + "integrity": "sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==", + "license": "MIT" + }, "node_modules/memoize-one": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", @@ -10416,6 +10766,15 @@ "node": ">=10" } }, + "node_modules/normalize-svg-path": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz", + "integrity": "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==", + "license": "MIT", + "dependencies": { + "svg-arc-to-cubic-bezier": "^3.0.0" + } + }, "node_modules/normalize-url": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", @@ -10609,6 +10968,12 @@ "node": ">=6" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -10637,6 +11002,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-svg-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", + "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -10805,6 +11176,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, "node_modules/potpack": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz", @@ -10920,6 +11297,15 @@ "node": ">=6" } }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -11712,7 +12098,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11769,6 +12154,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", + "license": "MIT" + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -11877,7 +12268,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -12153,6 +12543,21 @@ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, "node_modules/skmeans": { "version": "0.9.7", "resolved": "https://registry.npmjs.org/skmeans/-/skmeans-0.9.7.tgz", @@ -12345,6 +12750,15 @@ "dev": true, "license": "MIT" }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -12535,6 +12949,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-arc-to-cubic-bezier": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz", + "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==", + "license": "ISC" + }, "node_modules/sweepline-intersections": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/sweepline-intersections/-/sweepline-intersections-1.5.0.tgz", @@ -12598,6 +13018,12 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -13024,6 +13450,16 @@ "node": ">=4" } }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, "node_modules/unicode-property-aliases-ecmascript": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", @@ -13034,6 +13470,22 @@ "node": ">=4" } }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, "node_modules/union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", @@ -13131,6 +13583,12 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -13739,6 +14197,20 @@ } } }, + "node_modules/vite-compatible-readable-stream": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/vite-compatible-readable-stream/-/vite-compatible-readable-stream-3.6.1.tgz", + "integrity": "sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/vite-node": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.7.tgz", @@ -14478,6 +14950,12 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT" } } } diff --git a/package.json b/package.json index 7878744..2446847 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@mui/material": "^6.1.6", "@mui/styles": "^6.1.6", "@mui/x-date-pickers": "^7.26.0", + "@react-pdf/renderer": "^4.3.0", "@sentry/react": "^8.38.0", "@turf/area": "^7.2.0", "@turf/turf": "^7.2.0", diff --git a/src/cableEstimator/binSVGs/sideView.tsx b/src/cableEstimator/binSVGs/sideView.tsx new file mode 100644 index 0000000..e3a1364 --- /dev/null +++ b/src/cableEstimator/binSVGs/sideView.tsx @@ -0,0 +1,373 @@ +import { colors } from "@mui/material"; +import { jsonBin } from "common/DataImports/BinCables/BinCableImporter"; +import { useEffect, useState } from "react"; +import { distanceConversion } from "utils"; + +interface Props { + bin: jsonBin; +} + +interface svgPoint { + x: number; + y: number; +} + +export default function SideView(props: Props) { + const svgViewBoxSize = 250; + //percentage to of the svg to be the bin drawing from 0 to 1, + //NOTE: this is the percentage based on the longest side + //ie. if the bin is taller than it is wide the bin will be x percent of the height of the viewbox and the width will be scaled to the height and vice versa + const scaleMultiplier = 0.8; + const nodeSpacing = 4; //space in feet between the nodes + const dimensionFontSize = 7; + const gapSize = svgViewBoxSize * 0.05; + const { bin } = props; + const [scale, setScale] = useState(0); //the scale to multiply the bin dimension by in order to determine how long to draw the line + const [hopperHeight, setHopperHeight] = useState(0); + const [roofHeight, setRoofHeight] = useState(0); + + useEffect(() => { + //get the peak height of the hopper + let h = (bin.Diameter / 2) * Math.tan(bin.HopperAngle * (Math.PI / 180)); + setHopperHeight(h); + setRoofHeight(bin.Peak - bin.Sidewall); + + let heightscale = svgViewBoxSize / (bin.Peak + h); + let widthscale = svgViewBoxSize / bin.Diameter; + + //need to use the scale of the longer of the two dimensions + setScale((heightscale < widthscale ? heightscale : widthscale) * scaleMultiplier); + }, [bin, svgViewBoxSize]); + + const binOutline = () => { + let startX = svgViewBoxSize / 2; + let startY = 0; + //let paddingVal = (padding * svgViewBoxSize) + + return ( + + ); + }; + + const cableLines = () => { + let cables: JSX.Element[] = []; + //let paddingVal = (padding * svgViewBoxSize) + + // line for the roof at the top of the sidewall + // cables.push( + // + // ) + + bin.CableSums.forEach(sum => { + let cableLength = 0; + bin.Cables.forEach(c => { + if (sum.Orbit === c.Orbit) { + //numNodes = c.Nodes + cableLength = c.Length; + } + }); + let numNodes = Math.ceil(cableLength / nodeSpacing); + + let xVal = svgViewBoxSize / 2 + sum.DistanceFromCenter * scale; + let cableStart = sum.DistanceFromCenter * Math.tan(bin.RoofAngle * (Math.PI / 180)) * scale; + //need to offset the padding to the top since we are now below the half mark as well as pad the bottom + let cableEnd = cableStart + cableLength * scale; + + cables.push( + + ); + + let svgSpacing = (cableEnd - cableStart) / numNodes; + + for (let i = 0; i < numNodes; i++) { + let nodeY = cableEnd - svgSpacing * i; + cables.push( + + ); + } + }); + return cables; + }; + + const dimensionText = (key: string, x: number, y: number, text: string) => { + return ( + + {text} + + ); + }; + + const dimensionPath = (key: string, start: svgPoint, corner: svgPoint, end: svgPoint) => { + return ( + // + + ); + }; + + const bottomDiameter = () => { + let d: JSX.Element[] = []; + + let x1 = svgViewBoxSize / 2 - (bin.Diameter / 2) * scale; + let x2 = svgViewBoxSize / 2 + (bin.Diameter / 2) * scale; + + let y1 = + (roofHeight + bin.Sidewall + hopperHeight) * scale + + svgViewBoxSize * (1 - scaleMultiplier) * 0.1; + let y2 = + (roofHeight + bin.Sidewall + hopperHeight) * scale + + svgViewBoxSize * (1 - scaleMultiplier) * 0.25; + + d.push( + dimensionText( + "bottomText", + svgViewBoxSize / 2, + y2, + distanceConversion(bin.Diameter).toFixed(1) + ) + ); + + d.push( + dimensionPath( + "bottomDiameterLine1", + { x: x1, y: y1 }, + { x: x1, y: y2 }, + { x: svgViewBoxSize / 2 - gapSize, y: y2 } + ) + ); + d.push( + dimensionPath( + "bottomDiameterLine2", + { x: svgViewBoxSize / 2 + gapSize, y: y2 }, + { x: x2, y: y2 }, + { x: x2, y: y1 } + ) + ); + + return d; + }; + + //height of the sidewall, on the left side - always used + const sidwallDimension = () => { + let d: JSX.Element[] = []; + let centerX = svgViewBoxSize / 2; + let x1 = centerX - (bin.Diameter / 2) * scale - svgViewBoxSize * (1 - scaleMultiplier) * 0.1; + + let x2 = x1 - svgViewBoxSize * (1 - scaleMultiplier) * 0.15; + let midpoint = (roofHeight + bin.Sidewall / 2) * scale; + + let y1 = roofHeight * scale; + let y2 = (roofHeight + bin.Sidewall) * scale; + + d.push( + dimensionText("sidewallText", x2, midpoint, distanceConversion(bin.Sidewall).toFixed(1)) + ); + d.push( + dimensionPath( + "sidwallTopLine", + { x: x1, y: y1 }, + { x: x2, y: y1 }, + { x: x2, y: midpoint - gapSize } + ) + ); + d.push( + dimensionPath( + "sidewallbottomLine", + { x: x2, y: midpoint + gapSize }, + { x: x2, y: y2 }, + { x: x1, y: y2 } + ) + ); + return d; + }; + + //height of the bottom of the sidewall to the peak of the bin, on the left side - only used for hopper bins + const peakDimension = () => { + let d: JSX.Element[] = []; + let centerX = svgViewBoxSize / 2; + let xWall = centerX - (bin.Diameter / 2) * scale - svgViewBoxSize * (1 - scaleMultiplier) * 0.1; + let xPeak = svgViewBoxSize / 2 - svgViewBoxSize * (1 - scaleMultiplier) * 0.1; + + let xText = xWall - svgViewBoxSize * (1 - scaleMultiplier) * 0.5; + let midpoint = ((roofHeight + bin.Sidewall) / 2) * scale; + + let yTop = 0; + let yBottom = (roofHeight + bin.Sidewall) * scale; + + d.push( + dimensionText( + "peakText", + xText, + midpoint, + distanceConversion(bin.Sidewall + roofHeight).toFixed(1) + ) + ); + d.push( + dimensionPath( + "peakTopLine", + { x: xPeak, y: yTop }, + { x: xText, y: yTop }, + { x: xText, y: midpoint - gapSize } + ) + ); + d.push( + dimensionPath( + "peakbottomLine", + { x: xText, y: midpoint + gapSize }, + { x: xText, y: yBottom }, + { x: xWall, y: yBottom } + ) + ); + + return d; + }; + + //total height of the bin, on the right side - always used + const totalDimension = () => { + let d: JSX.Element[] = []; + let centerX = svgViewBoxSize / 2; + //let paddingVal = (padding* svgViewBoxSize) + let binRightX = centerX + (bin.Diameter / 2) * scale; + + let xBinCenter = centerX + svgViewBoxSize * (1 - scaleMultiplier) * 0.1; + let xBinWall = binRightX + svgViewBoxSize * (1 - scaleMultiplier) * 0.1; + let x2 = binRightX + svgViewBoxSize * (1 - scaleMultiplier) * 0.2; + let midpoint = ((roofHeight + bin.Sidewall + hopperHeight) / 2) * scale; + + let y1 = 0; + let y2 = (roofHeight + bin.Sidewall + hopperHeight) * scale; + + d.push( + dimensionText( + "totalText", + x2, + midpoint, + distanceConversion(roofHeight + bin.Sidewall + hopperHeight).toFixed(1) + ) + ); + d.push( + dimensionPath( + "totalTopLine", + { x: xBinCenter, y: y1 }, + { x: x2, y: y1 }, + { x: x2, y: midpoint - gapSize } + ) + ); + d.push( + dimensionPath( + "totalBottomLine", + { x: x2, y: midpoint + gapSize }, + { x: x2, y: y2 }, + { x: hopperHeight > 0 ? xBinCenter : xBinWall, y: y2 } + ) + ); + + return d; + }; + + const binDimensions = () => { + let dimensions: JSX.Element[] = []; + + dimensions.push(...bottomDiameter()); + // sidwall Height + dimensions.push(...sidwallDimension()); + //total bin height + dimensions.push(...totalDimension()); + if (hopperHeight > 0) { + //show the height of the peak + sidwall without the hopper + dimensions.push(...peakDimension()); + } + + return dimensions; + }; + + return ( + + {cableLines()} + {binOutline()} + {binDimensions()} + + ); +} diff --git a/src/cableEstimator/binSVGs/topView.tsx b/src/cableEstimator/binSVGs/topView.tsx new file mode 100644 index 0000000..85690d2 --- /dev/null +++ b/src/cableEstimator/binSVGs/topView.tsx @@ -0,0 +1,73 @@ +import { colors } from "@mui/material"; +import { jsonBin } from "common/DataImports/BinCables/BinCableImporter"; + +interface Props { + bin: jsonBin; +} + +export default function TopView(props: Props) { + const { bin } = props; + const totalRadius = 25; + + const outerEdge = () => { + return ( + + ); + }; + + const orbits = () => { + let o: JSX.Element[] = []; + bin.CableSums.forEach((sum, i) => { + //get the radius of the bin + let binRadius = bin.Diameter / 2; + //determine the percentage the orbit is from the center + let perc = sum.DistanceFromCenter / binRadius; + //use the percentage to find the radius for the svg + let svgRadius = perc * totalRadius; + o.push( + + ); + //then determine the points the cables will be + let angleDist = 360 / sum.NumberOfCables; + let currentAngle = 90; + for (let k = 0; k < sum.NumberOfCables; k++) { + let x = svgRadius * Math.sin(currentAngle * (Math.PI / 180)); + let y = svgRadius * Math.cos(currentAngle * (Math.PI / 180)); + o.push( + + ); + currentAngle += angleDist; + } + }); + return o; + }; + + return ( + + {outerEdge()} + {orbits()} + + ); +} diff --git a/src/cableEstimator/cableDisplay.tsx b/src/cableEstimator/cableDisplay.tsx new file mode 100644 index 0000000..f64624d --- /dev/null +++ b/src/cableEstimator/cableDisplay.tsx @@ -0,0 +1,74 @@ +import { Box, Grid2 as Grid, Typography } from "@mui/material"; +import { jsonBin } from "common/DataImports/BinCables/BinCableImporter"; +import { useMobile } from "hooks"; +import { cloneDeep } from "lodash"; +import SideView from "./binSVGs/sideView"; +import TopView from "./binSVGs/topView"; +import CableMounting from "./cableMounting"; +import CableTable from "./cableTable"; + +interface Props { + bin: jsonBin; + updateBin: (newBin: jsonBin) => void; +} + +export default function CableDisplay(props: Props) { + const { bin, updateBin } = props; + const isMobile = useMobile(); + return ( + + + {bin.Manufacturer + " " + bin.Model} + + + + + + + + + + + + + + + + + + + + { + //update the json bin with the newly edited cables + let newBin = cloneDeep(bin); + //if the number of cables has changed will need to adjust the cable sums accordingly + let newCounts: Map = new Map(); + //loop through the cables to find the numbers for each orbit + newCables.forEach(cable => { + let currentOrbit = newCounts.get(cable.Orbit); + if (currentOrbit) { + newCounts.set(cable.Orbit, (currentOrbit + cable.Count) as number); + } else { + newCounts.set(cable.Orbit, cable.Count); + } + }); + //loop thorugh the sums setting the new counts for each orbit + newBin.CableSums.forEach(sum => { + let orbitCount = newCounts.get(sum.Orbit); + if (orbitCount && orbitCount !== sum.NumberOfCables) { + sum.NumberOfCables = orbitCount; + } + }); + newBin.Cables = newCables; + updateBin(newBin); + }} + /> + + + + + ); +} diff --git a/src/cableEstimator/cableEstimator.tsx b/src/cableEstimator/cableEstimator.tsx new file mode 100644 index 0000000..f587efd --- /dev/null +++ b/src/cableEstimator/cableEstimator.tsx @@ -0,0 +1,955 @@ +import { useEffect, useState } from "react"; +import { + Box, + Button, + Card, + FormControl, + FormControlLabel, + FormLabel, + Grid2 as Grid, + InputAdornment, + Radio, + RadioGroup, + Slider, + Switch, + TextField, + Typography +} from "@mui/material"; +//import MaterialTable from "material-table"; +//import { getTableIcons } from "common/ResponsiveTable"; +import CableDisplay from "./cableDisplay"; +import { distanceConversion, getDistanceUnit } from "utils"; +import { pond } from "protobuf-ts/pond"; +import CableQuote from "./cableQuote"; +import { useMobile } from "hooks"; +import { Cable, CableSum, jsonBin } from "common/DataImports/BinCables/BinCableImporter"; +import BinSelector from "bin/BinSelector"; +import { cloneDeep } from "lodash"; +import { + ConeVolume, + CylinderVolume, + RadiansToDegrees, + TriangleOppositeLength +} from "common/TrigFunctions"; +import ResponsiveTable, { Column } from "common/ResponsiveTable"; +import { makeStyles } from "@mui/styles"; + +const useStyles = makeStyles(() => ({ + sliderThumb: { + // height: 15, + // width: 15, + marginTop: 0, + marginLeft: 0, + //backgroundColor: "yellow" + }, + // sliderValLabel: { + // left: "calc(-50%)", + // top: 22, + // "& *": { + // background: "transparent", + // color: "#fff" + // } + // } +})) + +export default function CableEstimator() { + //the conversion constant to convert cubic feet to bushels + const conversionConstantFT = 0.7786; + const classes = useStyles(); + const [binOptions, setBinOptions] = useState([]); + const [displayedRows, setDisplayedRows] = useState([]); + const [tablePage, setTablePage] = useState(0) + const [pageSize, setPageSize] = useState(10) + const [rowCount, setRowCount] = useState(0) + const [selectedBin, setSelectedBin] = useState(); + const [quoteOpen, setQuoteOpen] = useState(false); + const [useCustomBin, setUseCustomBin] = useState(false); + const [diameterFormEntry, setDiameterFormEntry] = useState( + getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "9.1" : "30" + ); + //const [capacityFormEntry, setCapacityFormEntry] = useState("0"); + const [ringsFormEntry, setRingsFormEntry] = useState("0"); + const [ringHeightFormEntry, setRingHeightFormEntry] = useState("0"); + const [ringHeightFt, setRingHeightFt] = useState(0); + const [sidewallFormEntry, setSidewallFormEntry] = useState( + getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "7" : "23" + ); + const [peakFormEntry, setPeakFormEntry] = useState( + getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "9.1" : "30" + ); + //const [eaveToPeakFormEntry, setEaveToPeakFormEntry] = useState("0"); + const [roofAngleFormEntry, setRoofAngleFormEntry] = useState( + getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "7.6" : "25" + ); + const [hopperAngleFormEntry, setHopperAngleFormEntry] = useState("0"); + const nodeSpacing = 4; + const [customBin, setCustomBin] = useState({ + ID: 0, + Manufacturer: "", + Type: "Farm", //all bins are currently farm use + Bottom: "Flat Bottom", + Diameter: 30, + Name: "", + Model: "", + Capacity: 13942, + Rings: 0, + Sidewall: 23, + Peak: 30, //absolute total height from the ground to the peak of the cone + EaveToPeak: 0, //the length along the roof from the peak to the eave (not actually used anywhere at the moment) + RoofAngle: 25, + HopperAngle: 0, + CableSums: [], + Cables: [] + }); + const isMobile = useMobile(); + const [tableSearch, setTableSearch] = useState("") + const [loading, setLoading] = useState(false) + + const columns: Column[] = [ + { + title: "Manufacturer", + render: (row: jsonBin) => {row.Manufacturer} + }, + { + title: "Model", + render: (row: jsonBin) => {row.Model} + }, + { + title: "Capacity", + render: (row: jsonBin) => {row.Capacity} + }, + { + title: "Hopper Angle", + render: (row: jsonBin) => {row.HopperAngle} + }, + { + title: "Diameter " + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"), + render: (row: jsonBin) => {distanceConversion(row.Diameter).toFixed(2)} + }, + { + title: "Sidewall" + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"), + render: (row: jsonBin) => {distanceConversion(row.Sidewall).toFixed(2)} + }, + { + title: "Peak Height" + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"), + render: (row: jsonBin) => {distanceConversion(row.Peak).toFixed(2)} + }, + { + title: "Eave To Peak" + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"), + render: (row: jsonBin) => {distanceConversion(row.EaveToPeak).toFixed(2)} + }, + { + title: "Roof Angle", + render: (row: jsonBin) => {row.RoofAngle} + }, + { + title: "Rings", + render: (row: jsonBin) => {row.Rings} + } + ] + + useEffect(()=>{ + setLoading(true) + //create a clone to manipulate + let clone: jsonBin[] = cloneDeep(binOptions) + //having the search filter in here is quite inefficient as it will filter each time you change pages as well + //TODO: re-work the filter so it only filters when the search is changed + if(tableSearch !== ""){ + //filter the data in the cloned list + clone = clone.filter(bin => { + if(tableSearch.toLowerCase() === bin.Manufacturer.toLowerCase()) return true + if(tableSearch.toLowerCase() === bin.Model.toLowerCase()) return true + if(tableSearch.toLowerCase() === bin.Capacity.toString().toLowerCase()) return true + if(tableSearch.toLowerCase() === bin.HopperAngle.toString().toLowerCase()) return true + if(tableSearch.toLowerCase() === bin.Diameter.toString().toLowerCase()) return true + if(tableSearch.toLowerCase() === bin.Sidewall.toString().toLowerCase()) return true + if(tableSearch.toLowerCase() === bin.Peak.toString().toLowerCase()) return true + if(tableSearch.toLowerCase() === bin.EaveToPeak.toString().toLowerCase()) return true + if(tableSearch.toLowerCase() === bin.RoofAngle.toString().toLowerCase()) return true + if(tableSearch.toLowerCase() === bin.Rings.toString().toLowerCase()) return true + return false + }) + } + setRowCount(clone.length) + //split out the first x amount for the page + let rows = clone.splice(tablePage * pageSize, pageSize) + setDisplayedRows(rows) + setLoading(false) + },[binOptions, tablePage, pageSize, tableSearch]) + + const binOpTable = () => { + return ( + + title={Bins} + isLoading={loading} + setSearchText={(search) => {setTableSearch(search)}} + onRowClick={(bin) => { + setSelectedBin(bin); + }} + page={tablePage} + columns={columns} + pageSize={pageSize} + setPage={(page) => {setTablePage(page)}} + handleRowsPerPageChange={(e) => {setPageSize(e.target.value)}} + rows={displayedRows} + total={rowCount} + /> + ); + }; + + const cableSummaries = () => { + //numbers in the summaries are in feet + let summaries: CableSum[] = []; + if (customBin.Diameter < 24) { + summaries.push({ + Orbit: 0, + DistanceFromCenter: 2, + NumberOfCables: 1, + Bracket: 0 + }); + } else if (customBin.Diameter < 36) { + summaries.push({ + Orbit: 1, + DistanceFromCenter: 7, + NumberOfCables: 3, + Bracket: 0 + }); + } else if (customBin.Diameter < 42) { + summaries.push({ + Orbit: 0, + DistanceFromCenter: 2, + NumberOfCables: 1, + Bracket: 0 + }); + summaries.push({ + Orbit: 1, + DistanceFromCenter: 9, + NumberOfCables: 3, + Bracket: 0 + }); + } else if (customBin.Diameter < 48) { + summaries.push({ + Orbit: 0, + DistanceFromCenter: 2, + NumberOfCables: 1, + Bracket: 0 + }); + summaries.push({ + Orbit: 1, + DistanceFromCenter: 14, + NumberOfCables: 4, + Bracket: 0 + }); + } else if (customBin.Diameter < 54) { + summaries.push({ + Orbit: 0, + DistanceFromCenter: 2, + NumberOfCables: 1, + Bracket: 0 + }); + summaries.push({ + Orbit: 1, + DistanceFromCenter: 16, + NumberOfCables: 5, + Bracket: 0 + }); + } else if (customBin.Diameter < 60) { + summaries.push({ + Orbit: 0, + DistanceFromCenter: 2, + NumberOfCables: 1, + Bracket: 0 + }); + summaries.push({ + Orbit: 1, + DistanceFromCenter: 18, + NumberOfCables: 6, + Bracket: 0 + }); + } else if (customBin.Diameter < 72) { + summaries.push({ + Orbit: 0, + DistanceFromCenter: 2, + NumberOfCables: 1, + Bracket: 0 + }); + summaries.push({ + Orbit: 1, + DistanceFromCenter: 20, + NumberOfCables: 7, + Bracket: 0 + }); + } else if (customBin.Diameter < 78) { + summaries.push({ + Orbit: 1, + DistanceFromCenter: 10, + NumberOfCables: 4, + Bracket: 0 + }); + summaries.push({ + Orbit: 2, + DistanceFromCenter: 27, + NumberOfCables: 9, + Bracket: 0 + }); + } else if (customBin.Diameter < 90) { + summaries.push({ + Orbit: 1, + DistanceFromCenter: 10, + NumberOfCables: 4, + Bracket: 0 + }); + summaries.push({ + Orbit: 2, + DistanceFromCenter: 29, + NumberOfCables: 10, + Bracket: 0 + }); + } else if (customBin.Diameter < 105) { + summaries.push({ + Orbit: 0, + DistanceFromCenter: 2, + NumberOfCables: 1, + Bracket: 0 + }); + summaries.push({ + Orbit: 1, + DistanceFromCenter: 18, + NumberOfCables: 6, + Bracket: 0 + }); + summaries.push({ + Orbit: 2, + DistanceFromCenter: 36, + NumberOfCables: 12, + Bracket: 0 + }); + } else { + //diameter is 105 or larger + summaries.push({ + Orbit: 1, + DistanceFromCenter: 9, + NumberOfCables: 3, + Bracket: 0 + }); + summaries.push({ + Orbit: 2, + DistanceFromCenter: 26.5, + NumberOfCables: 9, + Bracket: 0 + }); + summaries.push({ + Orbit: 3, + DistanceFromCenter: 44, + NumberOfCables: 14, + Bracket: 0 + }); + } + + return summaries; + }; + + const buildCables = () => { + let bin = cloneDeep(customBin); + let binRadius = bin.Diameter / 2; + //use the diameter of the bin to build the cable summaries (orbit information) + let sums = cableSummaries(); + + //then use the orbits and the rest of the bins dimensions to build the cables + let hasCenter = false; + let roofRadians = bin.RoofAngle * (Math.PI / 180); + let hopperRadians = bin.HopperAngle * (Math.PI / 180); + let newCables: Cable[] = []; + + sums.forEach((sum, i) => { + if (i === 0) { + hasCenter = true; + } + let hopperHeight = 0; + // calculate the hight of the cone section of the cable here + let topHeight = (binRadius - sum.DistanceFromCenter) * Math.tan(roofRadians); + //if the hopper angle is greater than 0 calculate the hopper height for the orbit + if (bin.HopperAngle > 0) { + hopperHeight = (binRadius - sum.DistanceFromCenter) * Math.tan(hopperRadians); + } + + //get the maximum value for the top of the bin to the botton + let maxCablelength = Math.round((topHeight + bin.Sidewall + hopperHeight) * 10) / 10; + + //shorten cable for the clearance from the bottom and round the cable down to the nearest even number + let lenX10 = (maxCablelength - 2) * 10; + let cableLen = (lenX10 - (lenX10 % 20)) / 10; + + //set the bracket in the cableSum based on the length of the cable and whether the bin has a center cable + if (cableLen < 36) { + sum.Bracket = 3; + } else { + if (hasCenter) { + sum.Bracket = 2; + } else { + sum.Bracket = 1; + } + } + + // divide the cable by the node spacing to get the number of nodes + let sensors = Math.ceil(cableLen / nodeSpacing); + + //add in the cables for each orbit + //moisture cables + var moistureCables: Cable = { + Orbit: sum.Orbit, + Length: cableLen, + Nodes: sensors, + Type: 2, + Count: 1 + }; + newCables.push(moistureCables); + //temp Cables + if (sum.NumberOfCables > 1) { + let tempCables: Cable = { + Orbit: sum.Orbit, + Length: cableLen, + Nodes: sensors, + Type: 1, + Count: sum.NumberOfCables - 1 + }; + newCables.push(tempCables); + } + }); + + //add the summaries and cables to the bin + bin.CableSums = sums; + bin.Cables = newCables; + console.log(bin); + setCustomBin(bin); + }; + + const calcRoofAngle = (binRad: number, topConeHeight: number) => { + //calculate the angle using the the two given sides of a right triangle + let angle = RadiansToDegrees(Math.atan(topConeHeight / binRad)); //('arcTan(opposite / adjacent))' and convert to degrees + angle = Math.round(angle * 100) / 100; // round to 2 digits + return angle; + }; + + const calcConeHeight = (binRad: number, roofAngle: number) => { + return TriangleOppositeLength(binRad, roofAngle); + }; + + /** + * takes in the dimensions of the bin to calculate its bushel capacity + * @param diameter + * @param topConeHeight + * @param sidewallHeight + * @param hopperHeight + * @returns the bushel capacity of the bin with given dimensions + */ + const advancedGrainCalc = (bin: jsonBin) => { + let tcH = bin.Peak - bin.Sidewall; + let sH = bin.Sidewall; + let hH = TriangleOppositeLength(bin.Diameter / 2, bin.HopperAngle); //get the height of the hopper since it is not stored in the bin data + let d = bin.Diameter; + + //calculate the volumes and convert them to bushels + let topConeBushels = ConeVolume(d / 2, tcH) * conversionConstantFT; + let sidewallBushels = CylinderVolume(d / 2, sH) * conversionConstantFT; + let hopperBushels = ConeVolume(d / 2, hH ?? 0) * conversionConstantFT; + + return Math.round(topConeBushels + sidewallBushels + hopperBushels); + }; + + const customBinForm = () => { + return ( + + + {/* first row */} + + { + let bin = cloneDeep(customBin); + bin.Manufacturer = e.target.value; + setCustomBin(bin); + }} + /> + + + { + let bin = cloneDeep(customBin); + bin.Name = e.target.value; + setCustomBin(bin); + }} + /> + + + { + let bin = cloneDeep(customBin); + bin.Model = e.target.value; + setCustomBin(bin); + }} + /> + + {/* second row */} + + + {getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "m" : "ft"} + + ) + }} + value={diameterFormEntry} + error={isNaN(parseFloat(diameterFormEntry))} + onChange={e => { + setDiameterFormEntry(e.target.value); + let val = isNaN(parseFloat(e.target.value)) ? 0 : parseFloat(e.target.value); + if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) { + val = val * 3.281; + } + //when the diameter is changed use the current peak height to adjust the roof angle + let bin = cloneDeep(customBin); + bin.Diameter = val; + let newAngle = calcRoofAngle(bin.Diameter / 2, bin.Peak - bin.Sidewall); + setRoofAngleFormEntry(newAngle.toString()); + bin.RoofAngle = newAngle; + bin.Capacity = advancedGrainCalc(bin); + setCustomBin(bin); + }} + /> + + + + {getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "m" : "ft"} + + ) + }} + helperText={ + "total height from the bottom of the sidewall to the top of the roof, calculated from the diameter, roof angle, and sidewall height of the bin" + } + value={peakFormEntry} + error={isNaN(parseFloat(peakFormEntry))} + onChange={e => { + setPeakFormEntry(e.target.value); + let val = isNaN(parseFloat(e.target.value)) ? 0 : parseFloat(e.target.value); + if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) { + val = val * 3.281; + } + //when the peak height is changed use the diameter to calculate the roof angle + let bin = cloneDeep(customBin); + bin.Peak = val; + let newAngle = calcRoofAngle(bin.Diameter / 2, bin.Peak - bin.Sidewall); + setRoofAngleFormEntry(newAngle.toString()); + bin.RoofAngle = newAngle; + bin.Capacity = advancedGrainCalc(bin); + setCustomBin(bin); + }} + /> + + {/* third row */} + + { + setRingsFormEntry(e.target.value); + let bin = cloneDeep(customBin); + let val = isNaN(parseFloat(e.target.value)) ? 0 : parseFloat(e.target.value); + bin.Rings = val; + //use the new number of rings and ring height to get the sidewall height + let sidewallFt = val * ringHeightFt; + bin.Sidewall = sidewallFt; + if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) { + setSidewallFormEntry((sidewallFt / 3.281).toString()); + } else { + setSidewallFormEntry(sidewallFt.toString()); + } + //with the sidewall height changing the roof ange should also change + let newAngle = calcRoofAngle(bin.Diameter / 2, bin.Peak - bin.Sidewall); + setRoofAngleFormEntry(newAngle.toString()); + bin.RoofAngle = newAngle; + bin.Capacity = advancedGrainCalc(bin); + setCustomBin(bin); + }} + /> + + + {"in"} + }} + value={ringHeightFormEntry} + error={isNaN(parseFloat(ringHeightFormEntry))} + onChange={e => { + setRingHeightFormEntry(e.target.value); + let inVal = isNaN(parseFloat(e.target.value)) ? 0 : parseFloat(e.target.value); + // if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) { + // val = val * 3.281; + // } + let ringFt = inVal / 12; + setRingHeightFt(ringFt); + //based on this entry and the number of rings calculate the sidwall + let bin = cloneDeep(customBin); + let sidewallFt = bin.Rings * ringFt; + bin.Sidewall = sidewallFt; + if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) { + setSidewallFormEntry((sidewallFt / 3.281).toString()); + } else { + setSidewallFormEntry(sidewallFt.toString()); + } + //with the sidewall height changing the roof angle should also change with it + let newAngle = calcRoofAngle(bin.Diameter / 2, bin.Peak - bin.Sidewall); + setRoofAngleFormEntry(newAngle.toString()); + bin.RoofAngle = newAngle; + bin.Capacity = advancedGrainCalc(bin); + setCustomBin(bin); + }} + /> + + + + {getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "m" : "ft"} + + ) + }} + value={sidewallFormEntry} + error={isNaN(parseFloat(sidewallFormEntry))} + onChange={e => { + //need to handle conversions here + setSidewallFormEntry(e.target.value); + let val = isNaN(parseFloat(e.target.value)) ? 0 : parseFloat(e.target.value); + //if the value entered is meters convert it to feet for the bin + if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) { + val = val * 3.281; + } + let bin = cloneDeep(customBin); + bin.Sidewall = val; + let newAngle = calcRoofAngle(bin.Diameter / 2, bin.Peak - bin.Sidewall); + setRoofAngleFormEntry(newAngle.toString()); + bin.RoofAngle = newAngle; + bin.Capacity = advancedGrainCalc(bin); + setCustomBin(bin); + }} + /> + + {/* fourth row */} + + + { + let val = newValue as number; + setRoofAngleFormEntry(val.toString()); + let bin = cloneDeep(customBin); + bin.RoofAngle = val; + //adjusting the roof angle should also calculate the new top cone and adjust the peak height of the bin + let coneHeight = calcConeHeight(bin.Diameter / 2, bin.RoofAngle); + bin.Peak = bin.Sidewall + coneHeight; //assign it as feet + let peakHeightDisplay = bin.Sidewall + coneHeight; + if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) { + peakHeightDisplay = peakHeightDisplay / 3.281; + } + setPeakFormEntry(peakHeightDisplay.toFixed(1)); + bin.Capacity = advancedGrainCalc(bin); + setCustomBin(bin); + }} + /> + {"deg"} + }} + value={roofAngleFormEntry} + error={isNaN(parseFloat(roofAngleFormEntry))} + onChange={e => { + setRoofAngleFormEntry(e.target.value); + let bin = cloneDeep(customBin); + bin.RoofAngle = isNaN(parseFloat(e.target.value)) + ? 0 + : parseFloat(e.target.value); + //adjusting the roof angle should also calculate the new top cone and adjust the peak height of the bin + let coneHeight = calcConeHeight(bin.Diameter / 2, bin.RoofAngle); + bin.Peak = bin.Sidewall + coneHeight; + let peakHeightDisplay = bin.Sidewall + coneHeight; + if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) { + peakHeightDisplay = peakHeightDisplay / 3.281; + } + setPeakFormEntry(peakHeightDisplay.toFixed(1)); + bin.Capacity = advancedGrainCalc(bin); + setCustomBin(bin); + }} + /> + + + + + { + setHopperAngleFormEntry(newValue.toString()); + let bin = cloneDeep(customBin); + bin.HopperAngle = newValue as number; + bin.Capacity = advancedGrainCalc(bin); + setCustomBin(bin); + }} + /> + {"deg"} + }} + value={hopperAngleFormEntry} + error={isNaN(parseFloat(hopperAngleFormEntry))} + onChange={e => { + setHopperAngleFormEntry(e.target.value); + let bin = cloneDeep(customBin); + bin.HopperAngle = isNaN(parseFloat(e.target.value)) + ? 0 + : parseFloat(e.target.value); + bin.Capacity = advancedGrainCalc(bin); + setCustomBin(bin); + }} + /> + + + {/* bottom */} + + + Shape + { + let bin = cloneDeep(customBin); + bin.Bottom = value; + if (value === "Hopper Bottom") { + bin.HopperAngle = 25; + setHopperAngleFormEntry("25"); + } else { + bin.HopperAngle = 0; + setHopperAngleFormEntry("0"); + } + bin.Capacity = advancedGrainCalc(bin); + setCustomBin(bin); + }}> + } + label={"Flat Bottom"} + /> + } + label={"Hopper"} + /> + + + + + {/* not strictly necessary for quoting */} + {/* { + setCapacityFormEntry(e.target.value); + let bin = cloneDeep(customBin); + bin.Capacity = isNaN(parseFloat(e.target.value)) ? 0 : parseFloat(e.target.value); + setCustomBin(bin); + }} + /> */} + {/* this dimension isn't actually used for anything */} + {/* { + //need to handle conversions here + setEaveToPeakFormEntry(e.target.value); + let val = isNaN(parseFloat(e.target.value)) ? 0 : parseFloat(e.target.value); + //if the value entered is meters convert it to feet for the bin + if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) { + val = val * 3.281; + } + let bin = cloneDeep(customBin); + bin.EaveToPeak = val; + setCustomBin(bin); + }} + /> */} + + + Estimated Capacity: {customBin.Capacity} bushels + + + ); + }; + + const binDisplay = () => { + let displayBin = selectedBin; + if (useCustomBin) { + displayBin = customBin; + } + if (displayBin) { + return ( + { + setSelectedBin(newBin); + }} + /> + ); + } + }; + + const quoteDisplay = () => { + let displayBin = selectedBin; + if (useCustomBin) { + displayBin = customBin; + } + if (displayBin) { + return ( + { + setQuoteOpen(false); + }} + /> + ); + } + }; + + return ( + + { + setUseCustomBin(checked); + }} + name="storage" + /> + } + /> + + {/* grid of select boxes for pre-built options */} + + {!useCustomBin && ( + + { + setBinOptions(ops); + }} + /> + + )} + + + + + {useCustomBin ? customBinForm() : binOpTable()} + {/* summary of cable data */} + {binDisplay()} + {quoteDisplay()} + + ); +} diff --git a/src/cableEstimator/cableMounting.tsx b/src/cableEstimator/cableMounting.tsx new file mode 100644 index 0000000..0d239e2 --- /dev/null +++ b/src/cableEstimator/cableMounting.tsx @@ -0,0 +1,71 @@ +import { Box, Typography } from "@mui/material"; +import { jsonBin, CableSum, getBracket } from "common/DataImports/BinCables/BinCableImporter"; +import ResponsiveTable, { Column } from "common/ResponsiveTable"; +//import { getTableIcons } from "common/ResponsiveTable"; +//import MaterialTable from "material-table"; +import { pond } from "protobuf-ts/pond"; +import { distanceConversion, getDistanceUnit } from "utils"; + +interface Props { + bin: jsonBin; +} + +export default function CableMounting(props: Props) { + const { bin } = props; + // const [tablePage, setTablePage] = useState(0) + // const [pageSize, setPageSize] = useState(10) + + const roofDist = (distance: number) => { + return distance / Math.cos(bin.RoofAngle * (Math.PI / 180)); + }; + + const columns: Column[] = [ + { + title: "Orbit", + render: (row: CableSum) => {row.Orbit === 0 ? "Center" : row.Orbit} + }, + { + title: "From Center " + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"), + render: (row: CableSum) => ( + {distanceConversion(row.DistanceFromCenter).toFixed(2)} + ) + }, + { + title: "Along Roof" + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"), + render: (row: CableSum) => ( + {distanceConversion(roofDist(row.DistanceFromCenter)).toFixed(2)} + ) + }, + { + title: "Bracket", + render: (row: CableSum) => { + let bracketType = "None" + let bracket = getBracket(row.Bracket) + if(row.Orbit !== 0 && bracket !== undefined) { + bracketType = bracket.Name + " (" + row.NumberOfCables + ")" + }else { + bracketType = "Unknown" + } + return ( + + {bracketType} + + ) + } + } + ] + + return ( + + title={Install Guide} + rows={bin.CableSums} + columns={columns} + page={0} + pageSize={0} + handleRowsPerPageChange={()=>{}} + setPage={()=>{}} + total={0} + hidePagination + /> + ) +} diff --git a/src/cableEstimator/cableQuote.tsx b/src/cableEstimator/cableQuote.tsx new file mode 100644 index 0000000..ae37870 --- /dev/null +++ b/src/cableEstimator/cableQuote.tsx @@ -0,0 +1,52 @@ +import { Button, DialogActions, DialogContent } from "@mui/material"; +import { PDFViewer, StyleSheet } from "@react-pdf/renderer"; +import AgPDFTemplate from "common/pdfTemplates/Agriculture/agPDFTemplate"; +import ResponsiveDialog from "common/ResponsiveDialog"; +import { useMobile } from "hooks"; +import { jsonBin } from "common/DataImports/BinCables/BinCableImporter"; +import PdfContent from "./pdfComponents/pdfContent"; + +interface Props { + bin: jsonBin; + open: boolean; + close: () => void; +} + +export default function CableQuote(props: Props) { + const { open, close, bin } = props; + const isMobil = useMobile(); + const pdfStyle = StyleSheet.create({ + viewerDesktop: { + height: 500 * 1.44, + width: 500 + }, + viewerMobile: { + height: "100%", + width: "100%" + } + }); + + const pdfDoc = () => { + return } />; + }; + + return ( + + + + {pdfDoc()} + + + + + + + ); +} diff --git a/src/cableEstimator/cableTable.tsx b/src/cableEstimator/cableTable.tsx new file mode 100644 index 0000000..ee73c55 --- /dev/null +++ b/src/cableEstimator/cableTable.tsx @@ -0,0 +1,142 @@ +import { Box, Typography } from "@mui/material"; +//import { getTableIcons } from "common/ResponsiveTable"; +//import MaterialTable from "material-table"; +import { pond } from "protobuf-ts/pond"; +import { useEffect, useState } from "react"; +import { distanceConversion, getDistanceUnit } from "utils"; +import { Cable, jsonBin } from "common/DataImports/BinCables/BinCableImporter"; +import { cloneDeep } from "lodash"; +import ResponsiveTable, { Column } from "common/ResponsiveTable"; + +interface Props { + bin: jsonBin; + cablesChanged: (newCables: Cable[]) => void; +} + +export default function CableTable(props: Props) { + const { bin, /*cablesChanged*/ } = props; + //const nodeSpacing = 4; //space between the nodes in feet + + const [cableData, setCableData] = useState(bin.Cables); + + useEffect(() => { + let convertedCables: Cable[] = cloneDeep(bin.Cables); + convertedCables.forEach(cable => { + cable.Length = Math.round(distanceConversion(cable.Length) * 100) / 100; + }); + setCableData(convertedCables); + }, [bin]); + + // const table = () => { + // return ( + // Recommended Cables + // } + // icons={getTableIcons()} + // editable={{ + // onRowUpdate: (newCable, oldCable) => + // new Promise((resolve, reject) => { + // const dataUpdate = [...cableData]; + // if (oldCable) { + // //make sure the cables lengths are in feet + // if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) { + // newCable.Length = newCable.Length * 3.281; + // oldCable.Length = oldCable.Length * 3.281; + // } + // //if the length was eddited + // if (oldCable.Length !== newCable.Length) { + // //recalc the number of nodes + // newCable.Nodes = Math.ceil(newCable.Length / nodeSpacing); + // } + // const index = dataUpdate.indexOf(oldCable); + // //this line seems ridiculous, however the browser seems to see newCable.Count as a string if it was the one that was edited in the table + // newCable.Count = parseInt(newCable.Count.toString()); + // dataUpdate[index] = newCable; + // } + // resolve(cablesChanged(dataUpdate)); + // }) + // }} + // columns={[ + // { + // title: Orbit, + // render: (row: Cable) =>
{row.Orbit === 0 ? "Center" : row.Orbit.toString()}
+ // }, + // { + // title: Type, + // render: (row: Cable) =>
{row.Type === 2 ? "Moisture" : "Temperature"}
+ // }, + // { + // title: ( + // Number of Cables + // ), + // field: "Count", + // initialEditValue: 0, + // type: "numeric" + // }, + // { + // title: ( + // + // Length{getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"} + // + // ), + // field: "Length", + // initialEditValue: 0, + // type: "numeric" + // }, + // { + // title: ( + // Nodes Per Cable + // ), + // editable: "never", + // type: "numeric", + // field: "Nodes" + // } + // ]} + // options={{ + // search: false, + // paging: false + // }} + // /> + // ); + // }; + + const columns: Column[] = [ + { + title: "Orbit", + render: (row: Cable) => {row.Orbit === 0 ? "Center" : row.Orbit.toString()} + }, + { + title: "Type", + render: (row: Cable) => {row.Type === 2 ? "Moisture" : "Temperature"} + }, + { + title: "Number of Cables", + render: (row) => {row.Count} + }, + { + title: "Length" + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"), + render: (row) => {row.Length} + }, + { + title: "Nodes Per Cable", + render: (row) => {row.Nodes} + } + ] + + //TODO: in order to add back in the editable functionality will need to add that to the responsive table + return ( + Recommended Cables} + columns={columns} + rows={cableData} + page={0} + pageSize={0} + total={0} + setPage={()=>{}} + handleRowsPerPageChange={()=>{}} + hidePagination + /> + ); +} diff --git a/src/cableEstimator/pdfComponents/cableTable/cableTableHeader.tsx b/src/cableEstimator/pdfComponents/cableTable/cableTableHeader.tsx new file mode 100644 index 0000000..79f95ad --- /dev/null +++ b/src/cableEstimator/pdfComponents/cableTable/cableTableHeader.tsx @@ -0,0 +1,43 @@ +import { StyleSheet, Text, View } from "@react-pdf/renderer"; +import { pond } from "protobuf-ts/pond"; +import { getDistanceUnit } from "utils"; + +interface Props { + borderColour: string; +} + +export default function CableTableHeader(props: Props) { + const borderColor = props.borderColour; + const styles = StyleSheet.create({ + view: { + flexDirection: "row", + borderBottomColor: borderColor, + borderBottomWidth: 1, + alignItems: "center", + height: 24, + textAlign: "center", + fontFamily: "Helvetica-Bold", + flexGrow: 1 + }, + cell: { + width: "20%", + borderRightColor: borderColor, + borderRightWidth: 1 + }, + endCell: { + width: "20%" + } + }); + + return ( + + Orbit + Type + Qty + + Length {getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"} + + Nodes + + ); +} \ No newline at end of file diff --git a/src/cableEstimator/pdfComponents/cableTable/cableTableRow.tsx b/src/cableEstimator/pdfComponents/cableTable/cableTableRow.tsx new file mode 100644 index 0000000..8e80d6a --- /dev/null +++ b/src/cableEstimator/pdfComponents/cableTable/cableTableRow.tsx @@ -0,0 +1,60 @@ +import { StyleSheet, Text, View } from "@react-pdf/renderer"; +import { jsonBin } from "common/DataImports/BinCables/BinCableImporter"; +import React from "react"; +import { distanceConversion } from "utils"; + +interface Props { + bin: jsonBin; + borderColour: string; +} + +export default function CableTableRow(props: Props) { + const styles = StyleSheet.create({ + view: { + flexDirection: "row", + borderBottomColor: props.borderColour, + borderBottomWidth: 1, + alignItems: "center", + height: 24, + fontStyle: "normal", + fontWeight: 650 + }, + cell: { + width: "20%", + textAlign: "left", + borderRightColor: props.borderColour, + borderRightWidth: 1, + paddingLeft: 8 + }, + endCell: { + width: "20%", + paddingLeft: 8 + } + }); + + const rows = () => { + let r: JSX.Element[] = []; + props.bin.Cables.forEach((cable, i) => { + r.push( + + {cable.Orbit === 0 ? "Center" : cable.Orbit} + {cable.Type === 2 ? "Moisture" : "Temperature"} + {cable.Count} + {distanceConversion(cable.Length).toFixed(2)} + {cable.Nodes} + + ); + }); + return r; + }; + + return {rows()}; +} \ No newline at end of file diff --git a/src/cableEstimator/pdfComponents/mountingTable/mountingTableHeader.tsx b/src/cableEstimator/pdfComponents/mountingTable/mountingTableHeader.tsx new file mode 100644 index 0000000..a7708dd --- /dev/null +++ b/src/cableEstimator/pdfComponents/mountingTable/mountingTableHeader.tsx @@ -0,0 +1,54 @@ +import { StyleSheet, Text, View } from "@react-pdf/renderer"; +import { pond } from "protobuf-ts/pond"; +import { getDistanceUnit } from "utils"; + +interface Props { + borderColour: string; +} + +export default function MountingTableHeader(props: Props) { + const borderColor = props.borderColour; + const styles = StyleSheet.create({ + view: { + flexDirection: "row", + borderBottomColor: borderColor, + borderBottomWidth: 1, + alignItems: "center", + height: 24, + textAlign: "center", + fontFamily: "Helvetica-Bold", + flexGrow: 1 + }, + orbit: { + width: "14%", + borderRightColor: borderColor, + borderRightWidth: 1 + }, + centerDist: { + width: "25%", + borderRightColor: borderColor, + borderRightWidth: 1 + }, + roofDist: { + width: "33%", + borderRightColor: borderColor, + borderRightWidth: 1 + }, + bracket: { + width: "28%" + } + }); + + return ( + + Orbit + + Radius {getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"} + + + Along Roof {getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"} + + Bracket + + ); +} diff --git a/src/cableEstimator/pdfComponents/mountingTable/mountingTableRows.tsx b/src/cableEstimator/pdfComponents/mountingTable/mountingTableRows.tsx new file mode 100644 index 0000000..b86f1d1 --- /dev/null +++ b/src/cableEstimator/pdfComponents/mountingTable/mountingTableRows.tsx @@ -0,0 +1,83 @@ +import { StyleSheet, Text, View } from "@react-pdf/renderer"; +import { getBracket, jsonBin } from "common/DataImports/BinCables/BinCableImporter"; +import React from "react"; +import { distanceConversion } from "utils"; + +interface Props { + bin: jsonBin; + borderColour: string; +} + +export default function MountingTableRow(props: Props) { + const styles = StyleSheet.create({ + orbit: { + width: "14%", + textAlign: "left", + borderRightColor: props.borderColour, + borderRightWidth: 1, + paddingLeft: 8 + }, + centerDist: { + width: "25%", + textAlign: "left", + borderRightColor: props.borderColour, + borderRightWidth: 1, + paddingLeft: 8 + }, + roofDist: { + width: "33%", + textAlign: "left", + paddingLeft: 8, + borderRightColor: props.borderColour, + borderRightWidth: 1 + }, + bracket: { + width: "28%", + textAlign: "center", + fontSize: 8 + } + }); + + const roofDist = (distance: number) => { + return distance / Math.cos(props.bin.RoofAngle * (Math.PI / 180)); + }; + + const rows = () => { + let r: JSX.Element[] = []; + props.bin.CableSums.forEach((orbit, i) => { + let bracketType = "None" + let bracket = getBracket(orbit.Bracket) + if(orbit.Orbit !== 0 && bracket !== undefined) { + bracketType = bracket.Name + " (" + orbit.NumberOfCables + ")" + }else { + bracketType = "Unk" + } + + r.push( + + {orbit.Orbit === 0 ? "C" : orbit.Orbit} + + {distanceConversion(orbit.DistanceFromCenter).toFixed(2)} + + + {distanceConversion(roofDist(orbit.DistanceFromCenter)).toFixed(2)} + + + {bracketType} + + + ); + }); + return r; + }; + + return {rows()}; +} \ No newline at end of file diff --git a/src/cableEstimator/pdfComponents/pdfContent.tsx b/src/cableEstimator/pdfComponents/pdfContent.tsx new file mode 100644 index 0000000..bce1f15 --- /dev/null +++ b/src/cableEstimator/pdfComponents/pdfContent.tsx @@ -0,0 +1,81 @@ +import { StyleSheet, Text, View } from "@react-pdf/renderer"; +import { colors } from "@mui/material"; +import { jsonBin } from "common/DataImports/BinCables/BinCableImporter"; +import CableTableHeader from "./cableTable/cableTableHeader"; +import CableTableRow from "./cableTable/cableTableRow"; +import PDFSideView from "./pdfSVG/pdfSideView"; +import PDFTopView from "./pdfSVG/pdfTopView"; +import MountingTableHeader from "./mountingTable/mountingTableHeader"; +import MountingTableRow from "./mountingTable/mountingTableRows"; + +interface Props { + bin: jsonBin; +} + +export default function PdfContent(props: Props) { + const tableBorderColour = colors.grey[800]; + + const styles = StyleSheet.create({ + general: { + marginTop: 2, + flexDirection: "row", + borderTop: "1px solid black", + borderBottom: "1px solid black", + paddingTop: 12, + paddingBottom: 12, + fontFamily: "Helvetica-Bold", + fontSize: 20, + textAlign: "center" + }, + svgLabels: { + marginTop: 2, + flexDirection: "row", + borderTop: "1px solid black", + paddingTop: 12, + textAlign: "left", + fontFamily: "Helvetica-Bold" + }, + cableTable: { + borderColor: tableBorderColour, + borderWidth: 1 + } + }); + return ( + + + {props.bin.Manufacturer} + {props.bin.Model} + {props.bin.Bottom} + + + Side View + Top View + + + + + + + + + + + Install Guide + + + + + + + + + Recommended Cables + + + + + + + ); +} \ No newline at end of file diff --git a/src/cableEstimator/pdfComponents/pdfSVG/pdfSideView.tsx b/src/cableEstimator/pdfComponents/pdfSVG/pdfSideView.tsx new file mode 100644 index 0000000..cad1209 --- /dev/null +++ b/src/cableEstimator/pdfComponents/pdfSVG/pdfSideView.tsx @@ -0,0 +1,387 @@ +import { colors } from "@mui/material"; +import { Circle, Line, Path, Svg, Text } from "@react-pdf/renderer"; +import { jsonBin } from "common/DataImports/BinCables/BinCableImporter"; +import { useEffect, useState } from "react"; +import { distanceConversion } from "utils"; + +interface Props { + bin: jsonBin; +} + +interface svgPoint { + x: number; + y: number; +} + +export default function PDFSideView(props: Props) { + const svgViewBoxSize = 500; + //percentage to of the svg to be the bin drawing from 0 to 1, + //NOTE: this is the percentage based on the longest side + //ie. if the bin is taller than it is wide the bin will be x percent of the height of the viewbox and the width will be scaled to the height and vice versa + const scaleMultiplier = 0.8; + const nodeSpacing = 4; //space in feet between the nodes + const dimensionFontSize = 18; + const gapSize = svgViewBoxSize * 0.05; + const { bin } = props; + const [scale, setScale] = useState(0); //the scale to multiply the bin dimension by in order to determine how long to draw the line + const [hopperHeight, setHopperHeight] = useState(0); + const [roofHeight, setRoofHeight] = useState(0); + + useEffect(() => { + //get the peak height of the hopper + let h = (bin.Diameter / 2) * Math.tan(bin.HopperAngle * (Math.PI / 180)); + setHopperHeight(h); + setRoofHeight(bin.Peak - bin.Sidewall); + + let heightscale = svgViewBoxSize / (bin.Peak + h); + let widthscale = svgViewBoxSize / bin.Diameter; + + //need to use the scale of the longer of the two dimensions + setScale((heightscale < widthscale ? heightscale : widthscale) * scaleMultiplier); + }, [bin, svgViewBoxSize]); + + const binOutline = () => { + let startX = svgViewBoxSize / 2; + let startY = 0; + //let paddingVal = (padding * svgViewBoxSize) + + return ( + + ); + }; + + const cableLines = () => { + let cables: JSX.Element[] = []; + //let paddingVal = (padding * svgViewBoxSize) + + // line for the roof at the top of the sidewall + // cables.push( + // + // ) + + bin.CableSums.forEach(sum => { + let cableLength = 0; + bin.Cables.forEach(c => { + if (sum.Orbit === c.Orbit) { + //numNodes = c.Nodes + cableLength = c.Length; + } + }); + let numNodes = Math.ceil(cableLength / nodeSpacing); + + let xVal = svgViewBoxSize / 2 + sum.DistanceFromCenter * scale; + let cableStart = sum.DistanceFromCenter * Math.tan(bin.RoofAngle * (Math.PI / 180)) * scale; + //need to offset the padding to the top since we are now below the half mark as well as pad the bottom + let cableEnd = cableStart + cableLength * scale; + + cables.push( + + ); + + let svgSpacing = (cableEnd - cableStart) / numNodes; + + for (let i = 0; i < numNodes; i++) { + let nodeY = cableEnd - svgSpacing * i; + cables.push( + + ); + } + }); + return cables; + }; + + const dimensionText = (key: string, x: number, y: number, text: string) => { + return ( + + {text} + + ); + }; + + const dimensionPath = (key: string, start: svgPoint, corner: svgPoint, end: svgPoint) => { + return ( + + ); + }; + + const bottomDiameter = () => { + let d: JSX.Element[] = []; + + let x1 = svgViewBoxSize / 2 - (bin.Diameter / 2) * scale; + let x2 = svgViewBoxSize / 2 + (bin.Diameter / 2) * scale; + + let y1 = + (roofHeight + bin.Sidewall + hopperHeight) * scale + + svgViewBoxSize * (1 - scaleMultiplier) * 0.1; + let y2 = + (roofHeight + bin.Sidewall + hopperHeight) * scale + + svgViewBoxSize * (1 - scaleMultiplier) * 0.25; + + d.push( + dimensionText( + "bottomText", + svgViewBoxSize / 2, + y2, + distanceConversion(bin.Diameter).toFixed(1) + ) + ); + + d.push( + dimensionPath( + "bottomDiameterLine1", + { x: x1, y: y1 }, + { x: x1, y: y2 }, + { x: svgViewBoxSize / 2 - gapSize, y: y2 } + ) + ); + d.push( + dimensionPath( + "bottomDiameterLine2", + { x: svgViewBoxSize / 2 + gapSize, y: y2 }, + { x: x2, y: y2 }, + { x: x2, y: y1 } + ) + ); + + return d; + }; + + //height of the sidewall, on the left side - always used + const sidwallDimension = () => { + let d: JSX.Element[] = []; + let centerX = svgViewBoxSize / 2; + let x1 = centerX - (bin.Diameter / 2) * scale - svgViewBoxSize * (1 - scaleMultiplier) * 0.1; + + let x2 = x1 - svgViewBoxSize * (1 - scaleMultiplier) * 0.15; + let midpoint = (roofHeight + bin.Sidewall / 2) * scale; + + let y1 = roofHeight * scale; + let y2 = (roofHeight + bin.Sidewall) * scale; + + d.push( + dimensionText("sidewallText", x2, midpoint, distanceConversion(bin.Sidewall).toFixed(1)) + ); + d.push( + dimensionPath( + "sidwallTopLine", + { x: x1, y: y1 }, + { x: x2, y: y1 }, + { x: x2, y: midpoint - gapSize } + ) + ); + d.push( + dimensionPath( + "sidewallbottomLine", + { x: x2, y: midpoint + gapSize }, + { x: x2, y: y2 }, + { x: x1, y: y2 } + ) + ); + return d; + }; + + //height of the bottom of the sidewall to the peak of the bin, on the left side - only used for hopper bins + const peakDimension = () => { + let d: JSX.Element[] = []; + let centerX = svgViewBoxSize / 2; + let xWall = centerX - (bin.Diameter / 2) * scale - svgViewBoxSize * (1 - scaleMultiplier) * 0.1; + let xPeak = svgViewBoxSize / 2 - svgViewBoxSize * (1 - scaleMultiplier) * 0.1; + + let xText = xWall - svgViewBoxSize * (1 - scaleMultiplier) * 0.5; + let midpoint = ((roofHeight + bin.Sidewall) / 2) * scale; + + let yTop = 0; + let yBottom = (roofHeight + bin.Sidewall) * scale; + + d.push( + dimensionText( + "peakText", + xText, + midpoint, + distanceConversion(bin.Sidewall + roofHeight).toFixed(1) + ) + ); + d.push( + dimensionPath( + "peakTopLine", + { x: xPeak, y: yTop }, + { x: xText, y: yTop }, + { x: xText, y: midpoint - gapSize } + ) + ); + d.push( + dimensionPath( + "peakbottomLine", + { x: xText, y: midpoint + gapSize }, + { x: xText, y: yBottom }, + { x: xWall, y: yBottom } + ) + ); + + return d; + }; + + //total height of the bin, on the right side - always used + const totalDimension = () => { + let d: JSX.Element[] = []; + let centerX = svgViewBoxSize / 2; + //let paddingVal = (padding* svgViewBoxSize) + let binRightX = centerX + (bin.Diameter / 2) * scale; + + let xBinCenter = centerX + svgViewBoxSize * (1 - scaleMultiplier) * 0.1; + let xBinWall = binRightX + svgViewBoxSize * (1 - scaleMultiplier) * 0.1; + let x2 = binRightX + svgViewBoxSize * (1 - scaleMultiplier) * 0.2; + let midpoint = ((roofHeight + bin.Sidewall + hopperHeight) / 2) * scale; + + let y1 = 0; + let y2 = (roofHeight + bin.Sidewall + hopperHeight) * scale; + + d.push( + dimensionText( + "totalText", + x2, + midpoint, + distanceConversion(roofHeight + bin.Sidewall + hopperHeight).toFixed(1) + ) + ); + d.push( + dimensionPath( + "peakTopLine", + { x: xBinCenter, y: y1 }, + { x: x2, y: y1 }, + { x: x2, y: midpoint - gapSize } + ) + ); + d.push( + dimensionPath( + "peakbottomLine", + { x: x2, y: midpoint + gapSize }, + { x: x2, y: y2 }, + { x: hopperHeight > 0 ? xBinCenter : xBinWall, y: y2 } + ) + ); + + return d; + }; + + const binDimensions = () => { + let dimensions: JSX.Element[] = []; + + dimensions.push(...bottomDiameter()); + // sidwall Height + dimensions.push(...sidwallDimension()); + //total bin height + dimensions.push(...totalDimension()); + if (hopperHeight > 0) { + //show the height of the peak + sidwall without the hopper + dimensions.push(...peakDimension()); + } + + return dimensions; + }; + + const grainEstimate = () => { + let x = svgViewBoxSize / 2 - (bin.Diameter / 2) * scale; + let y = + (roofHeight + bin.Sidewall + hopperHeight) * scale + + svgViewBoxSize * (1 - scaleMultiplier) * 0.65; + return ( + + {/* textAnchor="middle" + dominantBaseline={"middle"}> */} + {"Estimated Capacity: " + bin.Capacity + " Bushels"} + + ); + }; + + return ( + + {cableLines()} + {binOutline()} + {binDimensions()} + {grainEstimate()} + + ); +} \ No newline at end of file diff --git a/src/cableEstimator/pdfComponents/pdfSVG/pdfTopView.tsx b/src/cableEstimator/pdfComponents/pdfSVG/pdfTopView.tsx new file mode 100644 index 0000000..213b81b --- /dev/null +++ b/src/cableEstimator/pdfComponents/pdfSVG/pdfTopView.tsx @@ -0,0 +1,74 @@ +import { jsonBin } from "common/DataImports/BinCables/BinCableImporter"; +import { Circle, Svg } from "@react-pdf/renderer"; +import { colors } from "@mui/material"; + +interface Props { + bin: jsonBin; +} + +export default function PDFTopView(props: Props) { + const { bin } = props; + const totalRadius = 240; + + const outerEdge = () => { + return ( + + ); + }; + + const orbits = () => { + let o: JSX.Element[] = []; + bin.CableSums.forEach((sum, i) => { + //get the radius of the bin + let binRadius = bin.Diameter / 2; + //determine the percentage the orbit is from the center + let perc = sum.DistanceFromCenter / binRadius; + //use the percentage to find the radius for the svg + let svgRadius = perc * totalRadius; + o.push( + + ); + //then determine the points the cables will be + let angleDist = 360 / sum.NumberOfCables; + let currentAngle = 90; + for (let k = 0; k < sum.NumberOfCables; k++) { + let x = svgRadius * Math.sin(currentAngle * (Math.PI / 180)); + let y = svgRadius * Math.cos(currentAngle * (Math.PI / 180)); + o.push( + + ); + currentAngle += angleDist; + } + }); + return o; + }; + + return ( + + {outerEdge()} + {orbits()} + + ); +} \ No newline at end of file diff --git a/src/common/ResponsiveTable.tsx b/src/common/ResponsiveTable.tsx index 0e2d7fa..7280a4d 100644 --- a/src/common/ResponsiveTable.tsx +++ b/src/common/ResponsiveTable.tsx @@ -93,6 +93,7 @@ interface Props { selectedRows?: number[]; //which rows are selected will need to be controlled by the parent in order to keep track between page changes customElement?: JSX.Element //element that will be placed in the table head between the table actions and the column header rows mobileView?: boolean //can be used to force the table to use its mobile view for narrow containing elements ie, drawer + hidePagination?: boolean //when passed in will hide the pagination at the bottom of the table } export default function ResponsiveTable(props: Props) { @@ -120,7 +121,8 @@ export default function ResponsiveTable(props: Props) { rowSelect, selectedRows, customElement, - mobileView + mobileView, + hidePagination } = props const classes = useStyles(); const columns = typeof(props.columns) === "function" ? props.columns() : props.columns @@ -463,7 +465,7 @@ export default function ResponsiveTable(props: Props) { {event.stopPropagation()}} - onChange={(e, checked) => { + onChange={(_, checked) => { rowSelect(row, index, checked) }}/> @@ -529,15 +531,17 @@ export default function ResponsiveTable(props: Props) { } - handlePageChange(page)} - onRowsPerPageChange={handleRowsPerPageChange} - /> + {!hidePagination && + handlePageChange(page)} + onRowsPerPageChange={handleRowsPerPageChange} + /> + } + + +
+ + {props.content} + +