Compare commits

...

20 Commits

Author SHA1 Message Date
1f9a3a8e03 tools: bounds importer 2025-04-04 14:53:32 +09:00
6015218d59 docker-compose: add PostGIS db 2025-03-31 10:53:47 +09:00
442255ebaf Form from template 2025-03-26 10:31:00 +09:00
3bcea3f1ac ObjectTree: add padding 2025-03-07 16:53:39 +09:00
ada3b63b8d pass aspect ratio to fixedAspectRatio; remove printAreaDraw after printArea is defined 2025-03-07 16:50:54 +09:00
0ca6c136e3 ObjectTree: Memoize ObjectList 2025-03-06 15:37:49 +09:00
ec622da773 Remove duplication 2025-02-26 15:07:19 +09:00
300921751a lines/figures: hitTolerance for hover/click 2025-02-26 14:58:51 +09:00
4cc3a919ed MapPrint: fixedAspectRatioBox direction fix 2025-02-25 16:09:05 +09:00
349d7449f0 Add missing TCB types 2025-02-25 16:01:31 +09:00
8438e05301 MapComponent: Print button 2025-02-24 13:23:33 +09:00
85bafea7c8 ESLint 2025-01-31 15:56:10 +09:00
c08f839b70 Report test; Map printing test 2025-01-31 15:53:58 +09:00
0788a401ca Update 2025-01-30 12:36:39 +09:00
e6b3dc05d3 Region/district/city bounds routes 2025-01-14 14:32:41 +09:00
2bf657e8ed Update 2025-01-10 11:38:00 +09:00
59fded5cab Temporary disable nodes (prisma) 2024-12-20 10:15:56 +09:00
242ed1aee2 Rename client_app -> web_client 2024-12-20 10:01:46 +09:00
75d6420d6b Remove unused setter 2024-12-20 09:45:38 +09:00
7e8d1f50c8 Add CLIENT_PORT env 2024-12-20 09:39:12 +09:00
63 changed files with 14521 additions and 1838 deletions

View File

@ -6,5 +6,6 @@ POSTGRES_DB=ems
POSTGRES_USER=ems
POSTGRES_PASSWORD=
POSTGRES_PORT=5432
CLIENT_PORT=5173
EMS_PORT=5000
MONITOR_PORT=1234

1
client/.gitignore vendored
View File

