forked from VinokurovVE/tests
Tile generation
This commit is contained in:
@ -7,7 +7,7 @@ import { Draw, Modify, Select, Snap, Translate } from 'ol/interaction'
|
|||||||
import { ImageStatic, OSM, TileDebug, Vector as VectorSource, XYZ } from 'ol/source'
|
import { ImageStatic, OSM, TileDebug, Vector as VectorSource, XYZ } from 'ol/source'
|
||||||
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'
|
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'
|
||||||
import { Divider, IconButton, Slider, Stack, Select as MUISelect, MenuItem, Box, Typography } from '@mui/material'
|
import { Divider, IconButton, Slider, Stack, Select as MUISelect, MenuItem, Box, Typography } from '@mui/material'
|
||||||
import { Add, Adjust, Api, CircleOutlined, OpenWith, RectangleOutlined, Straighten, Timeline, Undo, Warning } from '@mui/icons-material'
|
import { Add, Adjust, Api, CircleOutlined, OpenWith, RectangleOutlined, Straighten, Timeline, Undo, Upload, Warning } from '@mui/icons-material'
|
||||||
import { Type } from 'ol/geom/Geometry'
|
import { Type } from 'ol/geom/Geometry'
|
||||||
import { click, never, noModifierKeys, platformModifierKeyOnly, primaryAction, shiftKeyOnly } from 'ol/events/condition'
|
import { click, never, noModifierKeys, platformModifierKeyOnly, primaryAction, shiftKeyOnly } from 'ol/events/condition'
|
||||||
import Feature from 'ol/Feature'
|
import Feature from 'ol/Feature'
|
||||||
@ -26,6 +26,7 @@ import { Stroke, Fill, Circle as CircleStyle, Style } from 'ol/style'
|
|||||||
import { calculateExtent, calculateRotationAngle, rotateProjection } from './mapUtils'
|
import { calculateExtent, calculateRotationAngle, rotateProjection } from './mapUtils'
|
||||||
import MapBrowserEvent from 'ol/MapBrowserEvent'
|
import MapBrowserEvent from 'ol/MapBrowserEvent'
|
||||||
import { get } from 'ol/proj'
|
import { get } from 'ol/proj'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
const MapComponent = () => {
|
const MapComponent = () => {
|
||||||
const [currentCoordinate, setCurrentCoordinate] = useState<Coordinate | null>(null)
|
const [currentCoordinate, setCurrentCoordinate] = useState<Coordinate | null>(null)
|
||||||
@ -33,15 +34,29 @@ const MapComponent = () => {
|
|||||||
const [currentX, setCurrentX] = useState<number | undefined>(undefined)
|
const [currentX, setCurrentX] = useState<number | undefined>(undefined)
|
||||||
const [currentY, setCurrentY] = useState<number | undefined>(undefined)
|
const [currentY, setCurrentY] = useState<number | undefined>(undefined)
|
||||||
|
|
||||||
|
const [testExtent, setTestExtent] = useState<Extent | null>(null)
|
||||||
|
|
||||||
|
const [file, setFile] = useState(null)
|
||||||
|
const [polygonExtent, setPolygonExtent] = useState<Extent | undefined>(undefined)
|
||||||
|
const [bottomLeft, setBottomLeft] = useState<Coordinate | undefined>(undefined)
|
||||||
|
const [topLeft, setTopLeft] = useState<Coordinate | undefined>(undefined)
|
||||||
|
const [topRight, setTopRight] = useState<Coordinate | undefined>(undefined)
|
||||||
|
const [bottomRight, setBottomRight] = useState<Coordinate | undefined>(undefined)
|
||||||
|
|
||||||
const mapElement = useRef<HTMLDivElement | null>(null)
|
const mapElement = useRef<HTMLDivElement | null>(null)
|
||||||
const [currentTool, setCurrentTool] = useState<Type | null>(null)
|
const [currentTool, setCurrentTool] = useState<Type | null>(null)
|
||||||
|
|
||||||
const map = useRef<Map | null>(null)
|
const map = useRef<Map | null>(null)
|
||||||
|
|
||||||
const [satMapsProvider, setSatMapsProvider] = useState<SatelliteMapsProvider>('yandex')
|
const [satMapsProvider, setSatMapsProvider] = useState<SatelliteMapsProvider>('custom')
|
||||||
|
|
||||||
const gMapsSatSource = useRef<XYZ>(googleMapsSatelliteSource)
|
const gMapsSatSource = useRef<XYZ>(googleMapsSatelliteSource)
|
||||||
|
|
||||||
|
const customMapSource = useRef<XYZ>(new XYZ({
|
||||||
|
url: `${import.meta.env.VITE_API_EMS_URL}/tile/custom/{z}/{x}/{y}`,
|
||||||
|
attributions: 'Custom map data'
|
||||||
|
}))
|
||||||
|
|
||||||
const yMapsSatSource = useRef<XYZ>(yandexMapsSatelliteSource)
|
const yMapsSatSource = useRef<XYZ>(yandexMapsSatelliteSource)
|
||||||
|
|
||||||
const satLayer = useRef<TileLayer>(new TileLayer({
|
const satLayer = useRef<TileLayer>(new TileLayer({
|
||||||
@ -204,6 +219,7 @@ const MapComponent = () => {
|
|||||||
const files = event.dataTransfer.files;
|
const files = event.dataTransfer.files;
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
|
setFile(file)
|
||||||
|
|
||||||
if (file.type.startsWith('image/')) {
|
if (file.type.startsWith('image/')) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
@ -316,6 +332,12 @@ const MapComponent = () => {
|
|||||||
const topRight = polygonFeature.getGeometry()?.getCoordinates()[0][2]
|
const topRight = polygonFeature.getGeometry()?.getCoordinates()[0][2]
|
||||||
const bottomRight = polygonFeature.getGeometry()?.getCoordinates()[0][3]
|
const bottomRight = polygonFeature.getGeometry()?.getCoordinates()[0][3]
|
||||||
|
|
||||||
|
setPolygonExtent(newExtent)
|
||||||
|
setBottomLeft(bottomLeft)
|
||||||
|
setTopLeft(topLeft)
|
||||||
|
setTopRight(topRight)
|
||||||
|
setBottomRight(bottomRight)
|
||||||
|
|
||||||
if (newExtent && bottomLeft && bottomRight && topRight && topLeft) {
|
if (newExtent && bottomLeft && bottomRight && topRight && topLeft) {
|
||||||
const originalExtent = calculateExtent(bottomLeft, topLeft, topRight, bottomRight)
|
const originalExtent = calculateExtent(bottomLeft, topLeft, topRight, bottomRight)
|
||||||
|
|
||||||
@ -332,17 +354,59 @@ const MapComponent = () => {
|
|||||||
|
|
||||||
const mapWidth = Math.abs(worldExtent[0] - worldExtent[2])
|
const mapWidth = Math.abs(worldExtent[0] - worldExtent[2])
|
||||||
const mapHeight = Math.abs(worldExtent[1] - worldExtent[3])
|
const mapHeight = Math.abs(worldExtent[1] - worldExtent[3])
|
||||||
const tileWidth = mapWidth / (zoomLevel * 4)
|
|
||||||
const tileHeight = mapHeight / (zoomLevel * 4)
|
|
||||||
const newMinX = worldExtent[0] + (tileWidth * minX)
|
|
||||||
const newMaxX = worldExtent[0] + (tileWidth * (maxX + 1))
|
|
||||||
|
|
||||||
const newMinY = worldExtent[1] + (tileHeight * (maxY + 1))
|
const tilesH = Math.sqrt(Math.pow(4, zoomLevel))
|
||||||
const newMaxY = worldExtent[1] + (tileHeight * minY)
|
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)
|
||||||
|
console.log(`tileWidth: ${tileWidth} minPosX: ${minPosX} maxPosX: ${maxPosX} minPosY: ${minPosY} maxPosY: ${maxPosY}`)
|
||||||
|
|
||||||
|
const newMinX = tileWidth * minPosX
|
||||||
|
const newMaxX = tileWidth * maxPosX
|
||||||
|
const newMinY = tileHeight * maxPosY
|
||||||
|
const newMaxY = tileHeight * minPosY
|
||||||
|
|
||||||
console.log('Tile slippy bounds: ', minX, maxX, minY, maxY)
|
console.log('Tile slippy bounds: ', minX, maxX, minY, maxY)
|
||||||
console.log('Tile bounds: ', newMinX, newMaxX, newMinY, newMaxY)
|
console.log('Tile bounds: ', newMinX, newMaxX, newMinY, newMaxY)
|
||||||
|
|
||||||
|
const angleDegrees = calculateRotationAngle(bottomLeft, bottomRight) * 180 / Math.PI
|
||||||
|
|
||||||
|
const paddingLeft = Math.abs(newExtent[0] - newMinX)
|
||||||
|
const paddingRight = Math.abs(newExtent[2] - newMaxX)
|
||||||
|
const paddingTop = Math.abs(newExtent[3] - newMaxY)
|
||||||
|
const paddingBottom = Math.abs(newExtent[1] - newMinY)
|
||||||
|
|
||||||
|
const pixelWidth = Math.abs(minX - (maxX + 1)) * 256
|
||||||
|
const pixelHeight = Math.abs(minY - (maxY + 1)) * 256
|
||||||
|
|
||||||
|
const width = Math.abs(newMinX - newMaxX)
|
||||||
|
const perPixel = width / pixelWidth
|
||||||
|
|
||||||
|
const paddingLeftPixel = paddingLeft / perPixel
|
||||||
|
const paddingRightPixel = paddingRight / perPixel
|
||||||
|
const paddingTopPixel = paddingTop / perPixel
|
||||||
|
const paddingBottomPixel = paddingBottom / perPixel
|
||||||
|
|
||||||
|
console.log('Rotation angle degrees: ', angleDegrees)
|
||||||
|
|
||||||
|
console.log('Padding top pixel: ', paddingTopPixel)
|
||||||
|
console.log('Padding left pixel: ', paddingLeftPixel)
|
||||||
|
console.log('Padding right pixel: ', paddingRightPixel)
|
||||||
|
console.log('Padding bottom pixel: ', paddingBottomPixel)
|
||||||
|
|
||||||
|
console.log('Per pixel: ', width / pixelWidth)
|
||||||
|
|
||||||
|
const boundsWidthPixel = Math.abs(newExtent[0] - newExtent[2]) / perPixel
|
||||||
|
const boundsHeightPixel = Math.abs(newExtent[1] - newExtent[3]) / perPixel
|
||||||
|
console.log('Bounds width pixel', boundsWidthPixel)
|
||||||
|
console.log('Bounds height pixel', boundsHeightPixel)
|
||||||
|
|
||||||
|
// Result will be sharp rotate(angleDegrees), resize(boundsWidthPixel), extend()
|
||||||
|
|
||||||
const newImageSource = new ImageStatic({
|
const newImageSource = new ImageStatic({
|
||||||
url: imageUrl,
|
url: imageUrl,
|
||||||
imageExtent: originalExtent,
|
imageExtent: originalExtent,
|
||||||
@ -623,10 +687,31 @@ const MapComponent = () => {
|
|||||||
|
|
||||||
// Satellite tiles setting
|
// Satellite tiles setting
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
satLayer.current?.setSource(satMapsProvider == 'google' ? gMapsSatSource.current : satMapsProvider == 'yandex' ? yMapsSatSource.current : gMapsSatSource.current)
|
satLayer.current?.setSource(satMapsProvider == 'google' ? gMapsSatSource.current : satMapsProvider == 'yandex' ? yMapsSatSource.current : satMapsProvider == 'custom' ? customMapSource.current : gMapsSatSource.current)
|
||||||
satLayer.current?.getSource()?.refresh()
|
satLayer.current?.getSource()?.refresh()
|
||||||
}, [satMapsProvider])
|
}, [satMapsProvider])
|
||||||
|
|
||||||
|
const submitOverlay = async () => {
|
||||||
|
if (file && polygonExtent && bottomLeft && topLeft && topRight && bottomRight) {
|
||||||
|
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', bottomLeft[0].toString())
|
||||||
|
formData.append('blY', bottomLeft[1].toString())
|
||||||
|
formData.append('tlX', topLeft[0].toString())
|
||||||
|
formData.append('tlY', topLeft[1].toString())
|
||||||
|
formData.append('trX', topRight[0].toString())
|
||||||
|
formData.append('trY', topRight[1].toString())
|
||||||
|
formData.append('brX', bottomRight[0].toString())
|
||||||
|
formData.append('brY', bottomRight[1].toString())
|
||||||
|
|
||||||
|
await fetch(`${import.meta.env.VITE_API_EMS_URL}/upload`, { method: 'POST', body: formData })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack flex={1} flexDirection='column'>
|
<Stack flex={1} flexDirection='column'>
|
||||||
<Stack my={1} spacing={1} direction='row' divider={<Divider orientation='vertical' flexItem />}>
|
<Stack my={1} spacing={1} direction='row' divider={<Divider orientation='vertical' flexItem />}>
|
||||||
@ -634,15 +719,24 @@ const MapComponent = () => {
|
|||||||
<Add />
|
<Add />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<Typography>
|
<Stack>
|
||||||
{currentCoordinate?.[0]}-{currentCoordinate?.[1]}
|
<Typography>
|
||||||
</Typography>
|
x: {currentCoordinate?.[0]}
|
||||||
|
</Typography>
|
||||||
|
<Typography>
|
||||||
|
y: {currentCoordinate?.[1]}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
<Typography>
|
<Typography>
|
||||||
Z={currentZ}
|
Z={currentZ}
|
||||||
X={currentX}
|
X={currentX}
|
||||||
Y={currentY}
|
Y={currentY}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
|
<IconButton onClick={() => submitOverlay()}>
|
||||||
|
<Upload />
|
||||||
|
</IconButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack my={1} spacing={1} direction='row' divider={<Divider orientation='vertical' flexItem />}>
|
<Stack my={1} spacing={1} direction='row' divider={<Divider orientation='vertical' flexItem />}>
|
||||||
|
|
||||||
@ -660,6 +754,7 @@ const MapComponent = () => {
|
|||||||
>
|
>
|
||||||
<MenuItem value={'google'}>Google</MenuItem>
|
<MenuItem value={'google'}>Google</MenuItem>
|
||||||
<MenuItem value={'yandex'}>Яндекс</MenuItem>
|
<MenuItem value={'yandex'}>Яндекс</MenuItem>
|
||||||
|
<MenuItem value={'custom'}>Custom</MenuItem>
|
||||||
</MUISelect>
|
</MUISelect>
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export interface SatelliteMapsProviders {
|
export interface SatelliteMapsProviders {
|
||||||
google: 'google';
|
google: 'google';
|
||||||
yandex: 'yandex';
|
yandex: 'yandex';
|
||||||
|
custom: 'custom';
|
||||||
}
|
}
|
||||||
export type SatelliteMapsProvider = SatelliteMapsProviders[keyof SatelliteMapsProviders]
|
export type SatelliteMapsProvider = SatelliteMapsProviders[keyof SatelliteMapsProviders]
|
12
ems/src/constants/index.ts
Normal file
12
ems/src/constants/index.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Extent } from "../interfaces/map"
|
||||||
|
|
||||||
|
const epsg3857extent = [
|
||||||
|
-20037508.342789244,
|
||||||
|
-20037508.342789244,
|
||||||
|
20037508.342789244,
|
||||||
|
20037508.342789244
|
||||||
|
] as Extent
|
||||||
|
|
||||||
|
export {
|
||||||
|
epsg3857extent
|
||||||
|
}
|
288
ems/src/index.ts
288
ems/src/index.ts
@ -9,12 +9,14 @@ import pump from 'pump'
|
|||||||
import md5 from 'md5'
|
import md5 from 'md5'
|
||||||
import bodyParser from 'body-parser';
|
import bodyParser from 'body-parser';
|
||||||
import cors from 'cors'
|
import cors from 'cors'
|
||||||
|
import { Coordinate, Extent } from './interfaces/map';
|
||||||
|
import { epsg3857extent } from './constants';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.EMS_PORT || 5000;
|
const PORT = process.env.EMS_PORT || 5000;
|
||||||
|
|
||||||
const tileFolder = path.join(__dirname, '..', 'public', 'tile_data');
|
const tileFolder = path.join(__dirname, '..', 'public', 'tile_data');
|
||||||
const uploadDir = path.join(__dirname, '..', 'public', 'uploads');
|
const uploadDir = path.join(__dirname, '..', 'public', 'temp');
|
||||||
|
|
||||||
interface UploadProgress {
|
interface UploadProgress {
|
||||||
receivedChunks: Set<number>;
|
receivedChunks: Set<number>;
|
||||||
@ -23,53 +25,55 @@ interface UploadProgress {
|
|||||||
|
|
||||||
const uploadProgress: Record<string, UploadProgress> = {};
|
const uploadProgress: Record<string, UploadProgress> = {};
|
||||||
|
|
||||||
app.use(bodyParser.raw({
|
|
||||||
type: 'application/octet-stream',
|
|
||||||
limit: '100mb'
|
|
||||||
}))
|
|
||||||
|
|
||||||
app.use(cors())
|
app.use(cors())
|
||||||
|
|
||||||
|
app.use(bodyParser.json())
|
||||||
|
|
||||||
|
app.use(bodyParser.urlencoded({ extended: true }));
|
||||||
|
|
||||||
// Upload chunk handler
|
// Upload chunk handler
|
||||||
app.post('/upload', (req: Request, res: Response) => {
|
// app.post('/upload', bodyParser.raw({
|
||||||
const chunkNumber = parseInt(req.headers['x-chunk-number'] as string, 10);
|
// type: 'application/octet-stream',
|
||||||
const totalChunks = parseInt(req.headers['x-total-chunks'] as string, 10);
|
// limit: '100mb'
|
||||||
const fileId = req.headers['x-file-id'] as string;
|
// }), (req: Request, res: Response) => {
|
||||||
|
// const chunkNumber = parseInt(req.headers['x-chunk-number'] as string, 10);
|
||||||
|
// const totalChunks = parseInt(req.headers['x-total-chunks'] as string, 10);
|
||||||
|
// const fileId = req.headers['x-file-id'] as string;
|
||||||
|
|
||||||
if (isNaN(chunkNumber) || isNaN(totalChunks) || !fileId) {
|
// if (isNaN(chunkNumber) || isNaN(totalChunks) || !fileId) {
|
||||||
return res.status(400).send('Invalid headers');
|
// return res.status(400).send('Invalid headers');
|
||||||
}
|
// }
|
||||||
|
|
||||||
const chunkDir = path.join(uploadDir, fileId);
|
// const chunkDir = path.join(uploadDir, fileId);
|
||||||
if (!fs.existsSync(chunkDir)) {
|
// if (!fs.existsSync(chunkDir)) {
|
||||||
fs.mkdirSync(chunkDir, { recursive: true });
|
// fs.mkdirSync(chunkDir, { recursive: true });
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Save the chunk
|
// // Save the chunk
|
||||||
const chunkPath = path.join(chunkDir, `chunk-${chunkNumber}`);
|
// const chunkPath = path.join(chunkDir, `chunk-${chunkNumber}`);
|
||||||
fs.writeFileSync(chunkPath, req.body);
|
// fs.writeFileSync(chunkPath, req.body);
|
||||||
|
|
||||||
// Initialize or update upload progress
|
// // Initialize or update upload progress
|
||||||
if (!uploadProgress[fileId]) {
|
// if (!uploadProgress[fileId]) {
|
||||||
uploadProgress[fileId] = { receivedChunks: new Set(), totalChunks };
|
// uploadProgress[fileId] = { receivedChunks: new Set(), totalChunks };
|
||||||
}
|
// }
|
||||||
uploadProgress[fileId].receivedChunks.add(chunkNumber);
|
// uploadProgress[fileId].receivedChunks.add(chunkNumber);
|
||||||
|
|
||||||
// Check if all chunks have been received
|
// // Check if all chunks have been received
|
||||||
if (uploadProgress[fileId].receivedChunks.size === totalChunks) {
|
// if (uploadProgress[fileId].receivedChunks.size === totalChunks) {
|
||||||
assembleChunks(fileId, chunkDir)
|
// assembleChunks(fileId, chunkDir)
|
||||||
.then(() => {
|
// .then(() => {
|
||||||
delete uploadProgress[fileId]; // Clean up progress tracking
|
// delete uploadProgress[fileId]; // Clean up progress tracking
|
||||||
res.status(200).send('File assembled successfully');
|
// res.status(200).send('File assembled successfully');
|
||||||
})
|
// })
|
||||||
.catch((error) => {
|
// .catch((error) => {
|
||||||
console.error('Error assembling file:', error);
|
// console.error('Error assembling file:', error);
|
||||||
res.status(500).send('Failed to assemble file');
|
// res.status(500).send('Failed to assemble file');
|
||||||
});
|
// });
|
||||||
} else {
|
// } else {
|
||||||
res.status(200).send('Chunk received');
|
// res.status(200).send('Chunk received');
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
// Assemble chunks into final file
|
// Assemble chunks into final file
|
||||||
async function assembleChunks(fileId: string, chunkDir: string): Promise<void> {
|
async function assembleChunks(fileId: string, chunkDir: string): Promise<void> {
|
||||||
@ -95,15 +99,185 @@ async function assembleChunks(fileId: string, chunkDir: string): Promise<void> {
|
|||||||
|
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: function (req, file, cb) {
|
destination: function (req, file, cb) {
|
||||||
cb(null, path.join(__dirname, '..', 'public', 'uploads'))
|
cb(null, path.join(__dirname, '..', 'public', 'temp'))
|
||||||
},
|
},
|
||||||
filename: function (req, file, cb) {
|
filename: function (req, file, cb) {
|
||||||
cb(null, file.originalname)
|
cb(null, Date.now() + path.extname(file.originalname))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const upload = multer({ storage: storage })
|
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]
|
||||||
|
|
||||||
|
// Normalize the coordinates
|
||||||
|
const xNormalized = normalize(x, minX, maxX);
|
||||||
|
const yNormalized = normalize(y, minY, maxY);
|
||||||
|
|
||||||
|
// Get tile indices
|
||||||
|
const tileX = getTileIndex(xNormalized, tilesPerSide);
|
||||||
|
const tileY = getTileIndex(1 - yNormalized, tilesPerSide);
|
||||||
|
|
||||||
|
return { tileX, tileY };
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateRotationAngle(bottomLeft: Coordinate, bottomRight: Coordinate) {
|
||||||
|
// Calculate the difference in x and y coordinates between bottom right and bottom left
|
||||||
|
const deltaX = bottomRight.x - bottomLeft.x;
|
||||||
|
const deltaY = bottomRight.y - bottomLeft.y;
|
||||||
|
|
||||||
|
// Calculate the angle using atan2
|
||||||
|
const angle = -Math.atan2(deltaY, deltaX);
|
||||||
|
|
||||||
|
return angle;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initialImage(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 {
|
||||||
|
const imageMetadata = await sharp(file.path).metadata();
|
||||||
|
const originalImageWidth = imageMetadata.width
|
||||||
|
const originalImageHeight = imageMetadata.height
|
||||||
|
|
||||||
|
const perPixel = width / pixelWidth
|
||||||
|
console.log(perPixel)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if (!fs.existsSync(path.join(tileFolder, 'custom', zoomLevel.toString()))) {
|
||||||
|
fs.mkdirSync(path.join(tileFolder, 'custom', zoomLevel.toString()), { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
await sharp(path.join(uploadDir, file.filename))
|
||||||
|
.rotate(Math.ceil(angleDegrees), {
|
||||||
|
background: '#00000000'
|
||||||
|
})
|
||||||
|
.resize({
|
||||||
|
width: Math.ceil(boundsWidthPixel),
|
||||||
|
background: '#00000000'
|
||||||
|
})
|
||||||
|
.extend({
|
||||||
|
top: Math.ceil(paddingTopPixel),
|
||||||
|
left: Math.ceil(paddingLeftPixel),
|
||||||
|
bottom: Math.ceil(paddingBottomPixel),
|
||||||
|
right: Math.ceil(paddingRightPixel),
|
||||||
|
background: '#00000000'
|
||||||
|
})
|
||||||
|
.toFile(path.join(tileFolder, 'custom', zoomLevel.toString(), zoomLevel.toString() + '.png'))
|
||||||
|
.then(async () => {
|
||||||
|
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: 256,
|
||||||
|
height: 256,
|
||||||
|
left: left,
|
||||||
|
top: top
|
||||||
|
})
|
||||||
|
.toFile(path.join(tileFolder, 'custom', zoomLevel.toString(), x.toString(), y.toString() + '.png'))
|
||||||
|
.then(() => {
|
||||||
|
top = top + 256
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
left = left + 256
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
const bottomLeft: Coordinate = { x: blX, y: blY }
|
||||||
|
const topLeft: Coordinate = { x: tlX, y: tlY }
|
||||||
|
const topRight: Coordinate = { x: trX, y: trY }
|
||||||
|
const bottomRight: Coordinate = { x: brX, y: brY }
|
||||||
|
|
||||||
|
if (req.file) {
|
||||||
|
for (let z = 0; z <= 10; z++) {
|
||||||
|
await initialImage(req.file, [extentMinX, extentMinY, extentMaxX, extentMaxY], bottomLeft, topLeft, topRight, bottomRight, z)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200)
|
||||||
|
})
|
||||||
|
|
||||||
const fetchTileFromAPI = async (provider: string, z: string, x: string, y: string): Promise<Buffer> => {
|
const fetchTileFromAPI = async (provider: string, z: string, x: string, y: string): Promise<Buffer> => {
|
||||||
const url = provider === 'google'
|
const url = provider === 'google'
|
||||||
? `https://khms2.google.com/kh/v=984?x=${x}&y=${y}&z=${z}`
|
? `https://khms2.google.com/kh/v=984?x=${x}&y=${y}&z=${z}`
|
||||||
@ -116,28 +290,32 @@ const fetchTileFromAPI = async (provider: string, z: string, x: string, y: strin
|
|||||||
app.get('/tile/:provider/:z/:x/:y', async (req: Request, res: Response) => {
|
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'].includes(provider)) {
|
if (!['google', 'yandex', 'custom'].includes(provider)) {
|
||||||
return res.status(400).send('Invalid provider');
|
return res.status(400).send('Invalid provider');
|
||||||
}
|
}
|
||||||
|
|
||||||
const tilePath = 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)) {
|
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);
|
||||||
|
|
||||||
try {
|
fs.mkdirSync(path.dirname(tilePath), { recursive: true });
|
||||||
const tileData = await fetchTileFromAPI(provider, z, x, y);
|
|
||||||
|
|
||||||
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');
|
} catch (error) {
|
||||||
res.send(tileData);
|
console.error('Error fetching tile from API:', error);
|
||||||
} catch (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')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,4 +2,11 @@ export interface SatelliteMapsProviders {
|
|||||||
google: 'google';
|
google: 'google';
|
||||||
yandex: 'yandex';
|
yandex: 'yandex';
|
||||||
}
|
}
|
||||||
export type SatelliteMapsProvider = SatelliteMapsProviders[keyof SatelliteMapsProviders]
|
export type SatelliteMapsProvider = SatelliteMapsProviders[keyof SatelliteMapsProviders]
|
||||||
|
|
||||||
|
export interface Coordinate {
|
||||||
|
x: number,
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Extent = [number, number, number, number]
|
Reference in New Issue
Block a user