diff --git a/client/src/App.tsx b/client/src/App.tsx index 3236f61..4960334 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -31,6 +31,7 @@ export const pages = [ component: , drawer: false, dashboard: false, + enabled: true, }, { label: "", @@ -39,6 +40,7 @@ export const pages = [ component: , drawer: false, dashboard: false, + enabled: false, }, { label: "", @@ -47,6 +49,7 @@ export const pages = [ component: , drawer: false, dashboard: false, + enabled: true, }, { label: "Настройки", @@ -55,6 +58,7 @@ export const pages = [ component: , drawer: false, dashboard: true, + enabled: true, }, { label: "Главная", @@ -62,7 +66,8 @@ export const pages = [ icon: , component:
, drawer: true, - dashboard: true + dashboard: true, + enabled: true, }, { label: "Пользователи", @@ -70,7 +75,8 @@ export const pages = [ icon: , component: , drawer: true, - dashboard: true + dashboard: true, + enabled: true, }, { label: "Роли", @@ -78,7 +84,8 @@ export const pages = [ icon: , component: , drawer: true, - dashboard: true + dashboard: true, + enabled: true, }, { label: "Документы", @@ -86,7 +93,8 @@ export const pages = [ icon: , component: , drawer: true, - dashboard: true + dashboard: true, + enabled: true, }, { label: "Отчеты", @@ -94,7 +102,8 @@ export const pages = [ icon: , component: , drawer: true, - dashboard: true + dashboard: true, + enabled: true, }, { label: "Серверы", @@ -102,7 +111,8 @@ export const pages = [ icon: , component: , drawer: true, - dashboard: true + dashboard: true, + enabled: true, }, { label: "Котельные", @@ -110,7 +120,8 @@ export const pages = [ icon: , component: , drawer: true, - dashboard: true + dashboard: true, + enabled: true, }, { label: "API Test", @@ -118,15 +129,17 @@ export const pages = [ icon: , component: , drawer: true, - dashboard: true + dashboard: true, + enabled: false, }, { - label: "Карта", + label: "ИКС", path: "/map-test", icon: , component: , drawer: true, - dashboard: true + dashboard: true, + enabled: false, }, { label: "Chunk test", @@ -134,7 +147,8 @@ export const pages = [ icon: , component: , drawer: true, - dashboard: true + dashboard: true, + enabled: false, }, { label: "Монитор", @@ -142,7 +156,8 @@ export const pages = [ icon: , component: , drawer: true, - dashboard: true + dashboard: true, + enabled: false, }, ] @@ -174,13 +189,13 @@ function App() { }> - {pages.filter((page) => !page.dashboard).map((page, index) => ( + {pages.filter((page) => !page.dashboard).filter((page) => page.enabled).map((page, index) => ( ))} : }> - {pages.filter((page) => page.dashboard).map((page, index) => ( + {pages.filter((page) => page.dashboard).filter((page) => page.enabled).map((page, index) => ( ))} } /> diff --git a/client/src/components/FolderViewer.tsx b/client/src/components/FolderViewer.tsx index cff206b..45853a7 100644 --- a/client/src/components/FolderViewer.tsx +++ b/client/src/components/FolderViewer.tsx @@ -184,7 +184,8 @@ export default function FolderViewer() { { + const { cities } = useCities(100, 1) + + useEffect(() => { + if (cities) { + cities.map((city: any) => { + citiesLayer.current?.getSource()?.addFeature(new Feature(new Point(fromLonLat([city.longitude, city.width])))) + }) + } + }, [cities]) + const [currentCoordinate, setCurrentCoordinate] = useState(null) const [currentZ, setCurrentZ] = useState(undefined) const [currentX, setCurrentX] = useState(undefined) const [currentY, setCurrentY] = useState(undefined) - const [testExtent, setTestExtent] = useState(null) - const [file, setFile] = useState(null) const [polygonExtent, setPolygonExtent] = useState(undefined) const [bottomLeft, setBottomLeft] = useState(undefined) @@ -72,12 +83,19 @@ const MapComponent = () => { }, })) + const nodeLayer = useRef(null) + const nodeLayerSource = useRef(new VectorSource()) + const overlayLayer = useRef(null) const overlayLayerSource = useRef(new VectorSource()) const drawingLayer = useRef(null) const drawingLayerSource = useRef(new VectorSource()) + const citiesLayer = useRef(new VectorLayer({ + source: new VectorSource() + })) + const regionsLayer = useRef(new VectorImageLayer({ source: regionsLayerSource, style: regionsLayerStyle @@ -98,6 +116,21 @@ const MapComponent = () => { type: currentTool, condition: noModifierKeys }) + + draw.current.on('drawend', function (s) { + console.log(s.feature.getGeometry()?.getType()) + let type = 'POLYGON' + + switch (s.feature.getGeometry()?.getType()) { + case 'LineString': + type = 'LINE' + case 'Polygon': + type = 'POLYGON' + } + const coordinates = (s.feature.getGeometry() as SimpleGeometry).getCoordinates() + uploadCoordinates(coordinates, type) + }) + map?.current?.addInteraction(draw.current) snap.current = new Snap({ source: drawingLayerSource.current }) map?.current?.addInteraction(snap.current) @@ -595,14 +628,18 @@ const MapComponent = () => { }, }) + nodeLayer.current = new VectorLayer({ + source: nodeLayerSource.current, + style: drawingLayerStyle + }) + map.current = new Map({ - layers: [baseLayer.current, new TileLayer({ - source: new TileDebug(), - }), satLayer.current, regionsLayer.current, drawingLayer.current, imageLayer.current, overlayLayer.current], + controls: [], + layers: [baseLayer.current, satLayer.current, regionsLayer.current, citiesLayer.current, drawingLayer.current, imageLayer.current, overlayLayer.current, nodeLayer.current], target: mapElement.current as HTMLDivElement, view: new View({ - center: mapCenter, - zoom: 2, + center: mapCenter,//center: fromLonLat([130.401113, 67.797368]), + zoom: 16, maxZoom: 21, //extent: mapExtent, }), @@ -665,6 +702,27 @@ const MapComponent = () => { } }, [currentTool]) + const uploadCoordinates = async (coordinates: any, type: any) => { + try { + const response = await fetch(`${import.meta.env.VITE_API_EMS_URL}/nodes`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ coordinates, object_id: 1, type: type }) // Replace with actual object_id + }); + + if (response.ok) { + const data = await response.json(); + console.log('Node created:', data); + } else { + console.error('Failed to upload coordinates'); + } + } catch (error) { + console.error('Error:', error); + } + }; + const [satelliteOpacity, setSatelliteOpacity] = useState(0) const [statusText, setStatusText] = useState('') @@ -712,52 +770,48 @@ const MapComponent = () => { } } + const mapControlsStyle: SxProps = { + borderRadius: '4px', + position: 'absolute', + zIndex: '1', + backgroundColor: (theme) => + theme.palette.mode === 'light' + ? '#FFFFFFAA' + : '#000000AA', + backdropFilter: 'blur(8px)' + } + + const { data: nodes } = useSWR('/nodes/all', () => fetcher('/nodes/all', BASE_URL.ems), { revalidateOnFocus: false }) + + useEffect(() => { + // Draw features based on database data + if (Array.isArray(nodes)) { + nodes.map(node => { + if (node.shape_type === 'LINE') { + let coordinates: Coordinate[] = [] + if (Array.isArray(node.shape)) { + node.shape.map((point: any) => { + const coordinate = [point.x as number, point.y as number] as Coordinate + coordinates.push(coordinate) + }) + } + //console.log(coordinates) + nodeLayerSource.current.addFeature(new Feature({ geometry: new LineString(coordinates) })) + } + }) + } + }, [nodes]) + return ( - - }> - - - - - - - x: {currentCoordinate?.[0]} - - - y: {currentCoordinate?.[1]} - - - - - Z={currentZ} - X={currentX} - Y={currentY} - - - submitOverlay()}> - - - - }> - - - setSatelliteOpacity(Array.isArray(value) ? value[0] : value)} /> - - - setSatMapsProvider(e.target.value as SatelliteMapsProvider)} - > - Google - Яндекс - Custom - - - + + }> { fetch(`${import.meta.env.VITE_API_EMS_URL}/hello`, { method: 'GET' }).then(res => console.log(res)) }}> @@ -812,14 +866,113 @@ const MapComponent = () => { - -
-
+ } + > + + submitOverlay()}> + + + + + + + + + + + setSatelliteOpacity(Array.isArray(value) ? value[0] : value)} /> + + setSatMapsProvider(e.target.value as SatelliteMapsProvider)} + > + Google + Яндекс + Custom + + + + + } + aria-controls="panel1-content" + id="panel1-header" + > + Объекты + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse + malesuada lacus ex, sit amet blandit leo lobortis eget. + + + - - {statusText} - + + } + > + + + x: {currentCoordinate?.[0]} + + + y: {currentCoordinate?.[1]} + + + + + Z={currentZ} + X={currentX} + Y={currentY} + + + + }> + + {statusText} + + + +
+
+
); }; diff --git a/client/src/components/map/MapStyles.ts b/client/src/components/map/MapStyles.ts index cecaf7a..1d5fe00 100644 --- a/client/src/components/map/MapStyles.ts +++ b/client/src/components/map/MapStyles.ts @@ -5,7 +5,8 @@ import Style from "ol/style/Style"; const drawingLayerStyle: FlatStyleLike = { 'fill-color': 'rgba(255, 255, 255, 0.2)', - 'stroke-color': '#ffcc33', + //'stroke-color': '#ffcc33', + 'stroke-color': '#000000', 'stroke-width': 2, 'circle-radius': 7, 'circle-fill-color': '#ffcc33', diff --git a/client/src/constants/index.ts b/client/src/constants/index.ts index c3b16d2..f3fee9b 100644 --- a/client/src/constants/index.ts +++ b/client/src/constants/index.ts @@ -7,5 +7,6 @@ export const BASE_URL = { auth: import.meta.env.VITE_API_AUTH_URL, info: import.meta.env.VITE_API_INFO_URL, fuel: import.meta.env.VITE_API_FUEL_URL, - servers: import.meta.env.VITE_API_SERVERS_URL + servers: import.meta.env.VITE_API_SERVERS_URL, + ems: import.meta.env.VITE_API_EMS_URL, } \ No newline at end of file diff --git a/client/src/layouts/DashboardLayout.tsx b/client/src/layouts/DashboardLayout.tsx index 896b5fe..993abb3 100644 --- a/client/src/layouts/DashboardLayout.tsx +++ b/client/src/layouts/DashboardLayout.tsx @@ -166,7 +166,7 @@ export default function DashboardLayout() { - {pages.filter((page) => page.drawer).map((item, index) => ( + {pages.filter((page) => page.drawer).filter((page) => page.enabled).map((item, index) => ( - - - +
diff --git a/client/src/main.tsx b/client/src/main.tsx index bc6a0b2..319487d 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -16,6 +16,13 @@ const mainTheme = createTheme( ].join(',') }, components: { + MuiAppBar: { + // styleOverrides: { + // colorPrimary: { + // backgroundColor: 'gray' + // } + // } + }, MuiListItemButton: { defaultProps: { //disableRipple: true @@ -38,6 +45,7 @@ const mainTheme = createTheme( }, MuiIconButton: { defaultProps: { + } }, MuiIcon: { diff --git a/client/src/pages/Documents.tsx b/client/src/pages/Documents.tsx index e81c244..2d0d37f 100644 --- a/client/src/pages/Documents.tsx +++ b/client/src/pages/Documents.tsx @@ -2,8 +2,6 @@ import FolderViewer from '../components/FolderViewer' export default function Documents() { return ( -
- -
+ ) } \ No newline at end of file diff --git a/client/src/pages/Main.tsx b/client/src/pages/Main.tsx index 4dfd633..1cd128f 100644 --- a/client/src/pages/Main.tsx +++ b/client/src/pages/Main.tsx @@ -2,13 +2,13 @@ import { Box, Card, Typography } from "@mui/material"; export default function Main() { return ( - + Последние файлы - + ) diff --git a/client/src/pages/MapTest.tsx b/client/src/pages/MapTest.tsx index c48a8ce..7207001 100644 --- a/client/src/pages/MapTest.tsx +++ b/client/src/pages/MapTest.tsx @@ -2,9 +2,7 @@ import MapComponent from '../components/map/MapComponent' function MapTest() { return ( -
- -
+ ) } diff --git a/client/src/pages/Reports.tsx b/client/src/pages/Reports.tsx index aeb5003..1ca710c 100644 --- a/client/src/pages/Reports.tsx +++ b/client/src/pages/Reports.tsx @@ -41,86 +41,84 @@ export default function Reports() { } return ( - <> - - - setSearch(value)} - onChange={(_, value) => setSelectedOption(value)} - isOptionEqualToValue={(option: ICity, value: ICity) => option.id === value.id} - getOptionLabel={(option: ICity) => option.name ? option.name : ""} - options={cities || []} - loading={isLoading} - value={selectedOption} - renderInput={(params) => ( - - {isLoading ? : null} - {params.InputProps.endAdornment} - - ) - }} - /> - )} - /> - - refreshReport()}> - - - - - - - Object.keys(report[key])))].map(id => { - const row: any = { id: Number(id) }; - Object.keys(report).forEach(key => { - row[key] = report[key][id]; - }); - return row; - }) - : - [] - } - columns={[ - { field: 'id', headerName: '№', width: 70 }, - ...Object.keys(report).map(key => ({ - field: key, - headerName: key.charAt(0).toUpperCase() + key.slice(1), - width: 150 - })) - ]} - initialState={{ - pagination: { - paginationModel: { page: 0, pageSize: 10 }, - }, - }} - pageSizeOptions={[10, 20, 50, 100]} - checkboxSelection={false} - disableRowSelectionOnClick - - processRowUpdate={(updatedRow) => { - return updatedRow - }} - - onProcessRowUpdateError={() => { - }} + + + setSearch(value)} + onChange={(_, value) => setSelectedOption(value)} + isOptionEqualToValue={(option: ICity, value: ICity) => option.id === value.id} + getOptionLabel={(option: ICity) => option.name ? option.name : ""} + options={cities || []} + loading={isLoading} + value={selectedOption} + renderInput={(params) => ( + + {isLoading ? : null} + {params.InputProps.endAdornment} + + ) + }} + /> + )} /> + + refreshReport()}> + + + + - + + Object.keys(report[key])))].map(id => { + const row: any = { id: Number(id) }; + Object.keys(report).forEach(key => { + row[key] = report[key][id]; + }); + return row; + }) + : + [] + } + columns={[ + { field: 'id', headerName: '№', width: 70 }, + ...Object.keys(report).map(key => ({ + field: key, + headerName: key.charAt(0).toUpperCase() + key.slice(1), + width: 150 + })) + ]} + initialState={{ + pagination: { + paginationModel: { page: 0, pageSize: 10 }, + }, + }} + pageSizeOptions={[10, 20, 50, 100]} + checkboxSelection={false} + disableRowSelectionOnClick + + processRowUpdate={(updatedRow) => { + return updatedRow + }} + + onProcessRowUpdateError={() => { + }} + /> + ) } \ No newline at end of file diff --git a/client/src/pages/Roles.tsx b/client/src/pages/Roles.tsx index 6e3396f..1afacab 100644 --- a/client/src/pages/Roles.tsx +++ b/client/src/pages/Roles.tsx @@ -31,7 +31,8 @@ export default function Roles() { flexDirection: 'column', alignItems: 'flex-start', gap: '16px', - flexGrow: 1 + flexGrow: 1, + p: '16px' }}> - + */} - -
diff --git a/ems/package-lock.json b/ems/package-lock.json index 4c30e96..6789aa3 100644 --- a/ems/package-lock.json +++ b/ems/package-lock.json @@ -9,16 +9,17 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@prisma/client": "^5.18.0", + "@prisma/client": "^5.19.1", "axios": "^1.7.4", "body-parser": "^1.20.2", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", + "express-validator": "^7.2.0", "ioredis": "^5.4.1", "md5": "^2.3.0", "multer": "^1.4.5-lts.1", - "prisma": "^5.18.0", + "pg": "^8.13.0", "pump": "^3.0.0", "sharp": "^0.33.5" }, @@ -32,6 +33,7 @@ "@types/pump": "^1.1.3", "@types/redis": "^4.0.11", "nodemon": "^3.1.4", + "prisma": "^5.19.1", "ts-node": "^10.9.2", "typescript": "^5.5.4" } @@ -430,9 +432,9 @@ } }, "node_modules/@prisma/client": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.18.0.tgz", - "integrity": "sha512-BWivkLh+af1kqC89zCJYkHsRcyWsM8/JHpsDMM76DjP3ZdEquJhXa4IeX+HkWPnwJ5FanxEJFZZDTWiDs/Kvyw==", + "version": "5.19.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.19.1.tgz", + "integrity": "sha512-x30GFguInsgt+4z5I4WbkZP2CGpotJMUXy+Gl/aaUjHn2o1DnLYNTA+q9XdYmAQZM8fIIkvUiA2NpgosM3fneg==", "hasInstallScript": true, "engines": { "node": ">=16.13" @@ -447,43 +449,48 @@ } }, "node_modules/@prisma/debug": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.18.0.tgz", - "integrity": "sha512-f+ZvpTLidSo3LMJxQPVgAxdAjzv5OpzAo/eF8qZqbwvgi2F5cTOI9XCpdRzJYA0iGfajjwjOKKrVq64vkxEfUw==" + "version": "5.19.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.19.1.tgz", + "integrity": "sha512-lAG6A6QnG2AskAukIEucYJZxxcSqKsMK74ZFVfCTOM/7UiyJQi48v6TQ47d6qKG3LbMslqOvnTX25dj/qvclGg==", + "devOptional": true }, "node_modules/@prisma/engines": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.18.0.tgz", - "integrity": "sha512-ofmpGLeJ2q2P0wa/XaEgTnX/IsLnvSp/gZts0zjgLNdBhfuj2lowOOPmDcfKljLQUXMvAek3lw5T01kHmCG8rg==", + "version": "5.19.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.19.1.tgz", + "integrity": "sha512-kR/PoxZDrfUmbbXqqb8SlBBgCjvGaJYMCOe189PEYzq9rKqitQ2fvT/VJ8PDSe8tTNxhc2KzsCfCAL+Iwm/7Cg==", + "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/debug": "5.18.0", - "@prisma/engines-version": "5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169", - "@prisma/fetch-engine": "5.18.0", - "@prisma/get-platform": "5.18.0" + "@prisma/debug": "5.19.1", + "@prisma/engines-version": "5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3", + "@prisma/fetch-engine": "5.19.1", + "@prisma/get-platform": "5.19.1" } }, "node_modules/@prisma/engines-version": { - "version": "5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169.tgz", - "integrity": "sha512-a/+LpJj8vYU3nmtkg+N3X51ddbt35yYrRe8wqHTJtYQt7l1f8kjIBcCs6sHJvodW/EK5XGvboOiwm47fmNrbgg==" + "version": "5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3.tgz", + "integrity": "sha512-xR6rt+z5LnNqTP5BBc+8+ySgf4WNMimOKXRn6xfNRDSpHvbOEmd7+qAOmzCrddEc4Cp8nFC0txU14dstjH7FXA==", + "devOptional": true }, "node_modules/@prisma/fetch-engine": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.18.0.tgz", - "integrity": "sha512-I/3u0x2n31rGaAuBRx2YK4eB7R/1zCuayo2DGwSpGyrJWsZesrV7QVw7ND0/Suxeo/vLkJ5OwuBqHoCxvTHpOg==", + "version": "5.19.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.19.1.tgz", + "integrity": "sha512-pCq74rtlOVJfn4pLmdJj+eI4P7w2dugOnnTXpRilP/6n5b2aZiA4ulJlE0ddCbTPkfHmOL9BfaRgA8o+1rfdHw==", + "devOptional": true, "dependencies": { - "@prisma/debug": "5.18.0", - "@prisma/engines-version": "5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169", - "@prisma/get-platform": "5.18.0" + "@prisma/debug": "5.19.1", + "@prisma/engines-version": "5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3", + "@prisma/get-platform": "5.19.1" } }, "node_modules/@prisma/get-platform": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.18.0.tgz", - "integrity": "sha512-Tk+m7+uhqcKDgnMnFN0lRiH7Ewea0OEsZZs9pqXa7i3+7svS3FSCqDBCaM9x5fmhhkufiG0BtunJVDka+46DlA==", + "version": "5.19.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.19.1.tgz", + "integrity": "sha512-sCeoJ+7yt0UjnR+AXZL7vXlg5eNxaFOwC23h0KvW1YIXUoa7+W2ZcAUhoEQBmJTW4GrFqCuZ8YSP0mkDa4k3Zg==", + "devOptional": true, "dependencies": { - "@prisma/debug": "5.18.0" + "@prisma/debug": "5.19.1" } }, "node_modules/@redis/bloom": { @@ -1244,6 +1251,18 @@ "node": ">= 0.10.0" } }, + "node_modules/express-validator": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.0.tgz", + "integrity": "sha512-I2ByKD8panjtr8Y05l21Wph9xk7kk64UMyvJCl/fFM/3CTJq8isXYPLeKW/aZBCdb/LYNv63PwhY8khw8VWocA==", + "dependencies": { + "lodash": "^4.17.21", + "validator": "~13.12.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -1592,6 +1611,11 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -1841,6 +1865,87 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "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==", + "dependencies": { + "pg-connection-string": "^2.7.0", + "pg-pool": "^3.7.0", + "pg-protocol": "^1.7.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz", + "integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz", + "integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -1853,19 +1958,58 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prisma": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.18.0.tgz", - "integrity": "sha512-+TrSIxZsh64OPOmaSgVPH7ALL9dfU0jceYaMJXsNrTkFHO7/3RANi5K2ZiPB1De9+KDxCWn7jvRq8y8pvk+o9g==", + "version": "5.19.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.19.1.tgz", + "integrity": "sha512-c5K9MiDaa+VAAyh1OiYk76PXOme9s3E992D7kvvIOhCrNsBQfy2mP2QAQtX0WNj140IgG++12kwZpYB9iIydNQ==", + "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/engines": "5.18.0" + "@prisma/engines": "5.19.1" }, "bin": { "prisma": "build/index.js" }, "engines": { "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" } }, "node_modules/process-nextick-args": { @@ -2181,6 +2325,14 @@ "node": ">=10" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -2374,6 +2526,14 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/ems/package.json b/ems/package.json index 887cc90..7c028f1 100644 --- a/ems/package.json +++ b/ems/package.json @@ -13,16 +13,17 @@ "license": "ISC", "description": "", "dependencies": { - "@prisma/client": "^5.18.0", + "@prisma/client": "^5.19.1", "axios": "^1.7.4", "body-parser": "^1.20.2", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", + "express-validator": "^7.2.0", "ioredis": "^5.4.1", "md5": "^2.3.0", "multer": "^1.4.5-lts.1", - "prisma": "^5.18.0", + "pg": "^8.13.0", "pump": "^3.0.0", "sharp": "^0.33.5" }, @@ -36,6 +37,7 @@ "@types/pump": "^1.1.3", "@types/redis": "^4.0.11", "nodemon": "^3.1.4", + "prisma": "^5.19.1", "ts-node": "^10.9.2", "typescript": "^5.5.4" } diff --git a/ems/prisma/schema.prisma b/ems/prisma/schema.prisma index d233cb5..ba9d213 100644 --- a/ems/prisma/schema.prisma +++ b/ems/prisma/schema.prisma @@ -13,28 +13,17 @@ datasource db { url = env("DATABASE_URL") } -model Post { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - title String @db.VarChar(255) - content String? - published Boolean @default(false) - author User @relation(fields: [authorId], references: [id]) - authorId Int +enum ShapeType { + CIRCLE + ELLIPSIS + POLYGON + LINE } -model Profile { - id Int @id @default(autoincrement()) - bio String? - user User @relation(fields: [userId], references: [id]) - userId Int @unique +model nodes { + id String @id @default(uuid()) + object_id Int? + shape_type ShapeType + shape Json @db.Json + label String @db.Text } - -model User { - id Int @id @default(autoincrement()) - email String @unique - name String? - posts Post[] - profile Profile? -} \ No newline at end of file diff --git a/ems/src/index.ts b/ems/src/index.ts index 8ccc6d9..7186d1f 100644 --- a/ems/src/index.ts +++ b/ems/src/index.ts @@ -1,25 +1,27 @@ -import express, { Request, Response } from 'express'; -import fs from 'fs'; -import path from 'path'; -import axios from 'axios'; +import express, { Request, Response } from 'express' +import { PrismaClient } from '@prisma/client' +import fs from 'fs' +import path from 'path' +import axios from 'axios' import multer from 'multer' -import sharp from 'sharp'; -import bodyParser from 'body-parser'; +import bodyParser from 'body-parser' import cors from 'cors' -import { Coordinate, Extent } from './interfaces/map'; -import { epsg3857extent } from './constants'; +import { Coordinate } from './interfaces/map' +import { generateTilesForZoomLevel } from './utils/tiles' +import { query, validationResult } from 'express-validator' -const app = express(); -const PORT = process.env.EMS_PORT || 5000; +const prisma = new PrismaClient() +const app = express() +const PORT = process.env.EMS_PORT || 5000 -const tileFolder = path.join(__dirname, '..', 'public', 'tile_data'); -const uploadDir = path.join(__dirname, '..', 'public', 'temp'); +const tileFolder = path.join(__dirname, '..', 'public', 'tile_data') +const uploadDir = path.join(__dirname, '..', 'public', 'temp') app.use(cors()) app.use(bodyParser.json()) -app.use(bodyParser.urlencoded({ extended: true })); +app.use(bodyParser.urlencoded({ extended: true })) const storage = multer.diskStorage({ destination: function (req, file, cb) { @@ -32,167 +34,69 @@ const storage = multer.diskStorage({ const upload = multer({ storage: storage }) -function getTilesPerSide(zoom: number) { - return Math.pow(2, zoom) -} - -function normalize(value: number, min: number, max: number) { - return (value - min) / (max - min) -} - -function getTileIndex(normalized: number, tilesPerSide: number) { - return Math.floor(normalized * tilesPerSide) -} - -function getGridCellPosition(x: number, y: number, extent: Extent, zoom: number) { - const tilesPerSide = getTilesPerSide(zoom) - const minX = extent[0] - const minY = extent[1] - const maxX = extent[2] - const maxY = extent[3] - const xNormalized = normalize(x, minX, maxX) - const yNormalized = normalize(y, minY, maxY) - const tileX = getTileIndex(xNormalized, tilesPerSide) - const tileY = getTileIndex(1 - yNormalized, tilesPerSide) - return { tileX, tileY } -} - -function calculateRotationAngle(bottomLeft: Coordinate, bottomRight: Coordinate) { - const deltaX = bottomRight.x - bottomLeft.x - const deltaY = bottomRight.y - bottomLeft.y - const angle = -Math.atan2(deltaY, deltaX) - return angle -} - -function roundUpToNearest(number: number, mod: number) { - return Math.floor(number / mod) * mod -} - -async function generateTilesForZoomLevel(file: Express.Multer.File, polygonExtent: Extent, bottomLeft: Coordinate, topLeft: Coordinate, topRight: Coordinate, bottomRight: Coordinate, zoomLevel: number) { - const angleDegrees = calculateRotationAngle(bottomLeft, bottomRight) * 180 / Math.PI - - const { tileX: blX, tileY: blY } = getGridCellPosition(bottomLeft.x, bottomLeft.y, epsg3857extent, zoomLevel) - const { tileX: tlX, tileY: tlY } = getGridCellPosition(topLeft.x, topLeft.y, epsg3857extent, zoomLevel) - const { tileX: trX, tileY: trY } = getGridCellPosition(topRight.x, topRight.y, epsg3857extent, zoomLevel) - const { tileX: brX, tileY: brY } = getGridCellPosition(bottomRight.x, topRight.y, epsg3857extent, zoomLevel) - - const minX = Math.min(blX, tlX, trX, brX) - const maxX = Math.max(blX, tlX, trX, brX) - const minY = Math.min(blY, tlY, trY, brY) - const maxY = Math.max(blY, tlY, trY, brY) - - const mapWidth = Math.abs(epsg3857extent[0] - epsg3857extent[2]) - const mapHeight = Math.abs(epsg3857extent[1] - epsg3857extent[3]) - - const tilesH = Math.sqrt(Math.pow(4, zoomLevel)) - const tileWidth = mapWidth / (Math.sqrt(Math.pow(4, zoomLevel))) - const tileHeight = mapHeight / (Math.sqrt(Math.pow(4, zoomLevel))) - - - let minPosX = minX - (tilesH / 2) - let maxPosX = maxX - (tilesH / 2) + 1 - let minPosY = -(minY - (tilesH / 2)) - let maxPosY = -(maxY - (tilesH / 2) + 1) - - const newMinX = tileWidth * minPosX - const newMaxX = tileWidth * maxPosX - const newMinY = tileHeight * maxPosY - const newMaxY = tileHeight * minPosY - - - const paddingLeft = Math.abs(polygonExtent[0] - newMinX) - const paddingRight = Math.abs(polygonExtent[2] - newMaxX) - const paddingTop = Math.abs(polygonExtent[3] - newMaxY) - const paddingBottom = Math.abs(polygonExtent[1] - newMinY) - - const pixelWidth = Math.abs(minX - (maxX + 1)) * 256 - const pixelHeight = Math.abs(minY - (maxY + 1)) * 256 - - const width = Math.abs(newMinX - newMaxX) - +app.get('/nodes/all', async (req: Request, res: Response) => { try { - let perPixel = width / pixelWidth + const nodes = await prisma.nodes.findMany() - // constraint to original image width - const imageMetadata = await sharp(file.path).metadata().then(res => { - if (res.width) { - perPixel = pixelWidth <= res.width ? perPixel : width / res.width + res.json(nodes) + } catch (error) { + console.error('Error getting node:', error); + res.status(500).json({ error: 'Failed to get node' }); + } +}) + +app.get('/nodes', 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 node = await prisma.nodes.findFirst({ + where: { + id: id } }) - const paddingLeftPixel = paddingLeft / perPixel - const paddingRightPixel = paddingRight / perPixel - const paddingTopPixel = paddingTop / perPixel - const paddingBottomPixel = paddingBottom / perPixel + res.json(node) - const boundsWidthPixel = Math.abs(polygonExtent[0] - polygonExtent[2]) / perPixel - const boundsHeightPixel = Math.abs(polygonExtent[1] - polygonExtent[3]) / perPixel - - if (!fs.existsSync(path.join(tileFolder, 'custom', zoomLevel.toString()))) { - fs.mkdirSync(path.join(tileFolder, 'custom', zoomLevel.toString()), { recursive: true }); - } - - const initialZoomImage = await sharp(path.join(uploadDir, file.filename)) - .rotate(Math.ceil(angleDegrees), { - background: '#00000000' - }) - .resize({ - width: Math.ceil(boundsWidthPixel), - height: Math.ceil(boundsHeightPixel), - background: '#00000000' - }) - .extend({ - top: Math.ceil(paddingTopPixel), - left: Math.ceil(paddingLeftPixel), - bottom: Math.ceil(paddingBottomPixel), - right: Math.ceil(paddingRightPixel), - background: '#00000000' - }) - .toFormat('png') - .toBuffer({ resolveWithObject: true }) - - if (initialZoomImage) { - await sharp(initialZoomImage.data.buffer) - .resize({ - width: roundUpToNearest(Math.ceil(boundsWidthPixel) + Math.ceil(paddingLeftPixel) + Math.ceil(paddingRightPixel), Math.abs(minX - (maxX + 1))), - height: roundUpToNearest(Math.ceil(boundsHeightPixel) + Math.ceil(paddingTopPixel) + Math.ceil(paddingBottomPixel), Math.abs(minY - (maxY + 1))), - }) - .toFile(path.join(tileFolder, 'custom', zoomLevel.toString(), zoomLevel.toString() + '.png')) - .then(async (res) => { - let left = 0 - for (let x = minX; x <= maxX; x++) { - if (!fs.existsSync(path.join(tileFolder, 'custom', zoomLevel.toString(), x.toString()))) { - fs.mkdirSync(path.join(tileFolder, 'custom', zoomLevel.toString(), x.toString()), { recursive: true }); - } - - let top = 0 - for (let y = minY; y <= maxY; y++) { - console.log(`z: ${zoomLevel} x: ${x} y: ${y}`) - - try { - await sharp(path.join(tileFolder, 'custom', zoomLevel.toString(), zoomLevel.toString() + '.png')) - .extract({ - width: res.width / Math.abs(minX - (maxX + 1)), - height: res.height / Math.abs(minY - (maxY + 1)), - left: left, - top: top - }) - .toFile(path.join(tileFolder, 'custom', zoomLevel.toString(), x.toString(), y.toString() + '.png')) - .then(() => { - top = top + res.height / Math.abs(minY - (maxY + 1)) - }) - } catch (error) { - console.log(error) - } - } - left = left + res.width / Math.abs(minX - (maxX + 1)) - } - }) - } } catch (error) { - console.log(error) + console.error('Error getting node:', error); + res.status(500).json({ error: 'Failed to get node' }); } -} +}) + +app.post('/nodes', 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] + })); + + 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' + } + }); + + res.status(201).json(node); + } catch (error) { + console.error('Error creating node:', error); + res.status(500).json({ error: 'Failed to create node' }); + } +}) app.post('/upload', upload.single('file'), async (req: Request, res: Response) => { const { extentMinX, extentMinY, extentMaxX, extentMaxY, blX, blY, tlX, tlY, trX, trY, brX, brY } = req.body @@ -204,7 +108,7 @@ app.post('/upload', upload.single('file'), async (req: Request, res: Response) = if (req.file) { for (let z = 0; z <= 21; z++) { - await generateTilesForZoomLevel(req.file, [extentMinX, extentMinY, extentMaxX, extentMaxY], bottomLeft, topLeft, topRight, bottomRight, z) + await generateTilesForZoomLevel(uploadDir, tileFolder, req.file, [extentMinX, extentMinY, extentMaxX, extentMaxY], bottomLeft, topLeft, topRight, bottomRight, z) } } @@ -214,37 +118,37 @@ app.post('/upload', upload.single('file'), async (req: Request, res: Response) = const fetchTileFromAPI = async (provider: string, z: string, x: string, y: string): Promise => { const url = provider === 'google' ? `https://khms2.google.com/kh/v=984?x=${x}&y=${y}&z=${z}` - : `https://core-sat.maps.yandex.net/tiles?l=sat&x=${x}&y=${y}&z=${z}&scale=1&lang=ru_RU`; + : `https://core-sat.maps.yandex.net/tiles?l=sat&x=${x}&y=${y}&z=${z}&scale=1&lang=ru_RU` - const response = await axios.get(url, { responseType: 'arraybuffer' }); - return response.data; + const response = await axios.get(url, { responseType: 'arraybuffer' }) + return response.data } app.get('/tile/:provider/:z/:x/:y', async (req: Request, res: Response) => { - const { provider, z, x, y } = req.params; + const { provider, z, x, y } = req.params if (!['google', 'yandex', 'custom'].includes(provider)) { - return res.status(400).send('Invalid provider'); + return res.status(400).send('Invalid provider') } - const tilePath = provider === 'custom' ? path.join(tileFolder, provider, z, x, `${y}.png`) : path.join(tileFolder, provider, z, x, `${y}.jpg`); + const tilePath = provider === 'custom' ? path.join(tileFolder, provider, z, x, `${y}.png`) : path.join(tileFolder, provider, z, x, `${y}.jpg`) if (fs.existsSync(tilePath)) { - return res.sendFile(tilePath); + return res.sendFile(tilePath) } else { if (provider !== 'custom') { try { - const tileData = await fetchTileFromAPI(provider, z, x, y); + const tileData = await fetchTileFromAPI(provider, z, x, y) - fs.mkdirSync(path.dirname(tilePath), { recursive: true }); + fs.mkdirSync(path.dirname(tilePath), { recursive: true }) - fs.writeFileSync(tilePath, tileData); + fs.writeFileSync(tilePath, tileData) - res.contentType('image/jpeg'); - res.send(tileData); + res.contentType('image/jpeg') + res.send(tileData) } catch (error) { - console.error('Error fetching tile from API:', error); - res.status(500).send('Error fetching tile from API'); + console.error('Error fetching tile from API:', error) + res.status(500).send('Error fetching tile from API') } } else { res.status(404).send('Tile is not generated or not provided') diff --git a/ems/src/utils/tiles.ts b/ems/src/utils/tiles.ts new file mode 100644 index 0000000..1897fdf --- /dev/null +++ b/ems/src/utils/tiles.ts @@ -0,0 +1,167 @@ +import sharp from "sharp" +import { epsg3857extent } from "../constants" +import { Coordinate, Extent } from "../interfaces/map" +import path from "path" +import fs from 'fs' + +function getTilesPerSide(zoom: number) { + return Math.pow(2, zoom) +} + +function normalize(value: number, min: number, max: number) { + return (value - min) / (max - min) +} + +function getTileIndex(normalized: number, tilesPerSide: number) { + return Math.floor(normalized * tilesPerSide) +} + +function getGridCellPosition(x: number, y: number, extent: Extent, zoom: number) { + const tilesPerSide = getTilesPerSide(zoom) + const minX = extent[0] + const minY = extent[1] + const maxX = extent[2] + const maxY = extent[3] + const xNormalized = normalize(x, minX, maxX) + const yNormalized = normalize(y, minY, maxY) + const tileX = getTileIndex(xNormalized, tilesPerSide) + const tileY = getTileIndex(1 - yNormalized, tilesPerSide) + return { tileX, tileY } +} + +function calculateRotationAngle(bottomLeft: Coordinate, bottomRight: Coordinate) { + const deltaX = bottomRight.x - bottomLeft.x + const deltaY = bottomRight.y - bottomLeft.y + const angle = -Math.atan2(deltaY, deltaX) + return angle +} + +function roundUpToNearest(number: number, mod: number) { + return Math.floor(number / mod) * mod +} + +export async function generateTilesForZoomLevel(uploadDir: string, tileFolder: string, file: Express.Multer.File, polygonExtent: Extent, bottomLeft: Coordinate, topLeft: Coordinate, topRight: Coordinate, bottomRight: Coordinate, zoomLevel: number) { + const angleDegrees = calculateRotationAngle(bottomLeft, bottomRight) * 180 / Math.PI + + const { tileX: blX, tileY: blY } = getGridCellPosition(bottomLeft.x, bottomLeft.y, epsg3857extent, zoomLevel) + const { tileX: tlX, tileY: tlY } = getGridCellPosition(topLeft.x, topLeft.y, epsg3857extent, zoomLevel) + const { tileX: trX, tileY: trY } = getGridCellPosition(topRight.x, topRight.y, epsg3857extent, zoomLevel) + const { tileX: brX, tileY: brY } = getGridCellPosition(bottomRight.x, topRight.y, epsg3857extent, zoomLevel) + + const minX = Math.min(blX, tlX, trX, brX) + const maxX = Math.max(blX, tlX, trX, brX) + const minY = Math.min(blY, tlY, trY, brY) + const maxY = Math.max(blY, tlY, trY, brY) + + const mapWidth = Math.abs(epsg3857extent[0] - epsg3857extent[2]) + const mapHeight = Math.abs(epsg3857extent[1] - epsg3857extent[3]) + + const tilesH = Math.sqrt(Math.pow(4, zoomLevel)) + const tileWidth = mapWidth / (Math.sqrt(Math.pow(4, zoomLevel))) + const tileHeight = mapHeight / (Math.sqrt(Math.pow(4, zoomLevel))) + + + let minPosX = minX - (tilesH / 2) + let maxPosX = maxX - (tilesH / 2) + 1 + let minPosY = -(minY - (tilesH / 2)) + let maxPosY = -(maxY - (tilesH / 2) + 1) + + const newMinX = tileWidth * minPosX + const newMaxX = tileWidth * maxPosX + const newMinY = tileHeight * maxPosY + const newMaxY = tileHeight * minPosY + + + const paddingLeft = Math.abs(polygonExtent[0] - newMinX) + const paddingRight = Math.abs(polygonExtent[2] - newMaxX) + const paddingTop = Math.abs(polygonExtent[3] - newMaxY) + const paddingBottom = Math.abs(polygonExtent[1] - newMinY) + + const pixelWidth = Math.abs(minX - (maxX + 1)) * 256 + const pixelHeight = Math.abs(minY - (maxY + 1)) * 256 + + const width = Math.abs(newMinX - newMaxX) + + try { + let perPixel = width / pixelWidth + + // constraint to original image width + const imageMetadata = await sharp(file.path).metadata().then(res => { + if (res.width) { + perPixel = pixelWidth <= res.width ? perPixel : width / res.width + } + }) + + const paddingLeftPixel = paddingLeft / perPixel + const paddingRightPixel = paddingRight / perPixel + const paddingTopPixel = paddingTop / perPixel + const paddingBottomPixel = paddingBottom / perPixel + + const boundsWidthPixel = Math.abs(polygonExtent[0] - polygonExtent[2]) / perPixel + const boundsHeightPixel = Math.abs(polygonExtent[1] - polygonExtent[3]) / perPixel + + if (!fs.existsSync(path.join(tileFolder, 'custom', zoomLevel.toString()))) { + fs.mkdirSync(path.join(tileFolder, 'custom', zoomLevel.toString()), { recursive: true }); + } + + const initialZoomImage = await sharp(path.join(uploadDir, file.filename)) + .rotate(Math.ceil(angleDegrees), { + background: '#00000000' + }) + .resize({ + width: Math.ceil(boundsWidthPixel), + height: Math.ceil(boundsHeightPixel), + background: '#00000000' + }) + .extend({ + top: Math.ceil(paddingTopPixel), + left: Math.ceil(paddingLeftPixel), + bottom: Math.ceil(paddingBottomPixel), + right: Math.ceil(paddingRightPixel), + background: '#00000000' + }) + .toFormat('png') + .toBuffer({ resolveWithObject: true }) + + if (initialZoomImage) { + await sharp(initialZoomImage.data.buffer) + .resize({ + width: roundUpToNearest(Math.ceil(boundsWidthPixel) + Math.ceil(paddingLeftPixel) + Math.ceil(paddingRightPixel), Math.abs(minX - (maxX + 1))), + height: roundUpToNearest(Math.ceil(boundsHeightPixel) + Math.ceil(paddingTopPixel) + Math.ceil(paddingBottomPixel), Math.abs(minY - (maxY + 1))), + }) + .toFile(path.join(tileFolder, 'custom', zoomLevel.toString(), zoomLevel.toString() + '.png')) + .then(async (res) => { + let left = 0 + for (let x = minX; x <= maxX; x++) { + if (!fs.existsSync(path.join(tileFolder, 'custom', zoomLevel.toString(), x.toString()))) { + fs.mkdirSync(path.join(tileFolder, 'custom', zoomLevel.toString(), x.toString()), { recursive: true }); + } + + let top = 0 + for (let y = minY; y <= maxY; y++) { + console.log(`z: ${zoomLevel} x: ${x} y: ${y}`) + + try { + await sharp(path.join(tileFolder, 'custom', zoomLevel.toString(), zoomLevel.toString() + '.png')) + .extract({ + width: res.width / Math.abs(minX - (maxX + 1)), + height: res.height / Math.abs(minY - (maxY + 1)), + left: left, + top: top + }) + .toFile(path.join(tileFolder, 'custom', zoomLevel.toString(), x.toString(), y.toString() + '.png')) + .then(() => { + top = top + res.height / Math.abs(minY - (maxY + 1)) + }) + } catch (error) { + console.log(error) + } + } + left = left + res.width / Math.abs(minX - (maxX + 1)) + } + }) + } + } catch (error) { + console.log(error) + } +} \ No newline at end of file