@ -11,6 +11,7 @@ node_modules
dist
dist-ssr
*.local
stats.html
# Editor directories and files
.vscode/*

486
client/package-lock.json generated
View File

@ -9,8 +9,10 @@
"version": "0.0.0",
"dependencies": {
"-": "^0.0.1",
"@dnd-kit/core": "^6.3.1",
"@fontsource/inter": "^5.0.19",
"@fontsource/open-sans": "^5.0.28",
"@hello-pangea/dnd": "^17.0.0",
"@js-preview/docx": "^1.6.2",
"@js-preview/excel": "^1.7.8",
"@js-preview/pdf": "^2.0.2",
@ -29,6 +31,7 @@
"@mantine/tiptap": "^7.13.0",
"@tabler/icons-react": "^3.17.0",
"@tanstack/react-table": "^8.20.5",
"@techstark/opencv-js": "^4.10.0-release.1",
"@tiptap/extension-link": "^2.7.3",
"@tiptap/react": "^2.7.3",
"@tiptap/starter-kit": "^2.7.3",
@ -38,8 +41,12 @@
"axios": "^1.7.2",
"buffer": "^6.0.3",
"dayjs": "^1.11.13",
"docx-templates": "^4.13.0",
"easy-template-x": "^5.1.0",
"embla-carousel-react": "^8.3.0",
"file-type": "^19.0.0",
"html2canvas": "^1.4.1",
"jspdf": "^2.5.2",
"ol": "^10.0.0",
"ol-ext": "^4.0.23",
"proj4": "^2.12.0",
@ -65,6 +72,7 @@
"postcss": "^8.4.47",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",
"rollup-plugin-visualizer": "^5.12.0",
"sass-embedded": "^1.79.5",
"serve": "^14.2.3",
"tailwindcss": "^3.4.4",
@ -1863,9 +1871,9 @@
"dev": true
},
"node_modules/@babel/runtime": {
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz",
"integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==",
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@ -1937,6 +1945,42 @@
"integrity": "sha512-+imAQkHf7U/Rwvu0wk1XWgsP3WnpCWmK7B48f0XqSNzgk64+grljTKC7pnO/xBiEMUziF7vKRfbBnOQhg126qQ==",
"dev": true
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@ -2441,6 +2485,24 @@
"resolved": "https://registry.npmjs.org/@fontsource/open-sans/-/open-sans-5.0.28.tgz",
"integrity": "sha512-hBvJHY76pJT/JynGUB5EXWhnzjYfLdcMn655J5p1v9lTT9HdQSy+keq2KPVXO2Htlg998BBa3p6u/jlrZ6w0kg=="
},
"node_modules/@hello-pangea/dnd": {
"version": "17.0.0",
"resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-17.0.0.tgz",
"integrity": "sha512-LDDPOix/5N0j5QZxubiW9T0M0+1PR0rTDWeZF5pu1Tz91UQnuVK4qQ/EjY83Qm2QeX0eM8qDXANfDh3VVqtR4Q==",
"dependencies": {
"@babel/runtime": "^7.25.6",
"css-box-model": "^1.2.1",
"memoize-one": "^6.0.0",
"raf-schd": "^4.0.3",
"react-redux": "^9.1.2",
"redux": "^5.0.1",
"use-memo-one": "^1.1.3"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@ -3467,6 +3529,11 @@
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@techstark/opencv-js": {
"version": "4.10.0-release.1",
"resolved": "https://registry.npmjs.org/@techstark/opencv-js/-/opencv-js-4.10.0-release.1.tgz",
"integrity": "sha512-S4XELidRiQeA0q1s9VQLo540wCxUo24r1O4C+LqZ6llX+sPCXvZCPv3Ice8dEIr0uavyZ8YZeKXSBdDgMXSXjw=="
},
"node_modules/@tiptap/core": {
"version": "2.7.3",
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.7.3.tgz",
@ -3939,8 +4006,7 @@
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
"optional": true,
"peer": true
"optional": true
},
"node_modules/@types/react": {
"version": "18.3.3",
@ -4193,6 +4259,15 @@
"vite": "^4 || ^5"
}
},
"node_modules/@xmldom/xmldom": {
"version": "0.8.10",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
"integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/@zeit/schemas": {
"version": "2.36.0",
"resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz",
@ -4453,7 +4528,6 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"peer": true,
"bin": {
"atob": "bin/atob.js"
},
@ -4580,8 +4654,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"optional": true,
"peer": true,
"engines": {
"node": ">= 0.6.0"
}
@ -4859,7 +4931,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
"integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
"peer": true,
"bin": {
"btoa": "bin/btoa.js"
},
@ -5008,7 +5079,6 @@
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
"integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==",
"optional": true,
"peer": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/raf": "^3.4.0",
@ -5027,8 +5097,7 @@
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"optional": true,
"peer": true
"optional": true
},
"node_modules/chalk": {
"version": "4.1.2",
@ -5141,6 +5210,57 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/cliui/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
"node_modules/cliui/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@ -5316,7 +5436,6 @@
"integrity": "sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==",
"hasInstallScript": true,
"optional": true,
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
@ -5338,8 +5457,7 @@
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"dev": true
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"node_modules/create-ecdh": {
"version": "4.0.4",
@ -5440,12 +5558,18 @@
"node": ">=8"
}
},
"node_modules/css-box-model": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
"integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
"dependencies": {
"tiny-invariant": "^1.0.6"
}
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"optional": true,
"peer": true,
"dependencies": {
"utrie": "^1.0.2"
}
@ -5696,6 +5820,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/define-lazy-prop": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
"integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/define-properties": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
@ -5789,6 +5922,18 @@
"node": ">=6.0.0"
}
},
"node_modules/docx-templates": {
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/docx-templates/-/docx-templates-4.13.0.tgz",
"integrity": "sha512-tTmR3WhROYctuyVReQ+PfCU3zprmC45/VuSVzn8EjovzpRkXYUdXiDatB9M8pasj0V+wuuOyY8bcSHvlQ2GNag==",
"dependencies": {
"jszip": "^3.10.1",
"sax": "1.3.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
@ -5814,8 +5959,7 @@
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.6.tgz",
"integrity": "sha512-zUTaUBO8pY4+iJMPE1B9XlO2tXVYIcEA4SNGtvDELzTSCQO7RzH+j7S180BmhmJId78lqGU2z19vgVx2Sxs/PQ==",
"optional": true,
"peer": true
"optional": true
},
"node_modules/earcut": {
"version": "3.0.0",
@ -5828,6 +5972,18 @@
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true
},
"node_modules/easy-template-x": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/easy-template-x/-/easy-template-x-5.1.0.tgz",
"integrity": "sha512-vypMbIMLWLXoooA9rsL3SVN2oQtZwmmx1m4H8gi6JfbEXQQ5VLHGOUHYi9APbvN9R8Gx93r1fphdSFRHxozeYw==",
"license": "MIT",
"dependencies": {
"@xmldom/xmldom": "0.8.10",
"json5": "2.2.3",
"jszip": "3.10.1",
"lodash.get": "4.4.2"
}
},
"node_modules/ejs": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
@ -6414,10 +6570,9 @@
}
},
"node_modules/fflate": {
"version": "0.4.8",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==",
"peer": true
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="
},
"node_modules/file-entry-cache": {
"version": "6.0.1",
@ -6688,6 +6843,15 @@
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
@ -7007,8 +7171,7 @@
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"optional": true,
"peer": true,
"license": "MIT",
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
@ -7066,6 +7229,11 @@
"node": ">= 4"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
},
"node_modules/immutable": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
@ -7704,7 +7872,6 @@
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"bin": {
"json5": "lib/cli.js"
},
@ -7734,23 +7901,66 @@
}
},
"node_modules/jspdf": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.1.tgz",
"integrity": "sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA==",
"peer": true,
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz",
"integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.14.0",
"@babel/runtime": "^7.23.2",
"atob": "^2.1.2",
"btoa": "^1.2.1",
"fflate": "^0.4.8"
"fflate": "^0.8.1"
},
"optionalDependencies": {
"canvg": "^3.0.6",
"core-js": "^3.6.0",
"dompurify": "^2.2.0",
"dompurify": "^2.5.4",
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/jszip/node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
},
"node_modules/jszip/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/jszip/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/jszip/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -7795,6 +8005,14 @@
"node": ">= 0.8.0"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lilconfig": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
@ -7849,6 +8067,13 @@
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"dev": true
},
"node_modules/lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
"deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.",
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -7922,6 +8147,11 @@
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="
},
"node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
},
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@ -8325,6 +8555,23 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/open": {
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
"integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
"dev": true,
"dependencies": {
"define-lazy-prop": "^2.0.0",
"is-docker": "^2.1.1",
"is-wsl": "^2.2.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -8386,8 +8633,7 @@
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"dev": true
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"node_modules/parent-module": {
"version": "1.0.1",
@ -8542,8 +8788,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"optional": true,
"peer": true
"optional": true
},
"node_modules/picocolors": {
"version": "1.1.0",
@ -8832,8 +9077,7 @@
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"node_modules/proj4": {
"version": "2.12.0",
@ -9147,11 +9391,15 @@
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"optional": true,
"peer": true,
"dependencies": {
"performance-now": "^2.1.0"
}
},
"node_modules/raf-schd": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
"integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ=="
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -9273,6 +9521,36 @@
"react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
},
"peerDependencies": {
"@types/react": "^18.2.25 || ^19",
"react": "^18.0 || ^19",
"redux": "^5.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"redux": {
"optional": true
}
}
},
"node_modules/react-redux/node_modules/use-sync-external-store": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
"integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-remove-scroll": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz",
@ -9499,6 +9777,11 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
},
"node_modules/regenerate": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@ -9609,6 +9892,15 @@
"jsesc": "bin/jsesc"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@ -9667,7 +9959,6 @@
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
"optional": true,
"peer": true,
"engines": {
"node": ">= 0.8.15"
}
@ -9733,6 +10024,32 @@
"fsevents": "~2.3.2"
}
},
"node_modules/rollup-plugin-visualizer": {
"version": "5.12.0",
"resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.12.0.tgz",
"integrity": "sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==",
"dev": true,
"dependencies": {
"open": "^8.4.0",
"picomatch": "^2.3.1",
"source-map": "^0.7.4",
"yargs": "^17.5.1"
},
"bin": {
"rollup-plugin-visualizer": "dist/bin/cli.js"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"rollup": "2.x || 3.x || 4.x"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/rope-sequence": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
@ -10202,6 +10519,11 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/sax": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz",
"integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA=="
},
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
@ -10384,8 +10706,7 @@
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"dev": true
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
},
"node_modules/sha.js": {
"version": "2.4.11",
@ -10466,6 +10787,15 @@
"integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==",
"dev": true
},
"node_modules/source-map": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
"dev": true,
"engines": {
"node": ">= 8"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@ -10505,7 +10835,6 @@
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
"optional": true,
"peer": true,
"engines": {
"node": ">=0.1.14"
}
@ -10854,7 +11183,6 @@
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
"optional": true,
"peer": true,
"engines": {
"node": ">=12.0.0"
}
@ -10980,8 +11308,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"optional": true,
"peer": true,
"dependencies": {
"utrie": "^1.0.2"
}
@ -11443,6 +11769,14 @@
}
}
},
"node_modules/use-memo-one": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz",
"integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/use-sidecar": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
@ -11494,8 +11828,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"optional": true,
"peer": true,
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
@ -12213,6 +12545,15 @@
"node": ">=0.4"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"engines": {
"node": ">=10"
}
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@ -12231,6 +12572,53 @@
"node": ">= 14"
}
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dev": true,
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/yargs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
"node_modules/yargs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@ -12,8 +12,10 @@
},
"dependencies": {
"-": "^0.0.1",
"@dnd-kit/core": "^6.3.1",
"@fontsource/inter": "^5.0.19",
"@fontsource/open-sans": "^5.0.28",
"@hello-pangea/dnd": "^17.0.0",
"@js-preview/docx": "^1.6.2",
"@js-preview/excel": "^1.7.8",
"@js-preview/pdf": "^2.0.2",
@ -32,6 +34,7 @@
"@mantine/tiptap": "^7.13.0",
"@tabler/icons-react": "^3.17.0",
"@tanstack/react-table": "^8.20.5",
"@techstark/opencv-js": "^4.10.0-release.1",
"@tiptap/extension-link": "^2.7.3",
"@tiptap/react": "^2.7.3",
"@tiptap/starter-kit": "^2.7.3",
@ -41,8 +44,12 @@
"axios": "^1.7.2",
"buffer": "^6.0.3",
"dayjs": "^1.11.13",
"docx-templates": "^4.13.0",
"easy-template-x": "^5.1.0",
"embla-carousel-react": "^8.3.0",
"file-type": "^19.0.0",
"html2canvas": "^1.4.1",
"jspdf": "^2.5.2",
"ol": "^10.0.0",
"ol-ext": "^4.0.23",
"proj4": "^2.12.0",
@ -68,6 +75,7 @@
"postcss": "^8.4.47",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",
"rollup-plugin-visualizer": "^5.12.0",
"sass-embedded": "^1.79.5",
"serve": "^14.2.3",
"tailwindcss": "^3.4.4",

BIN
client/public/template.docx Normal file

Binary file not shown.

Binary file not shown.

BIN
client/public/test.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

@ -5,47 +5,48 @@
right: 0;
height: 100%;
width: 5px;
background: #27bbff;
background: var(--mantine-color-anchor);
cursor: col-resize;
user-select: none;
touch-action: none;
border-radius: 6px;
transition: opacity .2s ease;
}
.resize_handler:hover {
opacity: 1;
}
.tr {
display: flex;
//width: 100%;
//max-width: 100%;
width: fit-content;
}
// .tr {
// display: flex;
// //width: 100%;
// //max-width: 100%;
// width: fit-content;
// }
.th {
position: relative;
}
.th,
.td {
display: flex;
width: auto;
}
// .th,
// .td {
// display: flex;
// width: auto;
// }
.thead {
display: flex;
width: 100%;
}
// .thead {
// display: flex;
// width: 100%;
// }
.table {
display: flex;
flex-direction: column;
width: 100%;
}
// .table {
// display: flex;
// flex-direction: column;
// width: 100%;
// }
.tbody {
display: flex;
flex-direction: column;
width: 100%;
}
// .tbody {
// display: flex;
// flex-direction: column;
// width: 100%;
// }

View File

@ -1,62 +1,96 @@
import { Input, Table } from '@mantine/core';
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
import { useMemo, useState } from 'react';
import { Badge, Button, Flex, Input, Modal, ScrollAreaAutosize, Select, Stack, Table, TextInput } from '@mantine/core';
import { Cell, ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
import { useEffect, useMemo, useState } from 'react';
import styles from './CustomTable.module.scss'
import { useRoles } from '../hooks/swrHooks';
import { IRole } from '../interfaces/role';
import { IconPlus } from '@tabler/icons-react';
import { CreateField } from '../interfaces/create';
import { AxiosResponse } from 'axios';
import FormFields from './FormFields';
import { useDisclosure } from '@mantine/hooks';
// Sample data
type DataType = {
id: number,
name: string,
age: number
type CustomTableProps<T> = {
data: T[];
columns: ColumnDef<T>[];
createFields?: CreateField[];
submitHandler?: (data: T) => Promise<AxiosResponse>
}
// Define columns
const columns: ColumnDef<DataType>[] = [
{
accessorKey: 'name',
header: 'Name',
cell: (info) => info.getValue(),
maxSize: Number.MAX_SAFE_INTEGER,
},
{
accessorKey: 'age',
header: 'Age',
cell: (info) => info.getValue(),
},
];
const CustomTable = () => {
const [data, setData] = useState<DataType[]>([
{ id: 1, name: 'John Doe', age: 25 },
{ id: 2, name: 'Jane Smith', age: 30 },
{ id: 3, name: 'Sam Green', age: 22 },
]);
const CustomTable = <T extends object>({
data: initialData,
columns,
createFields,
submitHandler
}: CustomTableProps<T>) => {
const [data, setData] = useState<T[]>(initialData);
const [searchText, setSearchText] = useState('');
const [editingCell, setEditingCell] = useState<{ rowIndex: string | number | null, columnId: string | number | null }>({ rowIndex: null, columnId: null });
const tableColumns = useMemo<ColumnDef<typeof data[0]>[]>(() => columns, []);
const tableColumns = useMemo(() => columns, [columns]);
// Function to handle cell edit
const handleEditCell = (
rowIndex: number,
columnId: keyof T,
value: T[keyof T]
) => {
const updatedData = [...data];
updatedData[rowIndex][columnId] = value;
setData(updatedData);
//setEditingCell({ rowIndex: null, columnId: null });
};
const filteredData = useMemo(() => {
if (!searchText) return data;
return data.filter((row) =>
Object.values(row).some((value) =>
value?.toString().toLowerCase().includes(searchText.toLowerCase())
)
);
}, [data, searchText])
const table = useReactTable({
data,
data: filteredData,
columns: tableColumns,
getCoreRowModel: getCoreRowModel(),
columnResizeMode: "onChange",
});
// Function to handle cell edit
const handleEditCell = (
rowIndex: number,
columnId: keyof DataType,
value: DataType[keyof DataType]
) => {
const updatedData = [...data];
(updatedData[rowIndex][columnId] as DataType[keyof DataType]) = value;
setData(updatedData);
//setEditingCell({ rowIndex: null, columnId: null });
};
const [opened, { open, close }] = useDisclosure(false);
return (
<Table striped withColumnBorders highlightOnHover className={styles.table}>
<Stack h='100%'>
{createFields && submitHandler &&
<Modal opened={opened} onClose={close} title="Добавление объекта" centered>
<FormFields
fields={createFields}
submitHandler={submitHandler}
/>
</Modal>
}
<Flex w='100%' gap='sm'>
<TextInput
placeholder="Поиск"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
w='100%'
/>
{createFields && submitHandler &&
<Button
leftSection={<IconPlus />}
onClick={open}
style={{ flexShrink: 0 }}
>
Добавить
</Button>
}
</Flex>
<ScrollAreaAutosize offsetScrollbars style={{ borderRadius: '4px' }}>
<Table stickyHeader striped withColumnBorders highlightOnHover className={styles.table}>
<Table.Thead className={styles.thead}>
{table.getHeaderGroups().map(headerGroup => (
<Table.Tr key={headerGroup.id} className={styles.tr}>
@ -90,13 +124,13 @@ const CustomTable = () => {
{isEditing ? (
<Input
type='text'
value={data[rowIndex][cell.column.id as keyof DataType]}
onChange={(e) => handleEditCell(rowIndex, (cell.column.id as keyof DataType), e.target.value)}
value={(data[rowIndex][cell.column.id as keyof T] as string)}
onChange={(e) => handleEditCell(rowIndex, (cell.column.id as keyof T), e.target.value as T[keyof T])}
onBlur={() => setEditingCell({ rowIndex: null, columnId: null })}
autoFocus
/>
) : (
flexRender(cell.column.columnDef.cell, cell.getContext())
<CellDisplay cell={cell} />
)}
</Table.Td>
);
@ -105,7 +139,67 @@ const CustomTable = () => {
))}
</Table.Tbody>
</Table>
</ScrollAreaAutosize>
</Stack>
);
};
type CellDisplayProps<T> = {
cell: Cell<T, unknown>;
}
const CellDisplay = <T extends object>({
cell
}: CellDisplayProps<T>) => {
const { roles } = useRoles()
const [roleOptions, setRoleOptions] = useState<{ label: string, value: string }[]>()
useEffect(() => {
if (Array.isArray(roles)) {
setRoleOptions(roles.map((role: IRole) => ({ label: role.name, value: role.id.toString() })))
}
}, [roles])
switch (cell.column.id) {
case 'activity':
return (
cell.getValue() ? (
<Badge fullWidth variant="light">
Активен
</Badge>
) : (
<Badge color="gray" fullWidth variant="light">
Отключен
</Badge>
)
)
case 'is_active':
return (
cell.getValue() ? (
<Badge fullWidth variant="light">
Активен
</Badge>
) : (
<Badge color="gray" fullWidth variant="light">
Отключен
</Badge>
)
)
case 'role_id':
return (
<Select
data={roleOptions}
value={Number(cell.getValue()).toString()}
variant="unstyled"
allowDeselect={false}
/>
)
default:
return (
flexRender(cell.column.columnDef.cell, cell.getContext())
)
}
}
export default CustomTable;

View File

@ -4,7 +4,7 @@ import { useStorages } from '../hooks/swrHooks'
import { Loader, Table } from '@mantine/core'
export default function ServerStorage() {
const [selectedOption, setSelectedOption] = useState<IRegion | null>(null)
const [selectedOption] = useState<IRegion | null>(null)
const { storages, isLoading: serversLoading } = useStorages(selectedOption?.id, 0, 10)

View File

@ -1,15 +1,18 @@
import { useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import useSWR from 'swr'
import { fetcher } from '../../http/axiosInstance'
import { BASE_URL } from '../../constants'
import { Accordion, NavLink, Text } from '@mantine/core';
import { setCurrentObjectId, useObjectsStore } from '../../store/objects';
import { NavLink, Stack, Text } from '@mantine/core';
import { IconChevronDown } from '@tabler/icons-react';
import { setSelectedObjectType } from '../../store/map';
import { setCurrentObjectId, useObjectsStore } from '../../store/objects';
const ObjectTree = () => {
const { selectedDistrict, selectedYear } = useObjectsStore()
const ObjectTree = ({
map_id
}: {
map_id: string,
}) => {
const { selectedYear, selectedDistrict } = useObjectsStore().id[map_id]
const [existingCount, setExistingCount] = useState(0)
const [planningCount, setPlanningCount] = useState(0)
@ -49,10 +52,10 @@ const ObjectTree = () => {
if (selectedDistrict) {
return (
<Accordion multiple chevronPosition='left'>
<TypeTree label='Существующие' value={'existing'} count={existingCount} objectList={existingObjectsList} planning={0} />
<TypeTree label='Планируемые' value={'planning'} count={planningCount} objectList={planningObjectsList} planning={1} />
</Accordion>
<Stack gap={0}>
<TypeTree map_id={map_id} label='Существующие' value={'existing'} count={existingCount} objectList={existingObjectsList} planning={0} />
<TypeTree map_id={map_id} label='Планируемые' value={'planning'} count={planningCount} objectList={planningObjectsList} planning={1} />
</Stack>
)
} else {
return (
@ -69,19 +72,21 @@ interface TypeTreeProps {
count: number;
objectList: unknown;
planning: number;
map_id: string;
}
const TypeTree = ({
label,
objectList,
count,
planning
planning,
map_id
}: TypeTreeProps) => {
return (
<NavLink p={0} label={`${label} ${count ? `(${count})` : ''}`}>
<NavLink px='xs' py={0} label={`${label} ${count ? `(${count})` : ''}`}>
{Array.isArray(objectList) && objectList.map(list => (
<ObjectList key={list.id} label={list.name} id={list.id} planning={planning} count={list.count} />
<ObjectList map_id={map_id} key={list.id} label={list.name} id={list.id} planning={planning} count={list.count} />
))}
</NavLink>
)
@ -92,15 +97,17 @@ interface IObjectList {
id: number;
planning: number;
count: number;
map_id: string;
}
const ObjectList = ({
label,
id,
planning,
count
count,
map_id
}: IObjectList) => {
const { selectedDistrict, selectedYear } = useObjectsStore()
const { selectedDistrict, selectedYear } = useObjectsStore().id[map_id]
const { data } = useSWR(
selectedDistrict && selectedYear ? `/general/objects/list?type=${id}&city_id=${selectedDistrict}&year=${selectedYear}&planning=${planning}` : null,
@ -111,15 +118,25 @@ const ObjectList = ({
}
)
const navLinks = useMemo(() => (
Array.isArray(data) ? data.map((type) => (
<NavLink key={type.object_id} label={type.caption ? type.caption : 'Без названия'} px='xs' py={0} onClick={() => setCurrentObjectId(map_id, type.object_id)} />
)) : null
), [data, map_id]);
return (
<NavLink onClick={() => {
setSelectedObjectType(id)
}} rightSection={<IconChevronDown size={14} />} p={0} label={`${label} ${count ? `(${count})` : ''}`}>
{Array.isArray(data) && data.map((type) => (
<NavLink key={type.object_id} label={type.caption ? type.caption : 'Без названия'} p={0} onClick={() => setCurrentObjectId(type.object_id)} />
))}
<NavLink onClick={() => setSelectedObjectType(map_id, id)} rightSection={<IconChevronDown size={14} />} px='xs' py={0} label={`${label} ${count ? `(${count})` : ''}`}>
{navLinks}
</NavLink>
)
);
// return (
// <NavLink onClick={() => { setSelectedObjectType(map_id, id) }} rightSection={<IconChevronDown size={14} />} p={0} label={`${label} ${count ? `(${count})` : ''}`}>
// {Array.isArray(data) && data.map((type) => (
// <NavLink key={type.object_id} label={type.caption ? type.caption : 'Без названия'} p={0} onClick={() => setCurrentObjectId(map_id, type.object_id)} />
// ))}
// </NavLink>
// )
}
export default ObjectTree

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,10 @@
import { Checkbox, Flex, NavLink, Slider, Stack } from '@mantine/core'
import BaseLayer from 'ol/layer/Base'
import Map from 'ol/Map'
import React, { useEffect, useState } from 'react'
import { useEffect, useState } from 'react'
interface MapLayersProps {
map: React.MutableRefObject<Map | null>
map: Map | null
}
const MapLayers = ({
@ -12,7 +12,7 @@ const MapLayers = ({
}: MapLayersProps) => {
return (
<Stack gap='0'>
{map.current?.getLayers().getArray() && map.current?.getLayers().getArray().map((layer, index) => (
{map?.getLayers().getArray() && map?.getLayers().getArray().map((layer, index) => (
<LayerSetting key={index} index={index} layer={layer} />
))}
</Stack>

View File

@ -0,0 +1,92 @@
import { Accordion, ColorSwatch, Flex, MantineStyleProp, ScrollAreaAutosize, Stack, Text, useMantineColorScheme } from '@mantine/core'
import useSWR from 'swr';
import { fetcher } from '../../../http/axiosInstance';
import { BASE_URL } from '../../../constants';
const MapLegend = ({
selectedDistrict,
selectedYear,
style
}: {
selectedDistrict: number | null,
selectedYear: number | null,
style: MantineStyleProp
}) => {
const { colorScheme } = useMantineColorScheme();
const { data: existingObjectsList } = useSWR(
selectedYear && selectedDistrict ? `/general/objects/list?year=${selectedYear}&city_id=${selectedDistrict}&planning=0` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
const { data: planningObjectsList } = useSWR(
selectedYear && selectedDistrict ? `/general/objects/list?year=${selectedYear}&city_id=${selectedDistrict}&planning=1` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
return (
<ScrollAreaAutosize offsetScrollbars maw='300px' w='100%' fz='xs' mt='auto' style={{...style, zIndex: 1, backdropFilter: 'blur(8px)', backgroundColor: colorScheme === 'light' ? '#FFFFFFAA' : '#000000AA', borderRadius: '4px' }}>
<Stack gap='sm' p='sm'>
<Text fz='xs'>
Легенда
</Text>
<Accordion defaultValue={['existing', 'planning']} multiple>
<Accordion.Item value='existing' key='existing'>
<Accordion.Control>Существующие</Accordion.Control>
<Accordion.Panel>
{existingObjectsList && <LegendGroup objectsList={existingObjectsList} border='solid' />}
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item value='planning' key='planning'>
<Accordion.Control>Планируемые</Accordion.Control>
<Accordion.Panel>
{planningObjectsList && <LegendGroup objectsList={planningObjectsList} border='dotted' />}
</Accordion.Panel>
</Accordion.Item>
</Accordion>
</Stack>
</ScrollAreaAutosize>
)
}
const LegendGroup = ({
objectsList,
border
}: {
objectsList: { id: number, name: string, count: number, r: number | null, g: number | null, b: number | null }[],
border: 'solid' | 'dotted'
}) => {
const borderStyle = () => {
switch (border) {
case 'solid':
return '2px solid black'
case 'dotted':
return '2px dotted black'
default:
return 'none'
}
}
return (
<Stack gap={4}>
{objectsList.map(object => (
<Flex gap='xs' align='center' key={object.id}>
<ColorSwatch style={{ border: borderStyle() }} radius={0} size={16} color={`rgb(${object.r},${object.g},${object.b})`} />
-
<Text fz='xs'>{object.name}</Text>
</Flex>
))}
</Stack>
)
}
export default MapLegend

View File

@ -0,0 +1,107 @@
import { Button, Flex, FloatingIndicator, Popover, SegmentedControl } from '@mantine/core'
import { Mode, setMode, useMapStore } from '../../store/map'
import { IconChevronDown, IconCropLandscape, IconCropPortrait, IconEdit, IconEye, IconPrinter } from '@tabler/icons-react'
import { useEffect, useState } from 'react'
import { PrintOrientation, setPrintOrientation, usePrintStore } from '../../store/print'
const MapMode = ({
map_id
}: { map_id: string }) => {
const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null);
const [controlsRefs, setControlsRefs] = useState<Record<string, HTMLButtonElement | null>>({});
const { mode } = useMapStore().id[map_id]
const setControlRef = (item: Mode) => (node: HTMLButtonElement) => {
controlsRefs[item] = node;
setControlsRefs(controlsRefs);
}
const { printOrientation } = usePrintStore()
useEffect(() => {
}, [printOrientation])
return (
<Flex ref={setRootRef} p={4} gap={4}>
<Button
variant={mode === 'view' ? 'filled' : 'subtle'}
key={'view'}
ref={setControlRef('view' as Mode)}
onClick={() => {
setMode(map_id, 'view' as Mode)
}}
leftSection={<IconEye size={16} />}
mod={{ active: mode === 'view' as Mode }}
>
Просмотр
</Button>
<Button
variant={mode === 'edit' ? 'filled' : 'subtle'}
key={'edit'}
ref={setControlRef('edit' as Mode)}
onClick={() => {
setMode(map_id, 'edit' as Mode)
}}
leftSection={<IconEdit size={16} />}
mod={{ active: mode === 'edit' as Mode }}
>
Редактирование
</Button>
<Popover width='auto' position='bottom-end' >
<Popover.Target>
<Button.Group>
<Button
variant={mode === 'print' ? 'filled' : 'subtle'}
key={'print'}
ref={setControlRef('print' as Mode)}
onClick={(e) => {
e.stopPropagation()
setMode(map_id, 'print' as Mode)
}}
leftSection={<IconPrinter size={16} />}
mod={{ active: mode === 'print' as Mode }}
>
Печать
</Button>
<Button variant={mode === 'print' ? 'filled' : 'subtle'} w='auto' p={8} title='Ориентация'>
<IconChevronDown size={16} />
</Button>
</Button.Group>
</Popover.Target>
<Popover.Dropdown p={0} style={{ display: 'flex' }}>
<SegmentedControl
color='blue'
value={printOrientation}
onChange={(value) => {
setPrintOrientation(value as PrintOrientation)
setMode(map_id, 'print' as Mode)
}}
data={[
{
value: 'horizontal',
label: (
<IconCropLandscape title='Горизонтальная' style={{ display: 'block' }} size={20} />
),
},
{
value: 'vertical',
label: (
<IconCropPortrait title='Вертикальная' style={{ display: 'block' }} size={20} />
),
},
]}
/>
</Popover.Dropdown>
</Popover>
<FloatingIndicator target={controlsRefs[mode]} parent={rootRef} />
</Flex >
)
}
export default MapMode

View File

@ -26,7 +26,7 @@ const customMapSource = new XYZ({
})
const regionsLayerSource = new VectorSource({
url: 'sakha_republic.geojson',
url: 'http://localhost:5000/gis/bounds/region',
format: new GeoJSON(),
})

View File

@ -4,39 +4,41 @@ import { useMapStore } from '../../../store/map';
interface IMapStatusbarProps {
mapControlsStyle: CSSProperties;
map_id: string;
}
const MapStatusbar = ({
mapControlsStyle,
map_id
}: IMapStatusbarProps) => {
const mapState = useMapStore()
const { currentCoordinate, currentX, currentY, currentZ, statusText } = useMapStore().id[map_id]
return (
<Flex gap='sm' p={'4px'} miw={'100%'} fz={'xs'} pos='absolute' bottom='0px' left='0px' style={{ ...mapControlsStyle, borderRadius: 0 }}>
<Flex gap='sm' p={'4px'} w={'100%'} fz={'xs'} style={{ ...mapControlsStyle, borderRadius: 0 }}>
<Text fz='xs' w={rem(130)}>
x: {mapState.currentCoordinate?.[0]}
x: {currentCoordinate?.[0]}
</Text>
<Text fz='xs' w={rem(130)}>
y: {mapState.currentCoordinate?.[1]}
y: {currentCoordinate?.[1]}
</Text>
<Divider orientation='vertical' />
<Text fz='xs'>
Z={mapState.currentZ}
Z={currentZ}
</Text>
<Text fz='xs'>
X={mapState.currentX}
X={currentX}
</Text>
<Text fz='xs'>
Y={mapState.currentY}
Y={currentY}
</Text>
<Text fz='xs' ml='auto'>
{mapState.statusText}
{statusText}
</Text>
</Flex>
)

View File

@ -1,4 +1,4 @@
import Feature, { FeatureLike } from "ol/Feature";
import { FeatureLike } from "ol/Feature";
import { Text } from "ol/style";
import Fill from "ol/style/Fill";
import { FlatStyleLike } from "ol/style/flat";
@ -90,15 +90,27 @@ export function overlayStyle(feature: FeatureLike) {
return styles
}
export function styleFunction(feature: Feature) {
return [
new Style({
const figureStyle = new Style({
fill: new Fill({
color: 'rgba(255,255,255,0.4)'
}),
stroke: new Stroke({
color: 'black',
width: 1.25
}),
text: new Text({
font: '12px Calibri,sans-serif',
fill: new Fill({ color: '#000' }),
stroke: new Stroke({
color: '#fff', width: 2
})
})
})
const lineStyle = new Style({
stroke: new Stroke({
color: '#3399CC',
width: 1.25
width: 1
}),
text: new Text({
font: '12px Calibri,sans-serif',
@ -106,85 +118,11 @@ export function styleFunction(feature: Feature) {
stroke: new Stroke({
color: '#fff', width: 2
}),
// get the text from the feature - `this` is ol.Feature
// and show only under certain resolution
text: feature.get('object_id')
placement: 'line',
overflow: true,
//declutterMode: 'obstacle'
})
})
];
}
export function firstStyleFunction(feature: Feature) {
return [
new Style({
fill: new Fill({
color: 'rgba(255,255,255,0.4)'
}),
stroke: new Stroke({
color: 'red',
width: 1.25
}),
text: new Text({
font: '12px Calibri,sans-serif',
fill: new Fill({ color: '#000' }),
stroke: new Stroke({
color: '#fff', width: 2
}),
// get the text from the feature - `this` is ol.Feature
// and show only under certain resolution
text: feature.get('object_id')
})
})
];
}
export function thirdStyleFunction(feature: Feature) {
return [
new Style({
fill: new Fill({
color: 'rgba(255,255,255,0.4)'
}),
stroke: new Stroke({
color: '#33ccb3',
width: 1.25
}),
text: new Text({
font: '12px Calibri,sans-serif',
fill: new Fill({ color: '#000' }),
stroke: new Stroke({
color: '#fff', width: 2
}),
// get the text from the feature - `this` is ol.Feature
// and show only under certain resolution
text: feature.get('object_id')
})
})
];
}
export function fourthStyleFunction(feature: Feature) {
return [
new Style({
fill: new Fill({
color: 'rgba(255,255,255,0.4)'
}),
stroke: new Stroke({
color: '#3399CC',
width: 1.25
}),
text: new Text({
font: '12px Calibri,sans-serif',
fill: new Fill({ color: '#000' }),
stroke: new Stroke({
color: '#fff', width: 2
}),
// get the text from the feature - `this` is ol.Feature
// and show only under certain resolution
text: `${feature.get('object_id')}\n ${feature.get('angle')}`
})
})
];
}
})
const drawingLayerStyle: FlatStyleLike = {
'fill-color': 'rgba(255, 255, 255, 0.2)',
@ -218,5 +156,7 @@ const regionsLayerStyle = new Style({
export {
drawingLayerStyle,
selectStyle,
regionsLayerStyle
regionsLayerStyle,
lineStyle,
figureStyle
}

View File

@ -1,79 +1,75 @@
import { ActionIcon, useMantineColorScheme } from '@mantine/core'
import { ActionIcon, Flex, useMantineColorScheme } from '@mantine/core'
import { IconArrowBackUp, IconArrowsMove, IconCircle, IconExclamationCircle, IconLine, IconPoint, IconPolygon, IconRuler, IconTransformPoint } from '@tabler/icons-react'
import { setCurrentTool, useMapStore } from '../../../store/map';
interface IToolbarProps {
onSave: () => void;
onRemove: () => void;
}
import { getDraw, setCurrentTool, useMapStore } from '../../../store/map';
import { saveFeatures } from '../mapUtils';
const MapToolbar = ({
onSave,
onRemove,
}: IToolbarProps) => {
const mapState = useMapStore()
map_id
}: { map_id: string }) => {
const { currentTool } = useMapStore().id[map_id]
const { colorScheme } = useMantineColorScheme();
return (
<ActionIcon.Group orientation='vertical' pos='absolute' top='8px' right='8px' style={{ zIndex: 1, backdropFilter: 'blur(8px)', backgroundColor: colorScheme === 'light' ? '#FFFFFFAA' : '#000000AA', borderRadius: '4px' }}>
<ActionIcon size='lg' variant='transparent' onClick={onSave}>
<Flex>
<ActionIcon.Group orientation='vertical' style={{ zIndex: 1, backdropFilter: 'blur(8px)', backgroundColor: colorScheme === 'light' ? '#FFFFFFAA' : '#000000AA', borderRadius: '4px' }}>
<ActionIcon size='lg' variant='transparent' onClick={() => saveFeatures(map_id)}>
<IconExclamationCircle />
</ActionIcon>
<ActionIcon size='lg' variant='transparent' onClick={onRemove}>
<ActionIcon size='lg' variant='transparent' onClick={() => getDraw(map_id)?.removeLastPoint()}>
<IconArrowBackUp />
</ActionIcon>
<ActionIcon
size='lg'
variant={mapState.currentTool === 'Edit' ? 'filled' : 'transparent'}
variant={currentTool === 'Edit' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool('Edit')
setCurrentTool(map_id, 'Edit')
}}>
<IconTransformPoint />
</ActionIcon>
<ActionIcon
size='lg'
variant={mapState.currentTool === 'Point' ? 'filled' : 'transparent'}
variant={currentTool === 'Point' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool('Point')
setCurrentTool(map_id, 'Point')
}}>
<IconPoint />
</ActionIcon>
<ActionIcon
size='lg'
variant={mapState.currentTool === 'LineString' ? 'filled' : 'transparent'}
variant={currentTool === 'LineString' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool('LineString')
setCurrentTool(map_id, 'LineString')
}}>
<IconLine />
</ActionIcon>
<ActionIcon
size='lg'
variant={mapState.currentTool === 'Polygon' ? 'filled' : 'transparent'}
variant={currentTool === 'Polygon' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool('Polygon')
setCurrentTool(map_id, 'Polygon')
}}>
<IconPolygon />
</ActionIcon>
<ActionIcon
size='lg'
variant={mapState.currentTool === 'Circle' ? 'filled' : 'transparent'}
variant={currentTool === 'Circle' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool('Circle')
setCurrentTool(map_id, 'Circle')
}}>
<IconCircle />
</ActionIcon>
<ActionIcon
size='lg'
variant={mapState.currentTool === 'Mover' ? 'filled' : 'transparent'}
variant={currentTool === 'Mover' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool('Mover')
setCurrentTool(map_id, 'Mover')
}}
>
<IconArrowsMove />
@ -81,13 +77,14 @@ const MapToolbar = ({
<ActionIcon
size='lg'
variant={mapState.currentTool === 'Measure' ? 'filled' : 'transparent'}
variant={currentTool === 'Measure' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool('Measure')
setCurrentTool(map_id, 'Measure')
}}>
<IconRuler />
</ActionIcon>
</ActionIcon.Group>
</Flex>
)
}

View File

@ -138,6 +138,7 @@ const formatArea = function (polygon: Geometry) {
};
export function measureStyleFunction(
map_id: string,
feature: FeatureLike,
drawType?: Type,
tip?: string,
@ -149,7 +150,7 @@ export function measureStyleFunction(
const type = geometry?.getType();
const segmentStyles = [segmentStyle];
const segments = getMeasureShowSegments()
const segments = getMeasureShowSegments(map_id)
if (!geometry) return

View File

@ -6,12 +6,14 @@ import TCBParameter from './TCBParameter'
import TableValue from './TableValue'
interface ObjectParameterProps {
showLabel?: boolean,
param: IObjectParam,
showLabel?: boolean;
param: IObjectParam;
map_id: string;
}
const ObjectParameter = ({
param
param,
map_id
}: ObjectParameterProps) => {
const { data: paramData } = useSWR(
`/general/params/all?param_id=${param.id_param}`,
@ -26,44 +28,48 @@ const ObjectParameter = ({
switch (type) {
case 'bit':
return (
<TableValue value={value} name={name} type='boolean' />
<TableValue map_id={map_id} value={value} name={name} type='boolean' />
)
case 'int':
return (
<TableValue map_id={map_id} value={value} name={name} type='number' />
)
case 'bigint':
return (
<TableValue value={value} name={name} type='number' />
<TableValue map_id={map_id} value={value} name={name} type='number' />
)
case 'tinyint':
return (
<TableValue value={value} name={name} type='number' />
<TableValue map_id={map_id} value={value} name={name} type='number' />
)
// TODO: Calculate from calc procedures
case 'calculate':
return (
<TableValue value={value} name={name} type='value' />
<TableValue map_id={map_id} value={value} name={name} type='value' />
)
case 'GTCB':
return (
<TCBParameter value={value as string} vtable={vtable} name={name} />
<TCBParameter map_id={map_id} value={value as string} vtable={vtable} name={name} />
)
case 'TCB':
return (
<TCBParameter value={value as string} vtable={vtable} name={name} />
<TCBParameter map_id={map_id} value={value as string} vtable={vtable} name={name} />
)
case type.match(/varchar\((\d+)\)/)?.input:
return (
<TableValue value={value} name={name} type='string' />
<TableValue map_id={map_id} value={value} name={name} type='string' />
)
case type.match(/numeric\((\d+),(\d+)\)/)?.input:
return (
<TableValue value={value} name={name} type='number' unit={unit} />
<TableValue map_id={map_id} value={value} name={name} type='number' unit={unit} />
)
case 'year':
return (
<TableValue value={value} name={name} type='number' />
<TableValue map_id={map_id} value={value} name={name} type='number' />
)
case 'uniqueidentifier':
return (
<TableValue value={value} name={name} type='value'/>
<TableValue map_id={map_id} value={value} name={name} type='value' />
)
default:
return (

View File

@ -6,8 +6,13 @@ import { BASE_URL } from '../../../constants';
import { fetcher } from '../../../http/axiosInstance';
import { useObjectsStore } from '../../../store/objects';
const ObjectParameters = () => {
const { currentObjectId } = useObjectsStore()
const ObjectParameters = ({
map_id
}: {
map_id: string
}) => {
const { currentObjectId } = useObjectsStore().id[map_id]
const { data: valuesData, isValidating: valuesValidating } = useSWR(
currentObjectId ? `/general/values/all?object_id=${currentObjectId}` : null,
@ -42,13 +47,13 @@ const ObjectParameters = () => {
sortedParams.map((param: IObjectParam) => {
if (param.date_po == null) {
return (
<ObjectParameter key={id_param} param={param} showLabel={false} />
<ObjectParameter map_id={map_id} key={id_param} param={param} showLabel={false} />
)
}
}
)
) : (
<ObjectParameter key={id_param} param={sortedParams[0]} />
<ObjectParameter map_id={map_id} key={id_param} param={sortedParams[0]} />
);
})
}

View File

@ -9,12 +9,14 @@ interface ITCBParameterProps {
vtable: string;
inactive?: boolean;
name: string;
map_id: string;
}
const TCBParameter = ({
value,
vtable,
name
name,
map_id
}: ITCBParameterProps) => {
//Get value
@ -28,6 +30,8 @@ const TCBParameter = ({
)
const tables = [
'BoilersTemper',
'FuelsParametrs',
'PipesTypes',
'vAddRepairEvent',
'vBoilers',
@ -80,7 +84,7 @@ const TCBParameter = ({
const TCBValue = (vtable: string) => {
if (tables.includes(vtable)) {
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
<TableValue map_id={map_id} value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
} else {
return (

View File

@ -10,6 +10,7 @@ interface TableValueProps {
type: 'value' | 'boolean' | 'number' | 'select' | 'string';
unit?: string | null | undefined;
vtable?: string;
map_id: string;
}
const TableValue = ({
@ -17,10 +18,10 @@ const TableValue = ({
value,
type,
unit,
vtable
vtable,
map_id
}: TableValueProps) => {
const { selectedDistrict } = useObjectsStore()
const { selectedDistrict } = useObjectsStore().id[map_id]
//Get available values
const { data: tcbAll, isValidating } = useSWR(
type === 'select' && selectedDistrict ? `/general/params/tcb?vtable=${vtable}&id_city=${selectedDistrict}` : null,
@ -56,10 +57,10 @@ const TableValue = ({
/>
:
type === 'select' && !isValidating && tcbAll ?
<Select size='xs' data={tcbAll} defaultValue={JSON.stringify(value)} />
<Select size='xs' data={tcbAll} value={JSON.stringify(value)} />
:
type === 'string' ?
<Textarea size='xs' defaultValue={value as string} autosize minRows={1} />
<Textarea size='xs' value={value as string} autosize minRows={1} />
:
<Text size='xs'>{value as string}</Text>
}

View File

@ -31,9 +31,9 @@ const TabsPane = ({
</ScrollAreaAutosize>
<ScrollAreaAutosize h='100%' offsetScrollbars p='xs'>
<ScrollAreaAutosize h='100%' offsetScrollbars>
{tabs.map(tab => (
<Tabs.Panel key={tab.value} value={tab.value}>
<Tabs.Panel p='xs' key={tab.value} value={tab.value}>
{tab.view}
</Tabs.Panel>
))}

View File

@ -1,34 +1,35 @@
import { Coordinate, distance, rotate } from "ol/coordinate";
import { containsExtent, Extent, getCenter, getHeight, getWidth } from "ol/extent";
import { Extent, getCenter, getHeight, getWidth } from "ol/extent";
import Feature from "ol/Feature";
import GeoJSON from "ol/format/GeoJSON";
import { Circle, Geometry, LineString, Polygon, SimpleGeometry } from "ol/geom";
import { Circle, Geometry, LineString, Point, Polygon, SimpleGeometry } from "ol/geom";
import VectorLayer from "ol/layer/Vector";
import VectorImageLayer from "ol/layer/VectorImage";
import Map from "ol/Map";
import { addCoordinateTransforms, addProjection, get, getTransform, Projection, ProjectionLike, transform } from "ol/proj";
import VectorSource from "ol/source/Vector";
import proj4 from "proj4";
import { firstStyleFunction, fourthStyleFunction, selectStyle, styleFunction, thirdStyleFunction } from "./MapStyles";
import { Type } from "ol/geom/Geometry";
import { Draw, Modify, Snap, Translate } from "ol/interaction";
import { noModifierKeys } from "ol/events/condition";
import { IGeometryType, IRectCoords } from "../../interfaces/map";
import { never, noModifierKeys, platformModifierKeyOnly, primaryAction } from "ol/events/condition";
import { IGeometryType } from "../../interfaces/map";
import { uploadCoordinates } from "../../actions/map";
import { ImageStatic } from "ol/source";
import ImageLayer from "ol/layer/Image";
import { IFigure, ILine } from "../../interfaces/gis";
import { fromCircle, fromExtent } from "ol/geom/Polygon";
import { measureStyleFunction, modifyStyle } from "./Measure/MeasureStyles";
import { getCurrentTool, getMeasureClearPrevious, getMeasureType, getTipPoint, setStatusText } from "../../store/map";
import { MutableRefObject } from "react";
import { getSelectedCity, getSelectedYear, setSelectedRegion } from "../../store/objects";
import { getCurrentTool, getDraw, getDrawingLayerSource, getImageLayer, getMap, getMeasureClearPrevious, getMeasureDraw, getMeasureModify, getMeasureSource, getMeasureType, getOverlayLayerSource, getSnap, getTipPoint, getTranslate, PrintOrientation, setDraw, setFile, setMeasureDraw, setPolygonExtent, setRectCoords, setSnap, setTranslate } from "../../store/map";
import Collection from "ol/Collection";
import { SketchCoordType } from "ol/interaction/Draw";
const calculateAngle = (coords: [number, number][]) => {
const [start, end] = coords;
const dx = end[0] - start[0];
const dy = end[1] - start[1];
return Math.atan2(dy, dx); // Angle in radians
}
export function processLine(
line: ILine,
scaling: number,
mapCenter: Coordinate,
linesLayer: MutableRefObject<VectorLayer<VectorSource>>
mapCenter: Coordinate
) {
const x1 = line.x1 * scaling
const y1 = line.y1 * scaling
@ -37,27 +38,30 @@ export function processLine(
const center = [mapCenter[0], mapCenter[1]]
const testCoords = [
const testCoords: [number, number][] = [
[center[0] + x1, center[1] - y1],
[center[0] + x2, center[1] - y2],
]
const feature = new Feature(new LineString(testCoords))
feature.setStyle(styleFunction(feature))
const geometry = new LineString(testCoords)
feature.set('type', line.type)
feature.set('geometry_type', 'line')
feature.set('planning', line.planning)
feature.set('object_id', line.object_id)
linesLayer.current?.getSource()?.addFeature(feature)
return {
type: "Feature",
geometry: new GeoJSON().writeGeometryObject(geometry),
properties: {
type: line.type,
geometry_type: 'line',
object_id: line.object_id,
planning: line.planning,
rotation: calculateAngle(testCoords)
}
}
}
export function processFigure(
figure: IFigure,
scaling: number,
mapCenter: Coordinate,
figuresLayer: MutableRefObject<VectorLayer<VectorSource>>
) {
if (figure.figure_type_id == 1) {
const width = figure.width * scaling
@ -75,13 +79,15 @@ export function processFigure(
const ellipseGeom = fromCircle(circleGeom, 64)
ellipseGeom.scale(1, height / width)
const feature = new Feature(ellipseGeom)
feature.setStyle(firstStyleFunction(feature))
feature.set('type', figure.type)
feature.set('object_id', figure.object_id)
feature.set('planning', figure.planning)
figuresLayer.current?.getSource()?.addFeature(feature)
return {
type: "Feature",
geometry: new GeoJSON().writeGeometryObject(ellipseGeom),
properties: {
type: figure.type,
object_id: figure.object_id,
planning: figure.planning
}
}
}
if (figure.figure_type_id == 3) {
@ -101,15 +107,15 @@ export function processFigure(
if (coords) {
const polygon = new Polygon([coords])
const feature = new Feature({
geometry: polygon
})
feature.set('object_id', figure.object_id)
feature.set('planning', figure.planning)
feature.set('type', figure.type)
feature.setStyle(thirdStyleFunction(feature))
figuresLayer.current?.getSource()?.addFeature(feature)
return {
type: "Feature",
geometry: new GeoJSON().writeGeometryObject(polygon),
properties: {
type: figure.type,
object_id: figure.object_id,
planning: figure.planning
}
}
}
}
@ -135,23 +141,179 @@ export function processFigure(
const geometry1 = new Polygon([testCoords])
const anchor1 = center
geometry1.rotate(-figure.angle * Math.PI / 180, anchor1)
const feature1 = new Feature(geometry1)
feature1.set('object_id', figure.object_id)
feature1.set('planning', figure.planning)
feature1.set('type', figure.type)
feature1.set('angle', figure.angle)
feature1.setStyle(fourthStyleFunction(feature1))
figuresLayer.current?.getSource()?.addFeature(feature1)
return {
type: "Feature",
geometry: new GeoJSON().writeGeometryObject(geometry1),
properties: {
type: figure.type,
object_id: figure.object_id,
planning: figure.planning,
angle: figure.angle
}
}
}
}
export const handleImageDrop = (
event: React.DragEvent<HTMLDivElement>,
map_id: string,
) => {
event.preventDefault()
event.stopPropagation()
if (!event.dataTransfer?.files) return
const files = event.dataTransfer.files
if (files.length > 0) {
const file = files[0]
setFile(map_id, file)
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = () => {
const imageUrl = reader.result as string;
const img = new Image();
img.src = imageUrl;
img.onload = () => {
const map = getMap(map_id)
if (map) {
const center = map.getView().getCenter() || [0, 0];
const width = img.naturalWidth;
const height = img.naturalHeight;
const resolution = map.getView().getResolution() || 0;
const extent = [
center[0] - (width * resolution) / 20,
center[1] - (height * resolution) / 20,
center[0] + (width * resolution) / 20,
center[1] + (height * resolution) / 20,
];
// Create a polygon feature with the same extent as the image
const polygonFeature = new Feature({ geometry: fromExtent(extent), });
const overlayLayerSource = getOverlayLayerSource(map_id)
// Add the polygon feature to the drawing layer source
overlayLayerSource.addFeature(polygonFeature);
const imageLayer = getImageLayer(map_id)
// Set up the initial image layer with the extent
imageLayer.setSource(new ImageStatic({ url: imageUrl, imageExtent: extent, }));
updateImageSource(map_id, imageUrl, polygonFeature)
//map.current.addLayer(imageLayer.current);
// Add interactions for translation and scaling
setTranslate(map_id, new Translate({
layers: [imageLayer],
features: new Collection([polygonFeature]),
}))
const defaultStyle = new Modify({ source: overlayLayerSource })
.getOverlay()
.getStyleFunction();
const modify = new Modify({
insertVertexCondition: never,
source: overlayLayerSource,
condition: function (event) {
return primaryAction(event) && !platformModifierKeyOnly(event);
},
deleteCondition: never,
features: new Collection([polygonFeature]),
style: function (feature) {
feature.get('features').forEach(function (modifyFeature: Feature) {
const modifyGeometry = modifyFeature.get('modifyGeometry')
if (modifyGeometry) {
const point = (feature.getGeometry() as Point).getCoordinates()
let modifyPoint = modifyGeometry.point
if (!modifyPoint) {
// save the initial geometry and vertex position
modifyPoint = point;
modifyGeometry.point = modifyPoint;
modifyGeometry.geometry0 = modifyGeometry.geometry;
// get anchor and minimum radius of vertices to be used
const result = calculateCenter(modifyGeometry.geometry0);
modifyGeometry.center = result.center;
modifyGeometry.minRadius = result.minRadius;
}
const center = modifyGeometry.center;
const minRadius = modifyGeometry.minRadius;
let dx, dy;
dx = modifyPoint[0] - center[0];
dy = modifyPoint[1] - center[1];
const initialRadius = Math.sqrt(dx * dx + dy * dy);
if (initialRadius > minRadius) {
const initialAngle = Math.atan2(dy, dx);
dx = point[0] - center[0];
dy = point[1] - center[1];
const currentRadius = Math.sqrt(dx * dx + dy * dy);
if (currentRadius > 0) {
const currentAngle = Math.atan2(dy, dx);
const geometry = modifyGeometry.geometry0.clone();
geometry.scale(currentRadius / initialRadius, undefined, center);
geometry.rotate(currentAngle - initialAngle, center);
modifyGeometry.geometry = geometry;
}
}
}
})
const res = map?.getView()?.getResolution()
if (typeof res === 'number' && feature && defaultStyle) {
return defaultStyle(feature, res)
}
}
});
const translate = getTranslate(map_id)
if (translate) {
translate.on('translateend', () => updateImageSource(map_id, imageUrl, polygonFeature));
map.addInteraction(translate);
}
//modify.on('modifyend', updateImageSource);
modify.on('modifystart', function (event) {
event.features.forEach(function (feature) {
feature.set(
'modifyGeometry',
{ geometry: feature.getGeometry()?.clone() },
true,
);
});
});
modify.on('modifyend', function (event) {
event.features.forEach(function (feature) {
const modifyGeometry = feature.get('modifyGeometry');
if (modifyGeometry) {
feature.setGeometry(modifyGeometry.geometry);
feature.unset('modifyGeometry', true);
}
})
updateImageSource(map_id, imageUrl, polygonFeature)
})
map.addInteraction(modify);
}
};
};
reader.readAsDataURL(file);
}
}
}
// Function to update the image layer with a new source when extent changes
export const updateImageSource = (
map_id: string,
imageUrl: string,
imageLayer: React.MutableRefObject<ImageLayer<ImageStatic>>,
polygonFeature: Feature<Polygon>,
setPolygonExtent: (value: React.SetStateAction<Extent | undefined>) => void,
setRectCoords: React.Dispatch<React.SetStateAction<IRectCoords | undefined>>
polygonFeature: Feature<Polygon>
) => {
const newExtent = polygonFeature.getGeometry()?.getExtent();
@ -160,14 +322,14 @@ export const updateImageSource = (
const topRight = polygonFeature.getGeometry()?.getCoordinates()[0][2]
const bottomRight = polygonFeature.getGeometry()?.getCoordinates()[0][3]
setRectCoords({
setRectCoords(map_id, {
bl: bottomLeft,
tl: topLeft,
tr: topRight,
br: bottomRight
})
setPolygonExtent(newExtent)
setPolygonExtent(map_id, newExtent)
if (newExtent && bottomLeft && bottomRight && topRight && topLeft) {
const originalExtent = calculateExtent(bottomLeft, topLeft, topRight, bottomRight)
@ -175,34 +337,68 @@ export const updateImageSource = (
url: imageUrl,
imageExtent: originalExtent,
projection: rotateProjection('EPSG:3857', calculateRotationAngle(bottomLeft, bottomRight), originalExtent)
});
imageLayer.current.setSource(newImageSource);
})
getImageLayer(map_id).setSource(newImageSource)
}
};
export const fixedAspectRatioBox = (
coordinates: SketchCoordType,
geometry: SimpleGeometry | undefined,
orientation?: PrintOrientation
): SimpleGeometry => {
if (!Array.isArray(coordinates) || coordinates.length < 2) {
return geometry ?? new Polygon([]);
}
const [start, end] = coordinates as Coordinate[];
const minX = start[0];
const minY = start[1];
const maxX = end[0];
const width = maxX - minX;
const height = orientation === 'horizontal' ? Math.abs(width / Math.sqrt(2)) : Math.abs(width * Math.sqrt(2)); // Maintain 1:√2 ratio
const maxY = end[1] > minY ? minY + height : minY - height;
const boxCoords: Coordinate[][] = [[
[minX, minY],
[maxX, minY],
[maxX, maxY],
[minX, maxY],
[minX, minY], // Close the polygon
]];
if (geometry) {
geometry.setCoordinates(boxCoords);
return geometry;
}
return new Polygon(boxCoords);
};
export const addInteractions = (
drawingLayerSource: React.MutableRefObject<VectorSource<Feature<Geometry>>>,
translate: React.MutableRefObject<Translate | null>,
draw: React.MutableRefObject<Draw | null>,
map: React.MutableRefObject<Map | null>,
snap: React.MutableRefObject<Snap | null>,
measureDraw: React.MutableRefObject<Draw | null>,
measureSource: React.MutableRefObject<VectorSource<Feature<Geometry>>>,
measureModify: React.MutableRefObject<Modify>,
map_id: string
) => {
const currentTool = getCurrentTool()
const clearPrevious = getMeasureClearPrevious()
const measureType = getMeasureType()
const tipPoint = getTipPoint()
console.log("Adding interactions")
const currentTool = getCurrentTool(map_id)
const clearPrevious = getMeasureClearPrevious(map_id)
const measureType = getMeasureType(map_id)
const tipPoint = getTipPoint(map_id)
const map = getMap(map_id)
const measureModify = getMeasureModify(map_id)
if (currentTool !== 'Measure' && currentTool !== 'Mover' && currentTool !== 'Edit') {
draw.current = new Draw({
source: drawingLayerSource.current,
setDraw(map_id, new Draw({
source: getDrawingLayerSource(map_id),
type: currentTool as Type,
condition: noModifierKeys
})
}))
draw.current.on('drawend', function (s) {
const draw = getDraw(map_id)
if (draw) {
draw.on('drawend', function (s) {
console.log(s.feature.getGeometry()?.getType())
let type: IGeometryType = 'POLYGON'
@ -221,9 +417,14 @@ export const addInteractions = (
uploadCoordinates(coordinates, type)
})
map?.current?.addInteraction(draw.current)
snap.current = new Snap({ source: drawingLayerSource.current })
map?.current?.addInteraction(snap.current)
map?.addInteraction(draw)
setSnap(map_id, new Snap({ source: getDrawingLayerSource(map_id) }))
const snap = getSnap(map_id)
if (snap) {
map?.addInteraction(snap)
}
}
}
if (currentTool == 'Measure') {
@ -234,35 +435,45 @@ export const addInteractions = (
const idleTip = 'Кликните, чтобы начать измерение';
let tip = idleTip;
measureDraw.current = new Draw({
source: measureSource.current,
setMeasureDraw(map_id, new Draw({
source: getMeasureSource(map_id),
type: drawType,
style: function (feature) {
return measureStyleFunction(feature, drawType, tip);
return measureStyleFunction(map_id, feature, drawType, tip);
},
});
measureDraw.current.on('drawstart', function () {
}))
const measureDraw = getMeasureDraw(map_id)
if (measureDraw) {
measureDraw.on('drawstart', function () {
if (clearPrevious) {
measureSource.current.clear();
getMeasureSource(map_id).clear();
}
measureModify.current.setActive(false);
measureModify.setActive(false);
tip = activeTip;
});
measureDraw.current.on('drawend', function () {
measureDraw.on('drawend', function () {
modifyStyle.setGeometry(tipPoint as Geometry);
measureModify.current.setActive(true);
map.current?.once('pointermove', function () {
measureModify.setActive(true);
map?.once('pointermove', function () {
modifyStyle.setGeometry('');
});
tip = idleTip;
});
measureModify.current.setActive(true);
map.current?.addInteraction(measureDraw.current);
measureModify.setActive(true)
map?.addInteraction(measureDraw)
}
}
if (currentTool == 'Mover') {
translate.current = new Translate()
map?.current?.addInteraction(translate.current)
setTranslate(map_id, new Translate())
const translate = getTranslate(map_id)
if (translate) {
map?.addInteraction(translate)
}
}
if (currentTool == 'Edit') {
@ -271,91 +482,12 @@ export const addInteractions = (
}
}
export function regionsInit(
map: React.MutableRefObject<Map | null>,
selectedRegion: React.MutableRefObject<Feature<Geometry> | null>,
regionsLayer: React.MutableRefObject<VectorImageLayer<Feature<Geometry>, VectorSource<Feature<Geometry>>>>,
) {
regionsLayer.current.once('change', function () {
if (getSelectedCity() === null || getSelectedYear() === null) {
const extent = regionsLayer.current.getSource()?.getExtent()
if (extent && !extent?.every(val => Math.abs(val) === Infinity)) {
map.current?.getView().fit(fromExtent(extent) as SimpleGeometry, { duration: 500, maxZoom: 18, padding: [60, 60, 60, 60] })
}
}
})
map.current?.on('click', function (e) {
if (selectedRegion.current !== null) {
selectedRegion.current = null
}
if (map.current) {
map.current.forEachFeatureAtPixel(e.pixel, function (feature, layer) {
if (layer === regionsLayer.current) {
selectedRegion.current = feature as Feature
// Zoom to the selected feature
zoomToFeature(map, selectedRegion.current)
if (feature.get('id')) {
setSelectedRegion(feature.get('id'))
}
return true
} else return false
});
}
})
// Show current selected region
map.current?.on('pointermove', function (e) {
if (selectedRegion.current !== null) {
selectedRegion.current.setStyle(undefined)
selectedRegion.current = null
}
if (map.current) {
map.current.forEachFeatureAtPixel(e.pixel, function (feature, layer) {
if (layer === regionsLayer.current) {
selectedRegion.current = feature as Feature
selectedRegion.current.setStyle(selectStyle)
if (feature.get('district')) {
setStatusText(feature.get('district'))
}
return true
} else return false
})
}
})
// Hide regions layer when fully visible
map.current?.on('moveend', function () {
const viewExtent = map.current?.getView().calculateExtent(map.current.getSize())
const features = regionsLayer.current.getSource()?.getFeatures()
let isViewCovered = false
features?.forEach((feature: Feature) => {
const featureExtent = feature?.getGeometry()?.getExtent()
if (viewExtent && featureExtent) {
if (containsExtent(featureExtent, viewExtent)) {
isViewCovered = true
}
}
})
regionsLayer.current.setVisible(!isViewCovered)
})
}
const zoomToFeature = (map: React.MutableRefObject<Map | null>, feature: Feature) => {
export const zoomToFeature = (map_id: string, feature: Feature) => {
const geometry = feature.getGeometry()
const extent = geometry?.getExtent()
if (map.current && extent) {
map.current.getView().fit(extent, {
if (getMap(map_id) && extent) {
getMap(map_id)?.getView().fit(extent, {
duration: 300,
maxZoom: 19,
})
@ -363,8 +495,8 @@ const zoomToFeature = (map: React.MutableRefObject<Map | null>, feature: Feature
}
// Function to save features to localStorage
export const saveFeatures = (layerRef: MutableRefObject<VectorLayer<VectorSource> | null>) => {
const features = layerRef.current?.getSource()?.getFeatures()
export const saveFeatures = (map_id: string) => {
const features = getDrawingLayerSource(map_id).getFeatures()
if (features && features.length > 0) {
const geoJSON = new GeoJSON()
const featuresJSON = geoJSON.writeFeatures(features)
@ -373,14 +505,14 @@ export const saveFeatures = (layerRef: MutableRefObject<VectorLayer<VectorSource
}
// Function to load features from localStorage
export const loadFeatures = (layerSource: React.MutableRefObject<VectorSource<Feature<Geometry>>>) => {
export const loadFeatures = (map_id: string) => {
const savedFeatures = localStorage.getItem('savedFeatures')
if (savedFeatures) {
const geoJSON = new GeoJSON()
const features = geoJSON.readFeatures(savedFeatures, {
featureProjection: 'EPSG:4326', // Ensure the projection is correct
})
layerSource.current?.addFeatures(features) // Add features to the vector source
getDrawingLayerSource(map_id).addFeatures(features)
//drawingLayer.current?.getSource()?.changed()
}
}
@ -531,6 +663,25 @@ function getGridCellPosition(x: number, y: number, extent: Extent, zoom: number)
return { tileX, tileY };
}
function calculateTransformations(alignPoints: Coordinate[]) {
const [P1, P2, P3, P4] = alignPoints;
// Translation vector (move P1 to P3)
const translation = [P3[0] - P1[0], P3[1] - P1[1]];
// Scaling factor (distance between P3 and P4 divided by P1 and P2)
const distanceLayer = Math.sqrt((P2[0] - P1[0]) ** 2 + (P2[1] - P1[1]) ** 2);
const distanceMap = Math.sqrt((P4[0] - P3[0]) ** 2 + (P4[1] - P3[1]) ** 2);
const scale = distanceMap / distanceLayer;
// Rotation angle (difference in angles between the two lines)
const angleLayer = Math.atan2(P2[1] - P1[1], P2[0] - P1[0]);
const angleMap = Math.atan2(P4[1] - P3[1], P4[0] - P3[0]);
const rotation = angleMap - angleLayer;
return { translation, scale, rotation };
}
function calculateCenter(geometry: SimpleGeometry) {
let center, coordinates, minRadius;
const type = geometry.getType();
@ -574,8 +725,41 @@ function calculateCenter(geometry: SimpleGeometry) {
};
}
function applyTransformations(
layer: VectorLayer,
transformations: {
translation: number[];
scale: number;
rotation: number;
},
origin: Coordinate
) {
const { translation, scale, rotation } = transformations;
const source = layer.getSource();
if (!source) return;
source.getFeatures().forEach((feature) => {
const geometry = feature.getGeometry();
if (geometry) {
// Translate
geometry.translate(translation[0], translation[1]);
// Scale (around the origin)
geometry.scale(scale, scale, origin);
// Rotate (around the origin)
geometry.rotate(rotation, origin);
}
});
console.log("Transformations applied to figuresLayer");
}
export {
rotateProjection,
calculateTransformations,
calculateRotationAngle,
calculateExtent,
calculateCentroid,
@ -583,5 +767,6 @@ export {
normalize,
getTileIndex,
getGridCellPosition,
calculateCenter
calculateCenter,
applyTransformations
}

View File

@ -21,7 +21,7 @@ interface Props {
}
interface ViewerProps {
url: string
url: string | ArrayBuffer | Blob
}
function PdfViewer({
@ -82,7 +82,7 @@ function DocxViewer({
)
}
function ExcelViewer({
export function ExcelViewer({
url
}: ViewerProps) {
const previewContainerRef = useRef(null)
@ -120,7 +120,7 @@ function ImageViewer({
width: '100%',
height: '100%'
}}>
<img alt='image-preview' src={url} style={{
<img alt='image-preview' src={url as string} style={{
display: 'flex',
maxWidth: '100%',
maxHeight: '100%'

View File

@ -1,8 +1,7 @@
import { IconBuildingFactory2, IconComponents, IconDeviceDesktopAnalytics, IconFiles, IconHome, IconLogin, IconLogin2, IconMap, IconPassword, IconReport, IconServer, IconSettings, IconShield, IconTable, IconUsers } from "@tabler/icons-react";
import { IconBuildingFactory2, IconComponents, IconDeviceDesktopAnalytics, IconFiles, IconHome, IconLogin, IconLogin2, IconMap, IconPassword, IconReport, IconServer, IconSettings, IconShield, IconUsers } from "@tabler/icons-react";
import SignIn from "../pages/auth/SignIn";
import SignUp from "../pages/auth/SignUp";
import PasswordReset from "../pages/auth/PasswordReset";
import TableTest from "../pages/TableTest";
import ComponentTest from "../pages/ComponentTest";
import MonitorPage from "../pages/MonitorPage";
import Settings from "../pages/Settings";
@ -14,6 +13,8 @@ import Reports from "../pages/Reports";
import Servers from "../pages/Servers";
import Boilers from "../pages/Boilers";
import MapTest from "../pages/MapTest";
import PrintReport from "../pages/PrintReport";
import DBManager from "../pages/DBManager";
// Определение страниц с путями и компонентом для рендера
@ -135,15 +136,6 @@ const pages = [
dashboard: true,
enabled: false,
},
{
label: "Table test",
path: "/table-test",
icon: <IconTable />,
component: <TableTest />,
drawer: true,
dashboard: true,
enabled: false,
},
{
label: "Component test",
path: "/component-test",
@ -153,6 +145,24 @@ const pages = [
dashboard: true,
enabled: false,
},
{
label: "Print report test",
path: "/print-report-test",
icon: <IconComponents />,
component: <PrintReport />,
drawer: true,
dashboard: true,
enabled: false,
},
{
label: "Тест БД",
path: "/db-manager",
icon: <IconComponents />,
component: <DBManager />,
drawer: true,
dashboard: true,
enabled: true,
},
]
export {

View File

@ -0,0 +1,48 @@
export const schemas = [
'2018',
'2019',
'2020',
'2021',
'2022',
'2023',
'2024',
]
export const scaleOptions = [
{
label: '1:500000',
value: '500'
},
{
label: '1:100000',
value: '250'
},
{
label: '1:50000',
value: '50'
},
{
label: '1:25000',
value: '25'
},
{
label: '1:10000',
value: '10'
},
]
export const satMapsProviders = [
{ label: 'Google', value: 'google' },
{ label: 'Яндекс', value: 'yandex' },
{ label: 'Подложка', value: 'custom' },
{ label: 'Static', value: 'static' }
]
export const printDimensions = {
a0: [1189, 841],
a1: [841, 594],
a2: [594, 420],
a3: [420, 297],
a4: [297, 210],
a5: [210, 148],
}

View File

@ -1,3 +1,10 @@
import { Point } from "ol/geom";
import { ToolType } from "../types/tools";
import { SatelliteMapsProvider } from "./map";
import Map from "ol/Map";
import { Coordinate } from "ol/coordinate";
import { Mode } from "../store/map";
export interface IFigure {
object_id: string,
figure_type_id: number,
@ -34,3 +41,57 @@ export interface ILine {
type: number,
planning: boolean
}
export interface ICitySettings {
city_id: number;
image_width: number;
image_height: number;
scale: number;
offset_x: number;
offset_y: number;
rotation: number;
image_scale: number;
}
export interface TypeRole {
id: number;
sname: string;
r: number | null;
g: number | null;
b: number | null;
object_type_id: number;
object_role_id: number;
parent_type_id: string | null;
owner_role_ids: string | null;
necessary_params_ids: string | null;
vis_params_ids: string | null;
read_only_params_ids: string | null;
inactive_r: number | null;
inactive_g: number | null;
inactive_b: number | null;
color_group: number | null;
}
export interface CreateMapState {
currentTool: ToolType;
measureType: "LineString" | "Polygon";
measureShowSegments: boolean;
measureClearPrevious: boolean;
tipPoint: Point | null;
map: Map | null;
currentZ: number | undefined;
currentX: number | undefined;
currentY: number | undefined;
currentCoordinate: Coordinate | null;
statusText: string;
satMapsProvider: SatelliteMapsProvider;
selectedObjectType: number | null;
alignMode: boolean;
mode: Mode;
typeRoles: TypeRole[] | null;
setCurrentCoordinate: (currentCoordinate: Coordinate | null) => void;
setCurrentZ: (currentZ: number | undefined) => void;
setCurrentX: (currentX: number | undefined) => void;
setCurrentY: (currentY: number | undefined) => void;
setStatusText: (statusText: string) => void;
}

View File

@ -9,7 +9,7 @@ import { pages } from '../constants/app';
function DashboardLayout() {
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure()
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true)
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(false)
const navigate = useNavigate()
const getPageTitle = () => {
@ -50,7 +50,7 @@ function DashboardLayout() {
</Group>
<Group w='auto'>
{getPageTitle()}
<Text fw='600'>{getPageTitle()}</Text>
</Group>
<Group id='header-portal' w='auto' ml='auto'>
@ -123,14 +123,20 @@ function DashboardLayout() {
leftSection={item.icon}
active={location.pathname === item.path}
style={{ textWrap: 'nowrap' }}
// styles={(theme, { active }) => ({
// root: {
// color: active ? theme.colors.blue[6] : theme.colors.dark[5],
// fontWeight: active ? "bold" : "normal",
// },
// leftSection: {
// color: active ? theme.colors.blue[6] : theme.colors.dark[5], // Icon color
// }
// })}
/>
))}
</AppShell.Navbar>
<AppShell.Main>
<Flex w={{
sm: desktopOpened ? 'calc(100% - 200px)' : 'calc(100% - 50px)',
base: '100%'
}} h={'calc(100% - 60px)'} style={{ transition: "width 0.2s ease" }} pos={'fixed'}>
<Flex bg={colorScheme === 'dark' ? undefined : '#E8E8E8'} w={{ sm: desktopOpened ? 'calc(100% - 200px)' : 'calc(100% - 50px)', base: '100%' }} h={'calc(100% - 60px)'} style={{ transition: "width 0.2s ease" }} pos={'fixed'}>
<Outlet />
</Flex>
</AppShell.Main>

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'
import { useBoilers } from '../hooks/swrHooks'
import { Badge, ScrollAreaAutosize, Table, Text } from '@mantine/core'
import { IBoiler } from '../interfaces/fuel'
import { Stack, Text } from '@mantine/core'
import CustomTable from '../components/CustomTable'
function Boilers() {
const [boilersPage, setBoilersPage] = useState(1)
@ -24,21 +24,43 @@ function Boilers() {
setBoilerSearch("")
}, [])
const boilersColumns = [
{ field: 'id_object', headerName: 'ID', type: "number" },
{ field: 'boiler_name', headerName: 'Название', type: "string", flex: 1 },
{ field: 'boiler_code', headerName: 'Код', type: "string", flex: 1 },
{ field: 'id_city', headerName: 'Город', type: "string", flex: 1 },
{ field: 'activity', headerName: 'Активен', type: "boolean", flex: 1 },
]
return (
<ScrollAreaAutosize w={'100%'} h={'100%'} p='sm'>
<Stack w={'100%'} h={'100%'} p='sm'>
<Text size="xl" fw={600}>
Котельные
</Text>
{boilers &&
<CustomTable data={boilers} columns={[
{
accessorKey: 'id_object',
header: 'ID',
cell: (info) => info.getValue(),
},
{
accessorKey: 'boiler_name',
header: 'Название',
cell: (info) => info.getValue(),
},
{
accessorKey: 'boiler_code',
header: 'Код',
cell: (info) => info.getValue(),
},
{
accessorKey: 'id_city',
header: 'Город',
cell: (info) => info.getValue(),
},
{
accessorKey: 'activity',
header: 'Активен',
cell: (info) => info.getValue(),
},
]} />
}
{/* {boilers &&
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
@ -77,8 +99,8 @@ function Boilers() {
))}
</Table.Tbody>
</Table>
}
</ScrollAreaAutosize>
} */}
</Stack>
)
}

