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)}
- >
-
-
-
-
-
-
+
+ }>
{
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)}
+ >
+
+
+
+
+
+
+
+ }
+ 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