View File

@ -0,0 +1,78 @@
import { Stack, Tabs } from '@mantine/core'
import useSWR from 'swr'
import { BASE_URL } from '../constants'
import { fetcher } from '../http/axiosInstance'
import { useState } from 'react'
import CustomTable from '../components/CustomTable'
const DBManager = () => {
const { data: tablesData } = useSWR(`/db/tables`, (key) => fetcher(key, BASE_URL.ems), {
revalidateOnFocus: false
})
return (
<Stack w={'100%'} h={'100%'} p='xs'>
{tablesData && Array.isArray(tablesData) && tablesData.length > 0 &&
<Tabs w='100%' h='80%'>
<Stack h='100%'>
<Tabs.List>
{tablesData.map(table => (
<Tabs.Tab key={table.tablename} value={table.tablename}>
{table.tablename}
</Tabs.Tab>
))}
</Tabs.List>
{tablesData.map(table => (
<Tabs.Panel h='100%' key={table.tablename} value={table.tablename} w='100%'>
<TableData tablename={table.tablename} />
</Tabs.Panel>
))}
</Stack>
</Tabs>
}
{/* <Card withBorder radius='sm'>
<Stack>
<Group justify='flex-start'>
<Text fw='bold'>Figures</Text>
</Group>
<Grid>
<Button>Import from New_Gis (erases data)</Button>
</Grid>
</Stack>
</Card> */}
</Stack>
)
}
const TableData = ({
tablename
}: { tablename: string }) => {
const [offset] = useState(0)
const [limit] = useState(20)
const { data: columnsData } = useSWR(`/db/columns/${tablename}`, (key) => fetcher(key, BASE_URL.ems), {
revalidateOnFocus: false
})
const { data: rowsData } = useSWR(columnsData ? `/db/rows/${tablename}?offset=${offset}&limit=${limit}` : null, (key) => fetcher(key, BASE_URL.ems), {
revalidateOnFocus: false
})
return (
<>
{columnsData && rowsData && Array.isArray(columnsData) && Array.isArray(rowsData) && columnsData.length > 0 &&
<CustomTable data={rowsData} columns={columnsData.map(column => (
{
accessorKey: column.column_name,
header: column.column_name,
cell: (info) => JSON.stringify(info.getValue()).length > 30 ? [JSON.stringify(info.getValue()).substring(0, 30), '...'].join('') : JSON.stringify(info.getValue()),
}
))} />
}
</>
)
}
export default DBManager

View File

@ -1,8 +1,81 @@
import { Container, Stack, Tabs } from '@mantine/core'
import MapComponent from '../components/map/MapComponent'
import { useEffect } from 'react'
import { initializeObjectsState } from '../store/objects'
import { deleteMapTab, setCurrentTab, useAppStore } from '../store/app'
import { initializeMapState, useMapStore } from '../store/map'
import { v4 as uuidv4 } from 'uuid'
function MapTest() {
const { mapTab, currentTab } = useAppStore()
const { id } = useMapStore()
const tabs = [
{
id: uuidv4(),
year: 2023,
region: 11,
district: 145,
},
// {
// id: uuidv4(),
// year: 2023,
// region: 11,
// district: 146,
// },
]
useEffect(() => {
tabs.map(tab => useAppStore.setState((state) => {
initializeObjectsState(tab.id, tab.region, tab.district, null, tab.year)
initializeMapState(tab.id)
return {
mapTab: {
...state.mapTab,
[tab.id]: {
year: tab.year,
region: tab.region,
district: tab.district
}
}
}
}))
setCurrentTab(tabs[0].id)
return () => {
tabs.map(tab => deleteMapTab(tab.id))
}
}, [])
return (
<MapComponent />
<Container fluid w='100%' pos='relative' p={0}>
<Tabs h='100%' variant='default' value={currentTab} onChange={setCurrentTab} keepMounted={true}>
<Stack gap={0} h='100%'>
<Tabs.List>
{Object.entries(mapTab).map(([key]) => (
<Tabs.Tab value={key} key={key}>
{id[key]?.mapLabel}
</Tabs.Tab>
))}
</Tabs.List>
{Object.entries(mapTab).map(([key]) => (
<Tabs.Panel value={key} key={key} h='100%' pos='relative'>
<MapComponent
key={key}
id={key}
active={currentTab === key}
/>
</Tabs.Panel>
))}
</Stack>
</Tabs>
</Container>
)
}

File diff suppressed because it is too large Load Diff

View File

@ -93,11 +93,11 @@ export default function Reports() {
</Table.Thead>
<Table.Tbody>
{[...new Set(Object.keys(report).flatMap(key => Object.keys(report[key])))].map(id => {
const row: any = { id: Number(id) };
const row: Record<string, unknown> = { id: Number(id) };
Object.keys(report).forEach(key => {
row[key] = report[key][id];
});
return (<Table.Tr key={row.id}>
return (<Table.Tr key={row.id as number}>
{[
{ field: 'id', headerName: '№', width: 70 },
...Object.keys(report).map(key => ({
@ -125,7 +125,7 @@ export default function Reports() {
)
}
return (
<Table.Td key={`${row.id}-${column.headerName}`}>{row[column.field]}</Table.Td>
<Table.Td key={`${row.id}-${column.headerName}`}>{row[column.field] as string}</Table.Td>
)
})}
</Table.Tr>)

View File

@ -1,63 +1,42 @@
import { useRoles } from '../hooks/swrHooks'
import { CreateField } from '../interfaces/create'
import RoleService from '../services/RoleService'
import FormFields from '../components/FormFields'
import { Button, Loader, Modal, ScrollAreaAutosize, Table } from '@mantine/core'
import { useDisclosure } from '@mantine/hooks'
import { IRole } from '../interfaces/role'
import { Loader, Stack } from '@mantine/core'
import CustomTable from '../components/CustomTable'
export default function Roles() {
const { roles, isError, isLoading } = useRoles()
const [opened, { open, close }] = useDisclosure(false);
const createFields: CreateField[] = [
{ key: 'name', headerName: 'Название', type: 'string', required: true, defaultValue: '' },
{ key: 'description', headerName: 'Описание', type: 'string', required: false, defaultValue: '' },
]
const columns = [
{ field: 'id', headerName: 'ID', type: "number" },
{ field: 'name', headerName: 'Название', flex: 1, editable: true },
{ field: 'description', headerName: 'Описание', flex: 1, editable: true },
];
if (isError) return <div>Произошла ошибка при получении данных.</div>
if (isLoading) return <Loader />
return (
<ScrollAreaAutosize w={'100%'} h={'100%'} p='sm'>
<Button onClick={open}>
Добавить роль
</Button>
<Modal opened={opened} onClose={close} title="Создание роли" centered>
<FormFields
fields={createFields}
<Stack w={'100%'} h={'100%'} p='sm'>
<CustomTable
createFields={createFields}
submitHandler={RoleService.createRole}
/>
</Modal>
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
{columns.map(column => (
<Table.Th key={column.field}>{column.headerName}</Table.Th>
))}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{roles.map((role: IRole) => (
<Table.Tr
key={role.id}
>
{columns.map(column => (
<Table.Td key={column.field}>{role[column.field as keyof IRole]}</Table.Td>
))}
</Table.Tr>
))}
</Table.Tbody>
</Table>
</ScrollAreaAutosize>
data={roles} columns={[
{
accessorKey: 'id',
header: 'id',
cell: (info) => info.getValue(),
},
{
accessorKey: 'name',
header: 'Название',
cell: (info) => info.getValue(),
},
{
accessorKey: 'description',
header: 'Описание',
cell: (info) => info.getValue(),
},
]} />
</Stack>
)
}

View File

@ -1,13 +0,0 @@
import { Flex } from '@mantine/core';
import CustomTable from '../components/CustomTable';
function TableTest() {
return (
<Flex direction='column' align='flex-start' gap='sm' p='sm'>
<CustomTable />
</Flex>
)
}
export default TableTest

View File

@ -3,10 +3,8 @@ import { IRole } from "../interfaces/role"
import { useEffect, useState } from "react"
import { CreateField } from "../interfaces/create"
import UserService from "../services/UserService"
import FormFields from "../components/FormFields"
import { Badge, Button, Flex, Loader, Modal, Pagination, ScrollAreaAutosize, Select, Table } from "@mantine/core"
import { useDisclosure } from "@mantine/hooks"
import { IUser } from "../interfaces/user"
import { Flex, Loader, Stack } from "@mantine/core"
import CustomTable from "../components/CustomTable"
export default function Users() {
const { users, isError, isLoading } = useUsers()
@ -21,8 +19,6 @@ export default function Users() {
}
}, [roles])
const [opened, { open, close }] = useDisclosure(false);
const createFields: CreateField[] = [
{ key: 'email', headerName: 'E-mail', type: 'string', required: true, defaultValue: '' },
{ key: 'login', headerName: 'Логин', type: 'string', required: true, defaultValue: '' },
@ -32,24 +28,6 @@ export default function Users() {
{ key: 'password', headerName: 'Пароль', type: 'string', required: true, defaultValue: '' },
]
const columns = [
{ field: 'id', headerName: 'ID', type: "number", flex: 1 },
{ field: 'email', headerName: 'Email', flex: 1, editable: true },
{ field: 'login', headerName: 'Логин', flex: 1, editable: true },
{ field: 'phone', headerName: 'Телефон', flex: 1, editable: true },
{ field: 'name', headerName: 'Имя', flex: 1, editable: true },
{ field: 'surname', headerName: 'Фамилия', flex: 1, editable: true },
{ field: 'is_active', headerName: 'Статус', type: "boolean", flex: 1, editable: true },
{
field: 'role_id',
headerName: 'Роль',
valueOptions: roles ? roles.map((role: IRole) => ({ label: role.name, value: role.id })) : [],
type: 'singleSelect',
flex: 1,
editable: true
},
];
if (isError) return (
<div>
Произошла ошибка при получении данных.
@ -65,97 +43,50 @@ export default function Users() {
}
return (
<ScrollAreaAutosize w={'100%'} h={'100%'} p='sm'>
<Button onClick={open}>
Добавить пользователя
</Button>
<Modal opened={opened} onClose={close} title="Регистрация пользователя" centered>
<FormFields
fields={createFields}
submitHandler={UserService.createUser}
/>
</Modal>
<Stack w={'100%'} h={'100%'} p='xs'>
{Array.isArray(roleOptions) &&
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
{columns.map(column => (
<Table.Th key={column.field}>{column.headerName}</Table.Th>
))}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{users.map((user: IUser) => (
<Table.Tr
key={user.id}
//bg={selectedRows.includes(element.position) ? 'var(--mantine-color-blue-light)' : undefined}
>
{columns.map(column => {
if (column.field === 'is_active') {
return (
user.is_active ? (
<Table.Td key={column.field}>
<Badge fullWidth variant="light">
Активен
</Badge>
</Table.Td>
) : (
<Table.Td key={column.field}>
<Badge color="gray" fullWidth variant="light">
Отключен
</Badge>
</Table.Td>
)
)
}
else if (column.field === 'role_id') {
return (
<Table.Td key={column.field}>
<Select
data={roleOptions}
defaultValue={user.role_id.toString()}
variant="unstyled"
allowDeselect={false}
/>
</Table.Td>
)
}
else return (
<Table.Td key={column.field}>{user[column.field as keyof IUser]}</Table.Td>
)
})}
</Table.Tr>
))}
</Table.Tbody>
</Table>
}
<Pagination total={10} />
{/* <DataGrid
density="compact"
autoHeight
style={{ width: "100%" }}
rows={users}
columns={columns}
initialState={{
pagination: {
paginationModel: { page: 0, pageSize: 10 },
<CustomTable
createFields={createFields}
submitHandler={UserService.createUser}
data={users}
columns={[
{
accessorKey: 'email',
header: 'E-mail',
cell: (info) => info.getValue(),
},
}}
pageSizeOptions={[10, 20, 50, 100]}
checkboxSelection
disableRowSelectionOnClick
processRowUpdate={(updatedRow) => {
return updatedRow
}}
onProcessRowUpdateError={() => {
}}
/> */}
</ScrollAreaAutosize>
{
accessorKey: 'login',
header: 'Логин',
cell: (info) => info.getValue(),
},
{
accessorKey: 'phone',
header: 'Телефон',
cell: (info) => info.getValue(),
},
{
accessorKey: 'name',
header: 'Имя',
cell: (info) => info.getValue(),
},
{
accessorKey: 'surname',
header: 'Фамилия',
cell: (info) => info.getValue(),
},
{
accessorKey: 'is_active',
header: 'Активен',
cell: (info) => info.getValue(),
},
{
accessorKey: 'role_id',
header: 'Роль',
cell: (info) => info.getValue(),
}
]} />
}
</Stack>
)
}

View File

@ -0,0 +1,39 @@
import { AxiosRequestConfig } from "axios";
import axiosInstance from "../http/axiosInstance";
import { BASE_URL } from "../constants";
import { Extent } from "ol/extent";
import { IRectCoords } from "../interfaces/map";
const config: AxiosRequestConfig = {
baseURL: BASE_URL.ems
}
export default class GisService {
static async getAddress(limit?: number, page?: number) {
return await axiosInstance.get(`/general/address?limit=${limit || 10}&page=${page || 1}`, config)
}
static async uploadOverlay(file: File | null, polygonExtent: Extent | undefined, rectCoords: IRectCoords | undefined) {
if (file && polygonExtent && rectCoords?.bl && rectCoords?.tl && rectCoords?.tr && rectCoords?.br) {
const formData = new FormData()
formData.append('file', file)
formData.append('extentMinX', polygonExtent[0].toString())
formData.append('extentMinY', polygonExtent[1].toString())
formData.append('extentMaxX', polygonExtent[2].toString())
formData.append('extentMaxY', polygonExtent[3].toString())
formData.append('blX', rectCoords?.bl[0].toString())
formData.append('blY', rectCoords?.bl[1].toString())
formData.append('tlX', rectCoords?.tl[0].toString())
formData.append('tlY', rectCoords?.tl[1].toString())
formData.append('trX', rectCoords?.tr[0].toString())
formData.append('trY', rectCoords?.tr[1].toString())
formData.append('brX', rectCoords?.br[0].toString())
formData.append('brY', rectCoords?.br[1].toString())
return await axiosInstance.post(`/tiles/upload`, formData, config)
}
}
//static async updateFigure(object_id: string, )
}

43
client/src/store/app.ts Normal file
View File

@ -0,0 +1,43 @@
import { create } from 'zustand';
export type Mode = 'edit' | 'view'
export interface AppState {
mapTab: Record<string, {
year: number | null,
region: number | null,
district: number | null
}>,
currentTab: string | null;
}
export const useAppStore = create<AppState>(() => ({
currentTab: null,
mapTab: {}
}))
const getCurrentTab = () => useAppStore.getState().currentTab
const setCurrentTab = (id: string | null) => useAppStore.setState(() => ({ currentTab: id }))
const setMapTabYear = (id: string, year: number | null) =>
useAppStore.setState((state) => {
return {
mapTab: {
...state.mapTab,
[id]: { ...state.mapTab[id], year: year }
}
}
})
const deleteMapTab = (id: string) =>
useAppStore.setState((state) => {
const { [id]: _, ...remainingTabs } = state.mapTab;
return { mapTab: remainingTabs };
})
export {
deleteMapTab,
getCurrentTab,
setCurrentTab,
setMapTabYear
}

View File

@ -1,11 +1,40 @@
import { create } from 'zustand';
import { ToolType } from '../types/tools';
import { Point } from 'ol/geom';
import { Geometry, Point } from 'ol/geom';
import Map from 'ol/Map';
import { Coordinate } from 'ol/coordinate';
import { SatelliteMapsProvider } from '../interfaces/map';
import { IRectCoords, SatelliteMapsProvider } from '../interfaces/map';
import { TypeRole } from '../interfaces/gis';
import VectorSource from 'ol/source/Vector';
import Feature from 'ol/Feature';
import { containsExtent, Extent } from 'ol/extent';
import { Draw, Modify, Select, Snap, Translate } from 'ol/interaction';
import TileLayer from 'ol/layer/Tile';
import { v4 as uuidv4 } from 'uuid';
import { googleMapsSatelliteSource } from '../components/map/MapSources';
import VectorLayer from 'ol/layer/Vector';
import { ImageStatic, OSM } from 'ol/source';
import ImageLayer from 'ol/layer/Image';
import { drawingLayerStyle, figureStyle, highlightStyleRed, highlightStyleYellow, lineStyle, overlayStyle, regionsLayerStyle, selectStyle } from '../components/map/MapStyles';
import { Fill, Stroke, Style } from 'ol/style';
import { VectorImage } from 'ol/layer';
import { click, pointerMove } from 'ol/events/condition';
import { measureStyleFunction, modifyStyle } from '../components/map/Measure/MeasureStyles';
import MapBrowserEvent from 'ol/MapBrowserEvent';
import { transform } from 'ol/proj';
import { applyTransformations, calculateTransformations, fixedAspectRatioBox, zoomToFeature } from '../components/map/mapUtils';
import { setCurrentObjectId, setSelectedRegion } from './objects';
import View from 'ol/View';
import { getPrintOrientation } from './print';
export type Mode = 'edit' | 'view' | 'print'
export type PrintScale = '500' | '250' | '50' | '25' | '10'
export type PrintOrientation = 'horizontal' | 'vertical'
interface MapState {
id: Record<string, {
currentTool: ToolType;
measureType: "LineString" | "Polygon";
measureShowSegments: boolean;
@ -19,15 +48,339 @@ interface MapState {
statusText: string;
satMapsProvider: SatelliteMapsProvider;
selectedObjectType: number | null;
alignMode: boolean;
mode: Mode;
typeRoles: TypeRole[] | null;
mapLabel: string;
file: File | null;
measureSource: VectorSource<Feature<Geometry>>;
measureDraw: Draw | null;
polygonExtent: Extent | undefined;
rectCoords: IRectCoords | undefined;
alignPoints: Coordinate[];
alignModeLayer: VectorLayer;
drawingLayer: VectorLayer;
drawingLayerSource: VectorSource;
draw: Draw | null;
snap: Snap | null;
translate: Translate | null;
overlayLayerSource: VectorSource;
satLayer: TileLayer;
nodeLayer: VectorLayer;
nodeLayerSource: VectorSource;
staticMapLayer: ImageLayer<ImageStatic>;
figuresLayer: VectorLayer<VectorSource>;
linesLayer: VectorLayer<VectorSource>;
regionsLayer: VectorImage;
citiesLayer: VectorLayer;
districtBoundLayer: VectorImage;
imageLayer: ImageLayer<ImageStatic>;
selectedArea: Feature | null;
baseLayer: TileLayer;
measureLayer: VectorLayer;
measureModify: Modify;
overlayLayer: VectorLayer;
regionSelect: Select;
lineSelect: Select;
previousView: View | undefined | null;
printArea: Extent | null;
printLayer: VectorLayer;
printSource: VectorSource;
printAreaDraw: Draw;
printPreviewSize: number[];
printDim: 'a0' | 'a1' | 'a2' | 'a3' | 'a4' | 'a5';
printRes: number;
printOrientation: PrintOrientation;
printScale: PrintScale;
printScaleLine: boolean;
}>;
}
export const useMapStore = create<MapState>(() => ({
id: {}
}))
export const initializeMapState = (
id: string
) => {
useMapStore.setState((state) => {
const hitTolerance = 10
const baseLayer = new TileLayer({ source: new OSM(), properties: { id: uuidv4(), name: 'OpenStreetMap' } })
const satLayer = new TileLayer({ source: googleMapsSatelliteSource, visible: false, properties: { id: uuidv4(), name: 'Спутник' } })
const staticMapLayer = new ImageLayer<ImageStatic>({ properties: { id: uuidv4(), name: 'Подложка' } })
// Region select
const regionSelect = new Select({ condition: pointerMove, style: selectStyle, layers: (layer) => layer.get('type') === 'region' })
// Line select
const lineSelect = new Select({ condition: click, style: highlightStyleRed, hitTolerance: hitTolerance, layers: (layer) => layer.get('type') === 'line', })
lineSelect.on('select', (e) => {
if (e.selected[0]) {
setCurrentObjectId(id, e.selected[0].get('object_id'))
}
})
const lineHover = new Select({ condition: pointerMove, style: highlightStyleYellow, hitTolerance: hitTolerance, layers: (layer) => layer.get('type') === 'line', })
// Figure select
const figureSelect = new Select({ condition: click, style: highlightStyleRed, hitTolerance: hitTolerance, layers: (layer) => layer.get('type') === 'figure' })
figureSelect.on('select', (e) => {
if (e.selected[0]) {
setCurrentObjectId(id, e.selected[0].get('object_id'))
}
})
const figureHover = new Select({ condition: pointerMove, style: highlightStyleYellow, hitTolerance: hitTolerance, layers: (layer) => layer.get('type') === 'figure', })
const measureSource = new VectorSource()
const measureLayer = new VectorLayer({
source: measureSource,
style: (feature) => measureStyleFunction(id, feature),
properties: { id: uuidv4(), type: 'measure', name: 'Линейка' }
})
const measureModify = new Modify({
source: measureSource,
style: modifyStyle
})
const nodeLayerSource = new VectorSource()
const nodeLayer = new VectorLayer({
source: nodeLayerSource,
style: drawingLayerStyle,
properties: {
id: uuidv4(),
name: 'Узлы'
}
})
const overlayLayerSource = new VectorSource()
const overlayLayer = new VectorLayer({
source: overlayLayerSource,
style: overlayStyle,
properties: {
id: uuidv4(),
name: 'Наложения'
}
})
const drawingLayerSource = new VectorSource()
const drawingLayer = new VectorLayer({
source: drawingLayerSource
})
const regionsLayer = new VectorImage({
source: new VectorSource(),
declutter: true,
style: regionsLayerStyle,
properties: {
id: uuidv4(),
name: 'Регион',
type: 'region'
}
})
const districtBoundLayer = new VectorImage({ style: new Style({ stroke: new Stroke({ color: 'red', width: 2 }), }) })
const citiesLayer = new VectorLayer({ source: new VectorSource(), properties: { id: uuidv4(), name: 'Города' } })
const linesLayer = new VectorLayer({
source: new VectorSource(),
declutter: true,
properties: { id: uuidv4(), type: 'line', name: 'Линии' },
style: function (feature) {
const objectType = feature.get('type')
const typeRole = getTypeRoles(id)?.find((el) => el.id.toString() === String(objectType))
if (typeRole) {
lineStyle.setStroke(new Stroke({
color: `rgb(${typeRole.r},${typeRole.g},${typeRole.b})`
}))
}
//lineStyle.getText()?.setText('11,4')//(feature.get('object_id'))
lineStyle.getText()?.setRotation(feature.get('rotation'))
lineStyle.getText()?.setOffsetY(-8)
return lineStyle
},
})
const figuresLayer = new VectorLayer({
source: new VectorSource(),
declutter: true,
properties: { id: uuidv4(), type: 'figure', name: 'Фигуры' },
style: function (feature) {
const objectType = feature.get('type')
const typeRole = getTypeRoles(id)?.find((el) => el.id.toString() === String(objectType))
if (typeRole) {
figureStyle.setFill(new Fill({
color: `rgb(${typeRole.r},${typeRole.g},${typeRole.b})`
}))
}
figureStyle.getText()?.setText(feature.get('object_id'))
return figureStyle
}
})
const imageLayer = new ImageLayer<ImageStatic>({ properties: { id: uuidv4(), name: 'Изображение' } })
const alignModeLayer = new VectorLayer({ source: new VectorSource(), properties: { id: uuidv4(), type: 'align', name: 'Подгонка' } })
const printSource = new VectorSource()
const printLayer = new VectorLayer({
source: printSource
})
const printAreaDraw = new Draw({
source: printSource,
type: 'Circle',
style: (feature) => {
return new Style({
fill: new Fill({
color: "rgba(0, 0, 255, 0.3)", // Semi-transparent blue fill
}),
stroke: new Stroke({
color: "blue",
width: 2,
}),
image: undefined, // 🚀 This removes the default point!
});
},
geometryFunction: function (coords, geom) {
const printOrientation = getPrintOrientation()
return fixedAspectRatioBox(coords, geom, printOrientation)
},
})
printAreaDraw.on('drawend', (e) => {
const extent = e.feature.getGeometry()?.getExtent()
if (extent) {
setPrintArea(id, extent)
}
})
const map = new Map({
controls: [],
layers: [
baseLayer,
satLayer,
staticMapLayer,
regionsLayer,
districtBoundLayer,
citiesLayer,
linesLayer,
figuresLayer,
drawingLayer,
imageLayer,
overlayLayer,
nodeLayer,
measureLayer,
alignModeLayer,
printLayer
]
})
map.addInteraction(regionSelect)
map.addInteraction(lineSelect)
map.addInteraction(lineHover)
map.addInteraction(figureSelect)
map.addInteraction(figureHover)
// map.on('pointermove', function (e: MapBrowserEvent<UIEvent>) {
// setCurrentCoordinate(id, e.coordinate)
// const currentExtent = get('EPSG:3857')?.getExtent() as Extent
// const { tileX, tileY } = getGridCellPosition(e.coordinate[0], e.coordinate[1], currentExtent, Number(map?.getView().getZoom()?.toFixed(0)))
// setCurrentZ(id, Number(map?.getView().getZoom()?.toFixed(0)))
// setCurrentX(id, tileX)
// setCurrentY(id, tileY)
// const pixel = map?.getEventPixel(e.originalEvent)
// if (pixel) {
// map?.forEachFeatureAtPixel(pixel, function (feature, layer) {
// if (layer.get('type') === 'region') {
// if (feature.get('entity_id')) {
// setStatusText(id, feature.get('entity_id'))
// }
// }
// })
// }
// })
map.on('click', function (e: MapBrowserEvent<UIEvent>) {
if (getAlignMode(id)) {
const alignPoints = getAlignPoints(id)
const alignModeLayer = getAlignModeLayer(id)
if (alignPoints.length < 4) {
//setAlignPoints(id, [...alignPoints, e.coordinate])
alignPoints.push(e.coordinate)
alignModeLayer.getSource()?.addFeature(new Feature(new Point(e.coordinate)))
if (alignPoints.length === 4) {
const transformations = calculateTransformations(alignPoints);
// Use the first map point (P3) as the origin for scaling and rotation
const origin = alignPoints[2];
applyTransformations(getFiguresLayer(id), transformations, origin);
applyTransformations(getLinesLayer(id), transformations, origin);
setAlignPoints(id, [])
alignModeLayer.getSource()?.clear()
}
}
} else {
const pixel = map?.getEventPixel(e.originalEvent)
if (pixel) {
map?.forEachFeatureAtPixel(pixel, function (feature, layer) {
if (layer.get('type') === 'region') {
console.log("clicked on region")
zoomToFeature(id, feature as Feature)
if (feature.get('entity_id')) {
setSelectedRegion(id, feature.get('entity_id'))
}
}
})
}
}
})
map.on('moveend', function () {
const viewExtent = map?.getView().calculateExtent(map.getSize())
const features = regionsLayer.getSource()?.getFeatures()
let isViewCovered = false
features?.forEach((feature: Feature) => {
const featureExtent = feature?.getGeometry()?.getExtent()
if (viewExtent && featureExtent) {
if (containsExtent(featureExtent, viewExtent)) {
isViewCovered = true
}
}
})
regionsLayer.setVisible(!isViewCovered)
})
map.setView(
new View({
center: transform([129.7466541, 62.083504], 'EPSG:4326', 'EPSG:3857'),//center: fromLonLat([130.401113, 67.797368]),
zoom: 5,
maxZoom: 21,
//extent: mapExtent,
})
)
return {
id: {
...state.id,
[id]: {
currentTool: null,
measureType: "LineString",
measureShowSegments: true,
measureClearPrevious: true,
tipPoint: null,
map: null,
map: map,
currentZ: undefined,
currentX: undefined,
currentY: undefined,
@ -35,81 +388,395 @@ export const useMapStore = create<MapState>(() => ({
statusText: '',
satMapsProvider: 'google',
selectedObjectType: null,
}));
const setCurrentZ = (z: number | undefined) => useMapStore.setState(() => ({ currentZ: z }))
const setCurrentX = (x: number | undefined) => useMapStore.setState(() => ({ currentX: x }))
const setCurrentY = (y: number | undefined) => useMapStore.setState(() => ({ currentY: y }))
const setCurrentCoordinate = (c: Coordinate | null) => useMapStore.setState(() => ({ currentCoordinate: c }))
const setStatusText = (t: string) => useMapStore.setState(() => ({ statusText: t }))
const setSatMapsProvider = (p: SatelliteMapsProvider) => useMapStore.setState(() => ({ satMapsProvider: p }))
const setSelectedObjectType = (t: number | null) => useMapStore.setState(() => ({ selectedObjectType: t }))
const setMap = (m: Map | null) => useMapStore.setState(() => ({ map: m }))
const setTipPoint = (tipPoint: Point | null) => {
useMapStore.setState(() => ({ tipPoint: tipPoint }))
alignMode: false,
mode: 'view',
typeRoles: null,
mapLabel: 'Карта',
file: null,
measureSource: measureSource,
measureDraw: null,
polygonExtent: undefined,
rectCoords: undefined,
alignPoints: [],
alignModeLayer: alignModeLayer,
drawingLayer: drawingLayer,
drawingLayerSource: drawingLayerSource,
draw: null,
snap: null,
translate: null,
overlayLayerSource: overlayLayerSource,
satLayer: satLayer,
nodeLayerSource: nodeLayerSource,
staticMapLayer: staticMapLayer,
figuresLayer: figuresLayer,
linesLayer: linesLayer,
regionsLayer: regionsLayer,
citiesLayer: citiesLayer,
districtBoundLayer: districtBoundLayer,
imageLayer: imageLayer,
selectedArea: null,
baseLayer: baseLayer,
measureLayer: measureLayer,
measureModify: measureModify,
nodeLayer: nodeLayer,
overlayLayer: overlayLayer,
regionSelect: regionSelect,
lineSelect: lineSelect,
previousView: null,
printArea: null,
printLayer: printLayer,
printSource: printSource,
printAreaDraw: printAreaDraw,
printPreviewSize: [640, 320],
printDim: 'a4',
printOrientation: 'horizontal',
printRes: 72,
printScale: '250',
printScaleLine: true
}
}
}
})
}
const getTipPoint = () => {
return useMapStore.getState().tipPoint
}
export const setPrintOrientation = (id: string, orientation: PrintOrientation) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], printOrientation: orientation }
}
}
})
const getMap = () => {
return useMapStore.getState().map
}
export const setPrintScaleLine = (id: string, bool: boolean) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], printScaleLine: bool }
}
}
})
const setMeasureType = (tool: "LineString" | "Polygon") => {
useMapStore.setState(() => ({ measureType: tool }))
}
export const setPrintScale = (id: string, printScale: PrintScale) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], printScale: printScale }
}
}
})
const getMeasureType = () => {
return useMapStore.getState().measureType
}
export const setPreviousView = (id: string, view: View | undefined | null) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], previousView: view }
}
}
})
const setCurrentTool = (tool: ToolType) => {
tool === useMapStore.getState().currentTool
? useMapStore.setState(() => ({ currentTool: null }))
: useMapStore.setState(() => ({ currentTool: tool }))
}
export const clearPrintArea = (id: string) => useMapStore.setState((state) => {
state.id[id].printSource.clear()
const getCurrentTool = () => {
return useMapStore.getState().currentTool
}
return {
id: {
...state.id,
[id]: { ...state.id[id], printArea: null }
}
}
})
const getMeasureShowSegments = () => {
return useMapStore.getState().measureShowSegments
}
export const setPrintArea = (id: string, extent: Extent | null) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], printArea: extent }
}
}
})
const getMeasureClearPrevious = () => {
return useMapStore.getState().measureClearPrevious
}
export const getFiguresLayer = (id: string) => useMapStore.getState().id[id].figuresLayer
export const getLinesLayer = (id: string) => useMapStore.getState().id[id].linesLayer
export const getMeasureModify = (id: string) => useMapStore.getState().id[id].measureModify
const setMeasureShowSegments = (bool: boolean) => {
useMapStore.setState(() => ({ measureShowSegments: bool }))
}
export const getOverlayLayerSource = (id: string) => useMapStore.getState().id[id].overlayLayerSource
export const setOverlayLayerSource = (id: string, source: VectorSource) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], overlayLayerSource: source }
}
}
})
const setMeasureClearPrevious = (bool: boolean) => {
useMapStore.setState(() => ({ measureClearPrevious: bool }))
}
export const getImageLayer = (id: string) => useMapStore.getState().id[id].imageLayer
export const setImageLayer = (id: string, layer: ImageLayer<ImageStatic>) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], imageLayer: layer }
}
}
})
export {
setCurrentTool,
getCurrentTool,
setMeasureShowSegments,
setMeasureClearPrevious,
getMeasureShowSegments,
getMeasureClearPrevious,
setMeasureType,
getMeasureType,
getTipPoint,
setTipPoint,
setCurrentZ,
setCurrentX,
setCurrentY,
setCurrentCoordinate,
setStatusText,
setSatMapsProvider,
setSelectedObjectType,
setMap,
getMap
}
export const getTranslate = (id: string) => useMapStore.getState().id[id].translate
export const setTranslate = (id: string, translate: Translate | null) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], translate: translate }
}
}
})
export const getSnap = (id: string) => useMapStore.getState().id[id].snap
export const setSnap = (id: string, snap: Snap | null) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], snap: snap }
}
}
})
export const getDraw = (id: string) => useMapStore.getState().id[id].draw
export const setDraw = (id: string, draw: Draw | null) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], draw: draw }
}
}
})
export const getDrawingLayerSource = (id: string) => useMapStore.getState().id[id].drawingLayerSource
export const getMeasureSource = (id: string) => useMapStore.getState().id[id].measureSource
export const getMeasureDraw = (id: string) => useMapStore.getState().id[id].measureDraw
export const setMeasureDraw = (id: string, draw: Draw) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], measureDraw: draw }
}
}
})
export const setAlignPoints = (id: string, c: Coordinate[]) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], alignPoints: c }
}
}
})
export const setRectCoords = (id: string, c: IRectCoords | undefined) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], rectCoords: c }
}
}
})
export const setPolygonExtent = (id: string, extent: Extent | undefined) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], polygonExtent: extent }
}
}
})
export const setFile = (id: string, file: File | null) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], file: file }
}
}
})
export const setMapLabel = (id: string, label: string) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], mapLabel: label }
}
}
})
export const setCurrentZ = (id: string, z: number | undefined) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], currentZ: z }
}
}
})
export const setCurrentX = (id: string, x: number | undefined) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], currentX: x }
}
}
})
export const setCurrentY = (id: string, y: number | undefined) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], currentY: y }
}
}
})
export const setCurrentCoordinate = (id: string, c: Coordinate | null) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], currentCoordinate: c }
}
}
})
export const setStatusText = (id: string, t: string) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], statusText: t }
}
}
})
export const setSatMapsProvider = (id: string, p: SatelliteMapsProvider) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], satMapsProvider: p }
}
}
})
export const setSelectedObjectType = (id: string, t: number | null) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], selectedObjectType: t }
}
}
})
export const setMap = (id: string, m: Map | null) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], map: m }
}
}
})
export const setAlignMode = (id: string, m: boolean) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], alignMode: m }
}
}
})
export const setTipPoint = (id: string, tipPoint: Point | null) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], tipPoint: tipPoint }
}
}
})
export const getTipPoint = (id: string) => useMapStore.getState().id[id].tipPoint
export const getAlignMode = (id: string) => useMapStore.getState().id[id].alignMode
export const getAlignModeLayer = (id: string) => useMapStore.getState().id[id].alignModeLayer
export const getAlignPoints = (id: string) => useMapStore.getState().id[id].alignPoints
export const getMap = (id: string) => useMapStore.getState().id[id].map
export const setMeasureType = (id: string, tool: "LineString" | "Polygon") => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], measureType: tool }
}
}
})
export const getMeasureType = (id: string) => useMapStore.getState().id[id].measureType
export const setCurrentTool = (id: string, tool: ToolType) =>
tool === useMapStore.getState().id[id].currentTool
? useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], currentTool: null }
}
}
})
: useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], currentTool: tool }
}
}
})
export const getCurrentTool = (id: string) => useMapStore.getState().id[id].currentTool
export const getMeasureShowSegments = (id: string) => useMapStore.getState().id[id].measureShowSegments
export const getMeasureClearPrevious = (id: string) => useMapStore.getState().id[id].measureClearPrevious
export const setMeasureShowSegments = (id: string, bool: boolean) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], measureShowSegments: bool }
}
}
})
export const setMeasureClearPrevious = (id: string, bool: boolean) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], measureClearPrevious: bool }
}
}
})
export const getMode = (id: string) => useMapStore.getState().id[id].mode
export const setMode = (id: string, mode: Mode) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], mode: mode }
}
}
})
export const getTypeRoles = (id: string) => useMapStore.getState().id[id].typeRoles
export const setTypeRoles = (id: string, typeRoles: TypeRole[] | null) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], typeRoles: typeRoles }
}
}
})

View File

@ -1,60 +1,87 @@
import { create } from 'zustand';
interface ObjectsState {
id: Record<string, {
selectedRegion: number | null;
selectedDistrict: number | null;
selectedCity: number | null;
selectedYear: number | null;
currentObjectId: string | null;
}>;
}
export const useObjectsStore = create<ObjectsState>(() => ({
selectedRegion: null,
selectedDistrict: null,
selectedCity: null,
selectedYear: null,
currentObjectId: null
id: {}
}));
const getSelectedCity = () => {
return useObjectsStore.getState().selectedCity
}
export const initializeObjectsState = (
id: string,
selectedRegion: number | null,
selectedDistrict: number | null,
selectedCity: number | null,
selectedYear: number | null,
) => useObjectsStore.setState((state) => {
return {
id: {
...state.id,
[id]: {
selectedRegion: selectedRegion,
selectedDistrict: selectedDistrict,
selectedCity: selectedCity,
selectedYear: selectedYear,
currentObjectId: null,
}
}
}
})
const setSelectedRegion = (region: number | null) => {
useObjectsStore.setState(() => ({ selectedRegion: region }))
}
export const getSelectedCity = (id: string) => useObjectsStore.getState().id[id].selectedCity
export const setSelectedCity = (id: string, city: number | null) => useObjectsStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], selectedCity: city }
}
}
})
const setSelectedDistrict = (district: number | null) => {
useObjectsStore.setState(() => ({ selectedDistrict: district }))
}
export const getSelectedRegion = (id: string) => useObjectsStore.getState().id[id].selectedRegion
export const setSelectedRegion = (id: string, region: number | null) => useObjectsStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], selectedRegion: region }
}
}
})
const setSelectedCity = (city: number | null) => {
useObjectsStore.setState(() => ({ selectedCity: city }))
}
export const getSelectedDistrict = (id: string) => useObjectsStore.getState().id[id].selectedDistrict
export const setSelectedDistrict = (id: string, district: number | null) => useObjectsStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], selectedDistrict: district }
}
}
})
const getSelectedYear = () => {
return useObjectsStore.getState().selectedYear
}
const setSelectedYear = (year: number | null) => {
useObjectsStore.setState(() => ({ selectedYear: year }))
}
export const getSelectedYear = (id: string) => useObjectsStore.getState().id[id].selectedYear
export const setSelectedYear = (id: string, year: number | null) => useObjectsStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], selectedYear: year }
}
}
})
const getCurrentObjectId = () => {
return useObjectsStore.getState().currentObjectId
}
const setCurrentObjectId = (objectId: string | null) => {
useObjectsStore.setState(() => ({ currentObjectId: objectId }))
}
export {
getSelectedCity,
setSelectedCity,
getSelectedYear,
setSelectedYear,
getCurrentObjectId,
setCurrentObjectId,
setSelectedRegion,
setSelectedDistrict
}
export const getCurrentObjectId = (id: string) => useObjectsStore.getState().id[id].currentObjectId
export const setCurrentObjectId = (id: string, objectId: string | null) => useObjectsStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], currentObjectId: objectId }
}
}
})

23
client/src/store/print.ts Normal file
View File

@ -0,0 +1,23 @@
import { create } from 'zustand';
export type PrintOrientation = 'horizontal' | 'vertical'
export type PrintFormat = 'a0' | 'a1' | 'a2' | 'a3' | 'a4' | 'a5'
export const printResolutions = ['72', '150', '200', '300']
export interface PrintState {
printFormat: PrintFormat;
printOrientation: PrintOrientation;
printResolution: number;
}
export const usePrintStore = create<PrintState>(() => ({
printFormat: 'a4',
printOrientation: 'horizontal',
printResolution: 72
}))
export const getPrintOrientation = () => usePrintStore.getState().printOrientation
export const setPrintFormat = (format: PrintFormat) => usePrintStore.setState(() => ({ printFormat: format }))
export const setPrintOrientation = (orientation: PrintOrientation) => usePrintStore.setState(() => ({ printOrientation: orientation }))
export const setPrintResolution = (resolution: number) => usePrintStore.setState(() => ({ printResolution: resolution }))

View File

@ -1,11 +1,13 @@
import { defineConfig } from 'vite'
import { defineConfig, type PluginOption } from 'vite'
import react from '@vitejs/plugin-react-swc'
import { nodePolyfills } from 'vite-plugin-node-polyfills'
import { visualizer } from 'rollup-plugin-visualizer'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
nodePolyfills(),
react(),
visualizer() as PluginOption
],
})

View File

@ -936,10 +936,10 @@
resolved "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz"
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.20.13", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
version "7.24.7"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz"
integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==
"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.20.13", "@babel/runtime@^7.23.2", "@babel/runtime@^7.25.6", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
version "7.26.0"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz"
integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==
dependencies:
regenerator-runtime "^0.14.0"
@ -982,6 +982,29 @@
resolved "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.0.tgz"
integrity sha512-+imAQkHf7U/Rwvu0wk1XWgsP3WnpCWmK7B48f0XqSNzgk64+grljTKC7pnO/xBiEMUziF7vKRfbBnOQhg126qQ==
"@dnd-kit/accessibility@^3.1.1":
version "3.1.1"
resolved "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz"
integrity sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==
dependencies:
tslib "^2.0.0"
"@dnd-kit/core@^6.3.1":
version "6.3.1"
resolved "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz"
integrity sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==
dependencies:
"@dnd-kit/accessibility" "^3.1.1"
"@dnd-kit/utilities" "^3.2.2"
tslib "^2.0.0"
"@dnd-kit/utilities@^3.2.2":
version "3.2.2"
resolved "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz"
integrity sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==
dependencies:
tslib "^2.0.0"
"@esbuild/win32-x64@0.21.5":
version "0.21.5"
resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz"
@ -1065,6 +1088,19 @@
resolved "https://registry.npmjs.org/@fontsource/open-sans/-/open-sans-5.0.28.tgz"
integrity sha512-hBvJHY76pJT/JynGUB5EXWhnzjYfLdcMn655J5p1v9lTT9HdQSy+keq2KPVXO2Htlg998BBa3p6u/jlrZ6w0kg==
"@hello-pangea/dnd@^17.0.0":
version "17.0.0"
resolved "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-17.0.0.tgz"
integrity sha512-LDDPOix/5N0j5QZxubiW9T0M0+1PR0rTDWeZF5pu1Tz91UQnuVK4qQ/EjY83Qm2QeX0eM8qDXANfDh3VVqtR4Q==
dependencies:
"@babel/runtime" "^7.25.6"
css-box-model "^1.2.1"
memoize-one "^6.0.0"
raf-schd "^4.0.3"
react-redux "^9.1.2"
redux "^5.0.1"
use-memo-one "^1.1.3"
"@humanwhocodes/config-array@^0.11.14":
version "0.11.14"
resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz"
@ -1430,6 +1466,11 @@
resolved "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz"
integrity sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==
"@techstark/opencv-js@^4.10.0-release.1":
version "4.10.0-release.1"
resolved "https://registry.npmjs.org/@techstark/opencv-js/-/opencv-js-4.10.0-release.1.tgz"
integrity sha512-S4XELidRiQeA0q1s9VQLo540wCxUo24r1O4C+LqZ6llX+sPCXvZCPv3Ice8dEIr0uavyZ8YZeKXSBdDgMXSXjw==
"@tiptap/core@^2.7.0", "@tiptap/core@^2.7.3":
version "2.7.3"
resolved "https://registry.npmjs.org/@tiptap/core/-/core-2.7.3.tgz"
@ -1707,7 +1748,7 @@
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^16.8.0 || ^17.0.0 || ^18.0.0", "@types/react@^16.9.0 || ^17.0.0 || ^18.0.0", "@types/react@^18.2.66", "@types/react@>=16.8":
"@types/react@*", "@types/react@^16.8.0 || ^17.0.0 || ^18.0.0", "@types/react@^16.9.0 || ^17.0.0 || ^18.0.0", "@types/react@^18.2.25 || ^19", "@types/react@^18.2.66", "@types/react@>=16.8":
version "18.3.3"
resolved "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz"
integrity sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==
@ -1828,6 +1869,11 @@
dependencies:
"@swc/core" "^1.5.7"
"@xmldom/xmldom@0.8.10":
version "0.8.10"
resolved "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz"
integrity sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==
"@zeit/schemas@2.36.0":
version "2.36.0"
resolved "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz"
@ -2396,6 +2442,15 @@ clipboardy@3.0.0:
execa "^5.1.1"
is-wsl "^2.2.0"
cliui@^8.0.1:
version "8.0.1"
resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz"
integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==
dependencies:
string-width "^4.2.0"
strip-ansi "^6.0.1"
wrap-ansi "^7.0.0"
clsx@^2.0.0, clsx@^2.1.1:
version "2.1.1"
resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz"
@ -2611,6 +2666,13 @@ crypto-random-string@^2.0.0:
resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz"
integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
css-box-model@^1.2.1:
version "1.2.1"
resolved "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz"
integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==
dependencies:
tiny-invariant "^1.0.6"
css-line-break@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz"
@ -2774,6 +2836,11 @@ define-data-property@^1.0.1, define-data-property@^1.1.4:
es-errors "^1.3.0"
gopd "^1.0.1"
define-lazy-prop@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz"
integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==
define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1:
version "1.2.1"
resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz"
@ -2834,6 +2901,14 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
docx-templates@^4.13.0:
version "4.13.0"
resolved "https://registry.npmjs.org/docx-templates/-/docx-templates-4.13.0.tgz"
integrity sha512-tTmR3WhROYctuyVReQ+PfCU3zprmC45/VuSVzn8EjovzpRkXYUdXiDatB9M8pasj0V+wuuOyY8bcSHvlQ2GNag==
dependencies:
jszip "^3.10.1"
sax "1.3.0"
dom-helpers@^5.0.1:
version "5.2.1"
resolved "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz"
@ -2847,7 +2922,7 @@ domain-browser@^4.22.0:
resolved "https://registry.npmjs.org/domain-browser/-/domain-browser-4.23.0.tgz"
integrity sha512-ArzcM/II1wCCujdCNyQjXrAFwS4mrLh4C7DZWlaI8mdh7h3BfKdNd3bKXITfl2PT9FtfQqaGvhi1vPRQPimjGA==
dompurify@^2.2.0:
dompurify@^2.5.4:
version "2.5.6"
resolved "https://registry.npmjs.org/dompurify/-/dompurify-2.5.6.tgz"
integrity sha512-zUTaUBO8pY4+iJMPE1B9XlO2tXVYIcEA4SNGtvDELzTSCQO7RzH+j7S180BmhmJId78lqGU2z19vgVx2Sxs/PQ==
@ -2862,6 +2937,16 @@ eastasianwidth@^0.2.0:
resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz"
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
easy-template-x@^5.1.0:
version "5.1.0"
resolved "https://registry.npmjs.org/easy-template-x/-/easy-template-x-5.1.0.tgz"
integrity sha512-vypMbIMLWLXoooA9rsL3SVN2oQtZwmmx1m4H8gi6JfbEXQQ5VLHGOUHYi9APbvN9R8Gx93r1fphdSFRHxozeYw==
dependencies:
"@xmldom/xmldom" "0.8.10"
json5 "2.2.3"
jszip "3.10.1"
lodash.get "4.4.2"
ejs@^3.1.6:
version "3.1.10"
resolved "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz"
@ -3038,7 +3123,7 @@ esbuild@^0.21.3:
"@esbuild/win32-ia32" "0.21.5"
"@esbuild/win32-x64" "0.21.5"
escalade@^3.1.2:
escalade@^3.1.1, escalade@^3.1.2:
version "3.1.2"
resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz"
integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==
@ -3241,10 +3326,10 @@ fastq@^1.6.0:
dependencies:
reusify "^1.0.4"
fflate@^0.4.8:
version "0.4.8"
resolved "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz"
integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==
fflate@^0.8.1:
version "0.8.2"
resolved "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz"
integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==
file-entry-cache@^6.0.1:
version "6.0.1"
@ -3386,6 +3471,11 @@ geotiff@^2.0.7:
xml-utils "^1.0.2"
zstddec "^0.1.0"
get-caller-file@^2.0.5:
version "2.0.5"
resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4:
version "1.2.4"
resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz"
@ -3590,7 +3680,7 @@ hmac-drbg@^1.0.1:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
html2canvas@^1.0.0-rc.5:
html2canvas@^1.0.0-rc.5, html2canvas@^1.4.1:
version "1.4.1"
resolved "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz"
integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==
@ -3623,6 +3713,11 @@ ignore@^5.2.0, ignore@^5.3.1:
resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz"
integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==
immediate@~3.0.5:
version "3.0.6"
resolved "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz"
integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
immutable@^4.0.0:
version "4.3.7"
resolved "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz"
@ -3751,7 +3846,7 @@ is-date-object@^1.0.1:
dependencies:
has-tostringtag "^1.0.0"
is-docker@^2.0.0:
is-docker@^2.0.0, is-docker@^2.1.1:
version "2.2.1"
resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz"
integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
@ -3976,7 +4071,7 @@ json-stable-stringify-without-jsonify@^1.0.1:
resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz"
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
json5@^2.2.0, json5@^2.2.3:
json5@^2.2.0, json5@^2.2.3, json5@2.2.3:
version "2.2.3"
resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz"
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
@ -3995,21 +4090,31 @@ jsonpointer@^5.0.0:
resolved "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz"
integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==
jspdf@^2.5.1:
version "2.5.1"
resolved "https://registry.npmjs.org/jspdf/-/jspdf-2.5.1.tgz"
integrity sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA==
jspdf@^2.5.1, jspdf@^2.5.2:
version "2.5.2"
resolved "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz"
integrity sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==
dependencies:
"@babel/runtime" "^7.14.0"
"@babel/runtime" "^7.23.2"
atob "^2.1.2"
btoa "^1.2.1"
fflate "^0.4.8"
fflate "^0.8.1"
optionalDependencies:
canvg "^3.0.6"
core-js "^3.6.0"
dompurify "^2.2.0"
dompurify "^2.5.4"
html2canvas "^1.0.0-rc.5"
jszip@^3.10.1, jszip@3.10.1:
version "3.10.1"
resolved "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz"
integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==
dependencies:
lie "~3.3.0"
pako "~1.0.2"
readable-stream "~2.3.6"
setimmediate "^1.0.5"
keyv@^4.5.3:
version "4.5.4"
resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz"
@ -4040,6 +4145,13 @@ levn@^0.4.1:
prelude-ls "^1.2.1"
type-check "~0.4.0"
lie@~3.3.0:
version "3.3.0"
resolved "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz"
integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==
dependencies:
immediate "~3.0.5"
lilconfig@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz"
@ -4079,6 +4191,11 @@ lodash.debounce@^4.0.8:
resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz"
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
lodash.get@4.4.2:
version "4.4.2"
resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz"
integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==
lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz"
@ -4153,6 +4270,11 @@ mdurl@^2.0.0:
resolved "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz"
integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==
memoize-one@^6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz"
integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==
merge-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz"
@ -4438,6 +4560,15 @@ onetime@^5.1.2:
dependencies:
mimic-fn "^2.1.0"
open@^8.4.0:
version "8.4.2"
resolved "https://registry.npmjs.org/open/-/open-8.4.2.tgz"
integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==
dependencies:
define-lazy-prop "^2.0.0"
is-docker "^2.1.1"
is-wsl "^2.2.0"
optionator@^0.9.3:
version "0.9.4"
resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz"
@ -4479,7 +4610,7 @@ pako@^2.0.4:
resolved "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz"
integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==
pako@~1.0.5:
pako@~1.0.2, pako@~1.0.5:
version "1.0.11"
resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz"
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
@ -4956,6 +5087,11 @@ quickselect@^2.0.0:
resolved "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz"
integrity sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==
raf-schd@^4.0.3:
version "4.0.3"
resolved "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz"
integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==
raf@^3.4.1:
version "3.4.1"
resolved "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz"
@ -5000,7 +5136,7 @@ rc@^1.0.1, rc@^1.1.6:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
"react-dom@^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom@^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom@^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom@^17.0.0 || ^18.0.0", react-dom@^18.2.0, react-dom@>=16.6.0, react-dom@>=16.8, react-dom@>=16.8.0, react-dom@>=18.0.0:
"react-dom@^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom@^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom@^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom@^17.0.0 || ^18.0.0", react-dom@^18.0.0, react-dom@^18.2.0, react-dom@>=16.6.0, react-dom@>=16.8, react-dom@>=16.8.0, react-dom@>=18.0.0:
version "18.3.1"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz"
integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==
@ -5035,6 +5171,14 @@ react-number-format@^5.3.1:
resolved "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.2.tgz"
integrity sha512-cg//jVdS49PYDgmcYoBnMMHl4XNTMuV723ZnHD2aXYtWWWqbVF3hjQ8iB+UZEuXapLbeA8P8H+1o6ZB1lcw3vg==
react-redux@^9.1.2:
version "9.2.0"
resolved "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz"
integrity sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==
dependencies:
"@types/use-sync-external-store" "^0.0.6"
use-sync-external-store "^1.4.0"
react-remove-scroll-bar@^2.3.6:
version "2.3.6"
resolved "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz"
@ -5106,7 +5250,7 @@ react-transition-group@^4.4.5, react-transition-group@4.4.5:
loose-envify "^1.4.0"
prop-types "^15.6.2"
"react@^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16.11.0 || ^17.0.0 || ^18.0.0", "react@^16.8.0 || ^17 || ^18 || ^19", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^16.8.0 || ^17.0.1 || ^18.0.0", "react@^17.0.0 || ^18.0.0", react@^18.2.0, react@^18.3.1, "react@>= 16", "react@>= 16.8 || 18.0.0", react@>=16.6.0, react@>=16.8, react@>=16.8.0, react@>=18.0.0:
"react@^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16.11.0 || ^17.0.0 || ^18.0.0", "react@^16.8.0 || ^17 || ^18 || ^19", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.1 || ^18.0.0", "react@^17.0.0 || ^18.0.0", "react@^18.0 || ^19", react@^18.0.0, react@^18.2.0, react@^18.3.1, "react@>= 16", "react@>= 16.8 || 18.0.0", react@>=16.6.0, react@>=16.8, react@>=16.8.0, react@>=18.0.0:
version "18.3.1"
resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz"
integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
@ -5142,6 +5286,19 @@ readable-stream@^3.5.0, readable-stream@^3.6.0:
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
readable-stream@~2.3.6:
version "2.3.8"
resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz"
integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.3"
isarray "~1.0.0"
process-nextick-args "~2.0.0"
safe-buffer "~5.1.1"
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
readable-web-to-node-stream@^3.0.2:
version "3.0.2"
resolved "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz"
@ -5177,6 +5334,11 @@ recharts@^2.10.3, recharts@^2.12.7:
tiny-invariant "^1.3.1"
victory-vendor "^36.6.8"
redux@^5.0.0, redux@^5.0.1:
version "5.0.1"
resolved "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz"
integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==
regenerate-unicode-properties@^10.1.0:
version "10.1.1"
resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz"
@ -5250,6 +5412,11 @@ regjsparser@^0.9.1:
dependencies:
jsesc "~0.5.0"
require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz"
integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
require-from-string@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz"
@ -5301,6 +5468,16 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^3.0.0"
inherits "^2.0.1"
rollup-plugin-visualizer@^5.12.0:
version "5.12.0"
resolved "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.12.0.tgz"
integrity sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==
dependencies:
open "^8.4.0"
picomatch "^2.3.1"
source-map "^0.7.4"
yargs "^17.5.1"
"rollup@^1.20.0 || ^2.0.0", rollup@^1.20.0||^2.0.0, rollup@^2.43.1:
version "2.79.1"
resolved "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz"
@ -5308,7 +5485,7 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
optionalDependencies:
fsevents "~2.3.2"
rollup@^1.20.0||^2.0.0||^3.0.0||^4.0.0, rollup@^2.0.0||^3.0.0||^4.0.0, rollup@^2.78.0||^3.0.0||^4.0.0, rollup@^4.13.0:
rollup@^1.20.0||^2.0.0||^3.0.0||^4.0.0, rollup@^2.0.0||^3.0.0||^4.0.0, rollup@^2.78.0||^3.0.0||^4.0.0, rollup@^4.13.0, "rollup@2.x || 3.x || 4.x":
version "4.18.0"
resolved "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz"
integrity sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==
@ -5367,12 +5544,7 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2,
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
safe-buffer@~5.1.0:
version "5.1.2"
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
safe-buffer@~5.1.1:
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
@ -5430,6 +5602,11 @@ sass-embedded@*, sass-embedded@^1.79.5:
sass-embedded-win32-ia32 "1.79.5"
sass-embedded-win32-x64 "1.79.5"
sax@1.3.0:
version "1.3.0"
resolved "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz"
integrity sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==
scheduler@^0.23.2:
version "0.23.2"
resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz"
@ -5507,7 +5684,7 @@ set-function-name@^2.0.1, set-function-name@^2.0.2:
functions-have-names "^1.2.3"
has-property-descriptors "^1.0.2"
setimmediate@^1.0.4:
setimmediate@^1.0.4, setimmediate@^1.0.5:
version "1.0.5"
resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz"
integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==
@ -5580,6 +5757,11 @@ source-map@^0.6.0:
resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
source-map@^0.7.4:
version "0.7.4"
resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz"
integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==
source-map@^0.8.0-beta.0:
version "0.8.0-beta.0"
resolved "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz"
@ -5638,7 +5820,16 @@ string_decoder@~1.1.1:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0:
string-width@^4.1.0, string-width@^4.2.0:
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -5908,7 +6099,7 @@ timers-browserify@^2.0.4:
dependencies:
setimmediate "^1.0.4"
tiny-invariant@^1.3.1:
tiny-invariant@^1.0.6, tiny-invariant@^1.3.1:
version "1.3.3"
resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz"
integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==
@ -6158,6 +6349,11 @@ use-latest@^1.2.1:
dependencies:
use-isomorphic-layout-effect "^1.1.1"
use-memo-one@^1.1.3:
version "1.1.3"
resolved "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz"
integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==
use-sidecar@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz"
@ -6176,6 +6372,11 @@ use-sync-external-store@^1.2.2:
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz"
integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==
use-sync-external-store@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz"
integrity sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
@ -6506,6 +6707,15 @@ workbox-window@^7.1.0, workbox-window@7.1.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.0.1, wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"
@ -6530,6 +6740,11 @@ xtend@^4.0.2:
resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
y18n@^5.0.5:
version "5.0.8"
resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz"
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
yallist@^3.0.2:
version "3.1.1"
resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz"
@ -6540,6 +6755,24 @@ yaml@^2.3.4:
resolved "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz"
integrity sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==
yargs-parser@^21.1.1:
version "21.1.1"
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz"
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
yargs@^17.5.1:
version "17.7.2"
resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz"
integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
dependencies:
cliui "^8.0.1"
escalade "^3.1.1"
get-caller-file "^2.0.5"
require-directory "^2.1.1"
string-width "^4.2.3"
y18n "^5.0.5"
yargs-parser "^21.1.1"
yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"

View File

@ -1,26 +1,26 @@
services:
client_app:
container_name: client_app
web_client:
container_name: web_client
build:
context: ./client
dockerfile: Dockerfile
ports:
- 5173:5173
- ${CLIENT_PORT}:5173
restart: always
redis_db:
image: "redis:alpine"
container_name: redis_db
ports:
- ${REDIS_PORT}:${REDIS_PORT}
environment:
- REDIS_PASSWORD=${REDIS_PASSWORD}
command: [ "redis-server", "--requirepass", "${REDIS_PASSWORD}" ]
volumes:
- ./redis_data:/data
expose:
- ${REDIS_PORT}:${REDIS_PORT}
restart: unless-stopped
# redis_db:
# image: "redis:alpine"
# container_name: redis_db
# ports:
# - ${REDIS_PORT}:${REDIS_PORT}
# environment:
# - REDIS_PASSWORD=${REDIS_PASSWORD}
# command: [ "redis-server", "--requirepass", "${REDIS_PASSWORD}" ]
# volumes:
# - ./redis_data:/data
# expose:
# - ${REDIS_PORT}:${REDIS_PORT}
# restart: unless-stopped
ems:
container_name: ems
@ -29,12 +29,12 @@ services:
dockerfile: Dockerfile
volumes:
- ./ems/public:/app/public
links:
- redis_db:redis_db
- psql_db:psql_db
depends_on:
- redis_db
- psql_db
# links:
# - redis_db:redis_db
# - psql_db:psql_db
# depends_on:
# - redis_db
# - psql_db
environment:
- REDIS_PASSWORD=${REDIS_PASSWORD}
- REDIS_HOST=${REDIS_HOST}
@ -45,24 +45,33 @@ services:
- ${EMS_PORT}:${EMS_PORT}
restart: always
monitor:
container_name: monitor
build:
context: ./monitor
dockerfile: Dockerfile
environment:
- MONITOR_PORT=${MONITOR_PORT}
ports:
- ${MONITOR_PORT}:${MONITOR_PORT}
postgis_db:
container_name: postgis_db
image: postgis/postgis:17-3.4-alpine
volumes:
- ./monitor/data:/app/data
restart: always
psql_db:
container_name: psql_db
image: postgres:16.4-alpine
volumes:
- ./psql_data:/var/lib/postgresql/data
- ./postgis_db:/var/lib/postgresql/data
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
ports:
- ${POSTGRES_PORT}:${POSTGRES_PORT}
expose:
- ${POSTGRES_PORT}
healthcheck:
test:
['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}']
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
restart: always
ems_db:
container_name: ems_db
image: postgres:16.4-alpine
volumes:
- ./ems_db:/var/lib/postgresql/data
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}

98
ems/package-lock.json generated
View File

@ -19,7 +19,7 @@
"ioredis": "^5.4.1",
"md5": "^2.3.0",
"multer": "^1.4.5-lts.1",
"pg": "^8.13.0",
"pg": "^8.13.1",
"pump": "^3.0.0",
"sharp": "^0.33.5",
"tedious": "^18.6.1"
@ -31,6 +31,7 @@
"@types/md5": "^2.3.5",
"@types/multer": "^1.4.12",
"@types/node": "^22.4.1",
"@types/pg": "^8.11.10",
"@types/pump": "^1.1.3",
"@types/redis": "^4.0.11",
"nodemon": "^3.1.4",
@ -896,6 +897,74 @@
"undici-types": "~6.19.2"
}
},
"node_modules/@types/pg": {
"version": "8.11.10",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.10.tgz",
"integrity": "sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg==",
"dev": true,
"dependencies": {
"@types/node": "*",
"pg-protocol": "*",
"pg-types": "^4.0.1"
}
},
"node_modules/@types/pg/node_modules/pg-types": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz",
"integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==",
"dev": true,
"dependencies": {
"pg-int8": "1.0.1",
"pg-numeric": "1.0.2",
"postgres-array": "~3.0.1",
"postgres-bytea": "~3.0.0",
"postgres-date": "~2.1.0",
"postgres-interval": "^3.0.0",
"postgres-range": "^1.1.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@types/pg/node_modules/postgres-array": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz",
"integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/@types/pg/node_modules/postgres-bytea": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz",
"integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==",
"dev": true,
"dependencies": {
"obuf": "~1.1.2"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@types/pg/node_modules/postgres-date": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz",
"integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/@types/pg/node_modules/postgres-interval": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz",
"integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/@types/pump": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@types/pump/-/pump-1.1.3.tgz",
@ -2454,6 +2523,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/obuf": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
"dev": true
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@ -2503,9 +2578,9 @@
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
},
"node_modules/pg": {
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.13.0.tgz",
"integrity": "sha512-34wkUTh3SxTClfoHB3pQ7bIMvw9dpFU1audQQeZG837fmHfHpr14n/AELVDoOYVDW2h5RDWU78tFjkD+erSBsw==",
"version": "8.13.1",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz",
"integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==",
"dependencies": {
"pg-connection-string": "^2.7.0",
"pg-pool": "^3.7.0",
@ -2547,6 +2622,15 @@
"node": ">=4.0.0"
}
},
"node_modules/pg-numeric": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz",
"integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/pg-pool": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz",
@ -2630,6 +2714,12 @@
"node": ">=0.10.0"
}
},
"node_modules/postgres-range": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz",
"integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==",
"dev": true
},
"node_modules/prisma": {
"version": "5.19.1",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.19.1.tgz",

View File

@ -23,7 +23,7 @@
"ioredis": "^5.4.1",
"md5": "^2.3.0",
"multer": "^1.4.5-lts.1",
"pg": "^8.13.0",
"pg": "^8.13.1",
"pump": "^3.0.0",
"sharp": "^0.33.5",
"tedious": "^18.6.1"
@ -35,6 +35,7 @@
"@types/md5": "^2.3.5",
"@types/multer": "^1.4.12",
"@types/node": "^22.4.1",
"@types/pg": "^8.11.10",
"@types/pump": "^1.1.3",
"@types/redis": "^4.0.11",
"nodemon": "^3.1.4",

143
ems/src/api/db/index.ts Normal file
View File

@ -0,0 +1,143 @@
import express, { Request, Response } from 'express';
import { pgQuery } from '../../utils/postgres';
const router = express.Router()
router.get('/rows/:table_name', async (req: Request, res: Response) => {
try {
const { table_name } = req.params
const { offset, limit } = req.query
const result = await pgQuery(
`
SELECT * FROM ${table_name}
OFFSET ${offset || 0} LIMIT ${limit || 10}
`
)
if (Array.isArray(result)) {
if (result.length > 0) {
res.status(200).json(result)
} else {
res.status(404).json('not found')
}
} else {
res.status(404).json('not found')
}
} catch (err) {
res.status(500)
}
})
router.get('/columns/:table_name', async (req: Request, res: Response) => {
try {
const { table_name } = req.params
const result = await pgQuery(
`
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = '${table_name}'
`
)
if (Array.isArray(result)) {
if (result.length > 0) {
res.status(200).json(result)
} else {
res.status(404).json('not found')
}
} else {
res.status(404).json('not found')
}
} catch (err) {
res.status(500)
}
})
router.get('/tables', async (req: Request, res: Response) => {
try {
const result = await pgQuery(
`
SELECT tablename FROM pg_tables WHERE schemaname = 'public';
`
)
if (Array.isArray(result)) {
if (result.length > 0) {
res.status(200).json(result)
} else {
res.status(404).json('not found')
}
} else {
res.status(404).json('not found')
}
} catch (err) {
res.status(500)
}
})
router.get('/figures/import', async (req: Request, res: Response) => {
try {
const { entity_type } = req.params
const result = await pgQuery(
`
SELECT * FROM bounds
WHERE entity_type = $1
`,
[entity_type]
)
if (Array.isArray(result)) {
if (result.length > 0) {
const geometries = result.map((bound: { id: number, entity_id: number, entity_type: string, geometry: JSON, published_at: string, deleted_at: string | null }) => {
return {
...bound.geometry,
properties: {
id: bound.id,
entity_id: bound.entity_id,
entity_type: bound.entity_type
}
}
})
res.status(200).json(geometries)
} else {
res.status(404).json('not found')
}
} else {
res.status(404).json('not found')
}
} catch (err) {
res.status(500)
}
})
router.get('/bounds/:entity_type/:entity_id', async (req: Request, res: Response) => {
try {
const { entity_type, entity_id } = req.params
const result = await pgQuery(
`
SELECT * FROM bounds
WHERE entity_type = $1
AND entity_id = $2
`,
[entity_type, entity_id]
)
if (Array.isArray(result)) {
if (result.length > 0) {
res.status(200).json(result[0].geometry)
} else {
res.status(404).json('not found')
}
} else {
res.status(404).json('not found')
}
} catch (err) {
res.status(500)
}
})
export default router

View File

@ -171,27 +171,34 @@ router.get('/objects/list', async (req: Request, res: Response) => {
const result = await tediousQuery(
`
SELECT
tTypes.id AS id,
tTypes.name AS name,
COUNT(vObjects.type) AS count
${GeneralDB}..tTypes.id AS id,
${GeneralDB}..tTypes.name AS name,
COUNT(vo.type) AS count,
tr.r,
tr.g,
tr.b
FROM
vObjects
${GeneralDB}..vObjects vo
JOIN
tTypes ON vObjects.type = tTypes.id
${GeneralDB}..tTypes ON vo.type = ${GeneralDB}..tTypes.id
LEFT JOIN ${GisDB}..TypeRoles tr ON tr.id = ${GeneralDB}..tTypes.id
WHERE
vObjects.id_city = ${city_id} AND vObjects.year = ${year}
vo.id_city = ${city_id} AND vo.year = ${year}
AND
(
CASE
WHEN TRY_CAST(vObjects.planning AS BIT) IS NOT NULL THEN TRY_CAST(vObjects.planning AS BIT)
WHEN vObjects.planning = 'TRUE' THEN 1
WHEN vObjects.planning = 'FALSE' THEN 0
WHEN TRY_CAST(vo.planning AS BIT) IS NOT NULL THEN TRY_CAST(vo.planning AS BIT)
WHEN vo.planning = 'TRUE' THEN 1
WHEN vo.planning = 'FALSE' THEN 0
ELSE NULL
END
) = ${planning}
GROUP BY
tTypes.id,
tTypes.name;
${GeneralDB}..tTypes.id,
${GeneralDB}..tTypes.name,
tr.r,
tr.g,
tr.b;
`
)
res.status(200).json(result)

View File

@ -1,8 +1,86 @@
import express, { Request, Response } from 'express';
import { tediousQuery } from '../../utils/tedious';
import { GeneralDB, GisDB } from '../../constants/db';
import { pgQuery } from '../../utils/postgres';
const router = express.Router()
router.get('/type-roles', async (req: Request, res: Response) => {
try {
const result = await tediousQuery(
`
SELECT * FROM ${GisDB}..TypeRoles;
`
)
res.status(200).json(result)
} catch (err) {
res.status(500)
}
})
router.get('/bounds/:entity_type', async (req: Request, res: Response) => {
try {
const { entity_type } = req.params
const result = await pgQuery(
`
SELECT * FROM bounds
WHERE entity_type = $1
`,
[entity_type]
)
if (Array.isArray(result)) {
if (result.length > 0) {
const geometries = result.map((bound: { id: number, entity_id: number, entity_type: string, geometry: JSON, published_at: string, deleted_at: string | null }) => {
return {
...bound.geometry,
properties: {
id: bound.id,
entity_id: bound.entity_id,
entity_type: bound.entity_type
}
}
})
res.status(200).json(geometries)
} else {
res.status(404).json('not found')
}
} else {
res.status(404).json('not found')
}
} catch (err) {
res.status(500)
}
})
router.get('/bounds/:entity_type/:entity_id', async (req: Request, res: Response) => {
try {
const { entity_type, entity_id } = req.params
const result = await pgQuery(
`
SELECT * FROM bounds
WHERE entity_type = $1
AND entity_id = $2
`,
[entity_type, entity_id]
)
if (Array.isArray(result)) {
if (result.length > 0) {
res.status(200).json(result[0].geometry)
} else {
res.status(404).json('not found')
}
} else {
res.status(404).json('not found')
}
} catch (err) {
res.status(500)
}
})
router.get('/images/all', async (req: Request, res: Response) => {
try {
const { offset, limit, city_id } = req.query

View File

@ -3,70 +3,70 @@ import { query, validationResult } from 'express-validator';
import { PrismaClient } from '@prisma/client';
const router = express.Router()
const prisma = new PrismaClient()
//const prisma = new PrismaClient()
router.get('/all', async (req: Request, res: Response) => {
try {
const nodes = await prisma.nodes.findMany()
// router.get('/all', async (req: Request, res: Response) => {
// try {
// const nodes = await prisma.nodes.findMany()
res.json(nodes)
} catch (error) {
console.error('Error getting node:', error);
res.status(500).json({ error: 'Failed to get node' });
}
})
// res.json(nodes)
// } catch (error) {
// console.error('Error getting node:', error);
// res.status(500).json({ error: 'Failed to get node' });
// }
// })
router.get('/', query('id').isString().isUUID(), async (req: Request, res: Response) => {
try {
const result = validationResult(req)
if (!result.isEmpty()) {
return res.send({ errors: result.array() })
}
// router.get('/', query('id').isString().isUUID(), async (req: Request, res: Response) => {
// try {
// const result = validationResult(req)
// if (!result.isEmpty()) {
// return res.send({ errors: result.array() })
// }
const { id } = req.params
// const { id } = req.params
const node = await prisma.nodes.findFirst({
where: {
id: id
}
})
// const node = await prisma.nodes.findFirst({
// where: {
// id: id
// }
// })
res.json(node)
// res.json(node)
} catch (error) {
console.error('Error getting node:', error);
res.status(500).json({ error: 'Failed to get node' });
}
})
// } catch (error) {
// console.error('Error getting node:', error);
// res.status(500).json({ error: 'Failed to get node' });
// }
// })
router.post('/', async (req: Request, res: Response) => {
try {
const { coordinates, object_id, type } = req.body;
// router.post('/', async (req: Request, res: Response) => {
// try {
// const { coordinates, object_id, type } = req.body;
// Convert the incoming array of coordinates into the shape structure
const shape = coordinates.map((point: number[]) => ({
object_id: object_id || null,
x: point[0],
y: point[1]
}));
// // Convert the incoming array of coordinates into the shape structure
// const shape = coordinates.map((point: number[]) => ({
// object_id: object_id || null,
// x: point[0],
// y: point[1]
// }));
console.log(shape)
// console.log(shape)
// Create a new node in the database
const node = await prisma.nodes.create({
data: {
object_id: object_id || null, // Nullable if object_id is not provided
shape_type: type, // You can adjust this dynamically
shape: shape, // Store the shape array as Json[]
label: 'Default'
}
});
// // Create a new node in the database
// const node = await prisma.nodes.create({
// data: {
// object_id: object_id || null, // Nullable if object_id is not provided
// shape_type: type, // You can adjust this dynamically
// shape: shape, // Store the shape array as Json[]
// label: 'Default'
// }
// });
res.status(201).json(node);
} catch (error) {
console.error('Error creating node:', error);
res.status(500).json({ error: 'Failed to create node' });
}
})
// res.status(201).json(node);
// } catch (error) {
// console.error('Error creating node:', error);
// res.status(500).json({ error: 'Failed to create node' });
// }
// })
export default router

View File

@ -10,7 +10,7 @@ const router = express.Router()
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, path.join(__dirname, '..', 'public', 'temp'))
cb(null, path.join(__dirname, '..', '..', '..', 'public', 'temp'))
},
filename: function (req, file, cb) {
cb(null, Date.now() + path.extname(file.originalname))

View File

@ -1,6 +1,7 @@
import express from 'express'
import bodyParser from 'body-parser'
import cors from 'cors'
import dbRouter from './api/db'
import generalRouter from './api/general'
import gisRouter from './api/gis'
import nodesRouter from './api/nodes'
@ -17,6 +18,7 @@ app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use('/db', dbRouter)
app.use('/general', generalRouter)
app.use('/gis', gisRouter)
app.use('/nodes', nodesRouter)

37
ems/src/utils/postgres.ts Normal file
View File

@ -0,0 +1,37 @@
import { Pool } from 'pg';
import 'dotenv/config';
// Environment variables for database configuration
const PG_HOST = process.env.PG_HOST || 'localhost';
const PG_PORT = Number(process.env.PG_PORT) || 5432;
const PG_USER = process.env.PG_USER || 'postgres';
const PG_PASSWORD = process.env.PG_PASSWORD || '';
const PG_DB = process.env.PG_DB || 'postgres';
// Create a connection pool
const pool = new Pool({
host: PG_HOST,
port: PG_PORT,
user: PG_USER,
password: PG_PASSWORD,
database: PG_DB,
});
export async function pgQuery(query: string, params: any[] = []) {
try {
// Get a client from the pool
const client = await pool.connect();
try {
// Execute the query with parameters
const result = await client.query(query, params);
return result.rows; // Return only the rows
} finally {
// Release the client back to the pool
client.release();
}
} catch (err) {
// Log error and rethrow it
console.error(`Error executing query: ${query}`, err);
throw err;
}
}

View File

@ -0,0 +1,5 @@
DB_NAME=
DB_USER=
DB_PASSWORD=
DB_HOST=
DB_PORT=

5200
tools/import_bounds/city.sql Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -0,0 +1,75 @@
import os
import json
import psycopg2
from datetime import datetime
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# Database connection parameters
DB_CONFIG = {
"dbname": os.getenv("DB_NAME"),
"user": os.getenv("DB_USER"),
"password": os.getenv("DB_PASSWORD"),
"host": os.getenv("DB_HOST"),
"port": int(os.getenv("DB_PORT", 5432)) # Default port to 5432 if not set
}
# Root directory where the folders are located
ROOT_DIR = "./"
def insert_bounds(entity_id, entity_type, geometry, conn):
"""Insert data into the bounds table."""
try:
with conn.cursor() as cur:
# Check if the GeoJSON is a GeometryCollection
if geometry.get('type') == 'GeometryCollection':
# Extract polygons from the GeometryCollection and convert them to MultiPolygon
geometry = {
"type": "MultiPolygon",
"coordinates": [
geom["coordinates"] for geom in geometry["geometries"] if geom["type"] == "Polygon"
]
}
# Insert into the bounds table
cur.execute(
"""
INSERT INTO bounds (entity_id, entity_type, geometry)
VALUES (%s, %s,
ST_Transform(
ST_Multi(
ST_GeomFromGeoJSON(%s::JSON)
),
3857)
);
""",
(entity_id, entity_type, json.dumps(geometry))
)
conn.commit()
except Exception as e:
print(f"Error inserting entity_id {entity_id} of type {entity_type}: {e}")
conn.rollback()
def process_geojson_files():
"""Process all GeoJSON files in the directory structure."""
try:
conn = psycopg2.connect(**DB_CONFIG)
for folder in os.listdir(ROOT_DIR):
folder_path = os.path.join(ROOT_DIR, folder)
if os.path.isdir(folder_path):
entity_type = folder # Folder name is the entity_type
for file in os.listdir(folder_path):
if file.endswith(".geojson"):
entity_id = int(os.path.splitext(file)[0]) # Extract ID from filename
file_path = os.path.join(folder_path, file)
with open(file_path, "r") as f:
geometry = json.load(f)
insert_bounds(entity_id, entity_type, geometry, conn)
conn.close()
except Exception as e:
print(f"Error processing files: {e}")
if __name__ == "__main__":
process_geojson_files()

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,55 @@
import os
import json
# Root directory where the folders are located
ROOT_DIR = "./"
OUTPUT_FILES = {}
def get_insert_query(entity_id, entity_type, geometry):
"""Generate SQL insert query for bounds."""
# If it's a GeometryCollection, extract and convert to MultiPolygon
if geometry.get('type') == 'GeometryCollection':
geometry = {
"type": "MultiPolygon",
"coordinates": [
geom["coordinates"] for geom in geometry["geometries"] if geom["type"] == "Polygon"
]
}
geojson_str = json.dumps(geometry).replace("'", "''") # Escape single quotes for SQL
query = f"""
INSERT INTO bounds (entity_id, entity_type, geometry)
VALUES ({entity_id}, '{entity_type}',
ST_Transform(
ST_Multi(
ST_GeomFromGeoJSON('{geojson_str}'::JSON)
),
3857)
);
"""
return query
def process_geojson_files():
"""Process all GeoJSON files and write SQL inserts to appropriate files."""
for folder in os.listdir(ROOT_DIR):
folder_path = os.path.join(ROOT_DIR, folder)
if os.path.isdir(folder_path):
entity_type = folder
sql_filename = f"{entity_type}.sql"
with open(sql_filename, "w", encoding="utf-8") as sql_file:
for file in os.listdir(folder_path):
if file.endswith(".geojson"):
try:
entity_id = int(os.path.splitext(file)[0])
file_path = os.path.join(folder_path, file)
with open(file_path, "r", encoding="utf-8") as f:
geometry = json.load(f)
query = get_insert_query(entity_id, entity_type, geometry)
sql_file.write(query + "\n")
except Exception as e:
print(f"Error processing {file}: {e}")
if __name__ == "__main__":
process_geojson_files()