forked from VinokurovVE/tests
Tile generation
This commit is contained in:
197
ems/src/index.ts
197
ems/src/index.ts
@ -4,9 +4,6 @@ import path from 'path';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import multer from 'multer'
|
import multer from 'multer'
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { pipeline } from 'stream';
|
|
||||||
import pump from 'pump'
|
|
||||||
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 { Coordinate, Extent } from './interfaces/map';
|
||||||
@ -18,85 +15,12 @@ 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', 'temp');
|
const uploadDir = path.join(__dirname, '..', 'public', 'temp');
|
||||||
|
|
||||||
interface UploadProgress {
|
|
||||||
receivedChunks: Set<number>;
|
|
||||||
totalChunks: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadProgress: Record<string, UploadProgress> = {};
|
|
||||||
|
|
||||||
app.use(cors())
|
app.use(cors())
|
||||||
|
|
||||||
app.use(bodyParser.json())
|
app.use(bodyParser.json())
|
||||||
|
|
||||||
app.use(bodyParser.urlencoded({ extended: true }));
|
app.use(bodyParser.urlencoded({ extended: true }));
|
||||||
|
|
||||||
// Upload chunk handler
|
|
||||||
// app.post('/upload', bodyParser.raw({
|
|
||||||
// type: 'application/octet-stream',
|
|
||||||
// limit: '100mb'
|
|
||||||
// }), (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) {
|
|
||||||
// return res.status(400).send('Invalid headers');
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const chunkDir = path.join(uploadDir, fileId);
|
|
||||||
// if (!fs.existsSync(chunkDir)) {
|
|
||||||
// fs.mkdirSync(chunkDir, { recursive: true });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Save the chunk
|
|
||||||
// const chunkPath = path.join(chunkDir, `chunk-${chunkNumber}`);
|
|
||||||
// fs.writeFileSync(chunkPath, req.body);
|
|
||||||
|
|
||||||
// // Initialize or update upload progress
|
|
||||||
// if (!uploadProgress[fileId]) {
|
|
||||||
// uploadProgress[fileId] = { receivedChunks: new Set(), totalChunks };
|
|
||||||
// }
|
|
||||||
// uploadProgress[fileId].receivedChunks.add(chunkNumber);
|
|
||||||
|
|
||||||
// // Check if all chunks have been received
|
|
||||||
// if (uploadProgress[fileId].receivedChunks.size === totalChunks) {
|
|
||||||
// assembleChunks(fileId, chunkDir)
|
|
||||||
// .then(() => {
|
|
||||||
// delete uploadProgress[fileId]; // Clean up progress tracking
|
|
||||||
// res.status(200).send('File assembled successfully');
|
|
||||||
// })
|
|
||||||
// .catch((error) => {
|
|
||||||
// console.error('Error assembling file:', error);
|
|
||||||
// res.status(500).send('Failed to assemble file');
|
|
||||||
// });
|
|
||||||
// } else {
|
|
||||||
// res.status(200).send('Chunk received');
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
// Assemble chunks into final file
|
|
||||||
async function assembleChunks(fileId: string, chunkDir: string): Promise<void> {
|
|
||||||
const finalPath = path.join(uploadDir, fileId);
|
|
||||||
const chunks = fs.readdirSync(chunkDir).sort((a, b) => {
|
|
||||||
const numA = parseInt(a.split('-')[1]);
|
|
||||||
const numB = parseInt(b.split('-')[1]);
|
|
||||||
return numA - numB;
|
|
||||||
});
|
|
||||||
|
|
||||||
const writeStream = fs.createWriteStream(finalPath);
|
|
||||||
for (const chunk of chunks) {
|
|
||||||
const chunkPath = path.join(chunkDir, chunk);
|
|
||||||
const data = fs.readFileSync(chunkPath);
|
|
||||||
writeStream.write(data);
|
|
||||||
fs.unlinkSync(chunkPath); // Remove chunk after writing
|
|
||||||
}
|
|
||||||
|
|
||||||
writeStream.end(() => {
|
|
||||||
fs.rmdirSync(chunkDir); // Remove temporary chunk directory
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: function (req, file, cb) {
|
destination: function (req, file, cb) {
|
||||||
cb(null, path.join(__dirname, '..', 'public', 'temp'))
|
cb(null, path.join(__dirname, '..', 'public', 'temp'))
|
||||||
@ -121,35 +45,30 @@ function getTileIndex(normalized: number, tilesPerSide: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getGridCellPosition(x: number, y: number, extent: Extent, zoom: number) {
|
function getGridCellPosition(x: number, y: number, extent: Extent, zoom: number) {
|
||||||
const tilesPerSide = getTilesPerSide(zoom);
|
const tilesPerSide = getTilesPerSide(zoom)
|
||||||
const minX = extent[0]
|
const minX = extent[0]
|
||||||
const minY = extent[1]
|
const minY = extent[1]
|
||||||
const maxX = extent[2]
|
const maxX = extent[2]
|
||||||
const maxY = extent[3]
|
const maxY = extent[3]
|
||||||
|
const xNormalized = normalize(x, minX, maxX)
|
||||||
// Normalize the coordinates
|
const yNormalized = normalize(y, minY, maxY)
|
||||||
const xNormalized = normalize(x, minX, maxX);
|
const tileX = getTileIndex(xNormalized, tilesPerSide)
|
||||||
const yNormalized = normalize(y, minY, maxY);
|
const tileY = getTileIndex(1 - yNormalized, tilesPerSide)
|
||||||
|
return { tileX, tileY }
|
||||||
// Get tile indices
|
|
||||||
const tileX = getTileIndex(xNormalized, tilesPerSide);
|
|
||||||
const tileY = getTileIndex(1 - yNormalized, tilesPerSide);
|
|
||||||
|
|
||||||
return { tileX, tileY };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateRotationAngle(bottomLeft: Coordinate, bottomRight: Coordinate) {
|
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 deltaX = bottomRight.x - bottomLeft.x;
|
const deltaY = bottomRight.y - bottomLeft.y
|
||||||
const deltaY = bottomRight.y - bottomLeft.y;
|
const angle = -Math.atan2(deltaY, deltaX)
|
||||||
|
return angle
|
||||||
// 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) {
|
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 angleDegrees = calculateRotationAngle(bottomLeft, bottomRight) * 180 / Math.PI
|
||||||
|
|
||||||
const { tileX: blX, tileY: blY } = getGridCellPosition(bottomLeft.x, bottomLeft.y, epsg3857extent, zoomLevel)
|
const { tileX: blX, tileY: blY } = getGridCellPosition(bottomLeft.x, bottomLeft.y, epsg3857extent, zoomLevel)
|
||||||
@ -192,12 +111,14 @@ async function initialImage(file: Express.Multer.File, polygonExtent: Extent, bo
|
|||||||
const width = Math.abs(newMinX - newMaxX)
|
const width = Math.abs(newMinX - newMaxX)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const imageMetadata = await sharp(file.path).metadata();
|
let perPixel = width / pixelWidth
|
||||||
const originalImageWidth = imageMetadata.width
|
|
||||||
const originalImageHeight = imageMetadata.height
|
|
||||||
|
|
||||||
const perPixel = width / pixelWidth
|
// constraint to original image width
|
||||||
console.log(perPixel)
|
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 paddingLeftPixel = paddingLeft / perPixel
|
||||||
const paddingRightPixel = paddingRight / perPixel
|
const paddingRightPixel = paddingRight / perPixel
|
||||||
@ -205,17 +126,19 @@ async function initialImage(file: Express.Multer.File, polygonExtent: Extent, bo
|
|||||||
const paddingBottomPixel = paddingBottom / perPixel
|
const paddingBottomPixel = paddingBottom / perPixel
|
||||||
|
|
||||||
const boundsWidthPixel = Math.abs(polygonExtent[0] - polygonExtent[2]) / 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()))) {
|
if (!fs.existsSync(path.join(tileFolder, 'custom', zoomLevel.toString()))) {
|
||||||
fs.mkdirSync(path.join(tileFolder, 'custom', zoomLevel.toString()), { recursive: true });
|
fs.mkdirSync(path.join(tileFolder, 'custom', zoomLevel.toString()), { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
await sharp(path.join(uploadDir, file.filename))
|
const initialZoomImage = await sharp(path.join(uploadDir, file.filename))
|
||||||
.rotate(Math.ceil(angleDegrees), {
|
.rotate(Math.ceil(angleDegrees), {
|
||||||
background: '#00000000'
|
background: '#00000000'
|
||||||
})
|
})
|
||||||
.resize({
|
.resize({
|
||||||
width: Math.ceil(boundsWidthPixel),
|
width: Math.ceil(boundsWidthPixel),
|
||||||
|
height: Math.ceil(boundsHeightPixel),
|
||||||
background: '#00000000'
|
background: '#00000000'
|
||||||
})
|
})
|
||||||
.extend({
|
.extend({
|
||||||
@ -225,37 +148,47 @@ async function initialImage(file: Express.Multer.File, polygonExtent: Extent, bo
|
|||||||
right: Math.ceil(paddingRightPixel),
|
right: Math.ceil(paddingRightPixel),
|
||||||
background: '#00000000'
|
background: '#00000000'
|
||||||
})
|
})
|
||||||
.toFile(path.join(tileFolder, 'custom', zoomLevel.toString(), zoomLevel.toString() + '.png'))
|
.toFormat('png')
|
||||||
.then(async () => {
|
.toBuffer({ resolveWithObject: true })
|
||||||
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
|
if (initialZoomImage) {
|
||||||
for (let y = minY; y <= maxY; y++) {
|
await sharp(initialZoomImage.data.buffer)
|
||||||
console.log(`z: ${zoomLevel} x: ${x} y: ${y}`)
|
.resize({
|
||||||
|
width: roundUpToNearest(Math.ceil(boundsWidthPixel) + Math.ceil(paddingLeftPixel) + Math.ceil(paddingRightPixel), Math.abs(minX - (maxX + 1))),
|
||||||
try {
|
height: roundUpToNearest(Math.ceil(boundsHeightPixel) + Math.ceil(paddingTopPixel) + Math.ceil(paddingBottomPixel), Math.abs(minY - (maxY + 1))),
|
||||||
await sharp(path.join(tileFolder, 'custom', zoomLevel.toString(), zoomLevel.toString() + '.png'))
|
})
|
||||||
.extract({
|
.toFile(path.join(tileFolder, 'custom', zoomLevel.toString(), zoomLevel.toString() + '.png'))
|
||||||
width: 256,
|
.then(async (res) => {
|
||||||
height: 256,
|
let left = 0
|
||||||
left: left,
|
for (let x = minX; x <= maxX; x++) {
|
||||||
top: top
|
if (!fs.existsSync(path.join(tileFolder, 'custom', zoomLevel.toString(), x.toString()))) {
|
||||||
})
|
fs.mkdirSync(path.join(tileFolder, 'custom', zoomLevel.toString(), x.toString()), { recursive: true });
|
||||||
.toFile(path.join(tileFolder, 'custom', zoomLevel.toString(), x.toString(), y.toString() + '.png'))
|
|
||||||
.then(() => {
|
|
||||||
top = top + 256
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
}
|
}
|
||||||
left = left + 256
|
})
|
||||||
}
|
}
|
||||||
})
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
@ -270,8 +203,8 @@ app.post('/upload', upload.single('file'), async (req: Request, res: Response) =
|
|||||||
const bottomRight: Coordinate = { x: brX, y: brY }
|
const bottomRight: Coordinate = { x: brX, y: brY }
|
||||||
|
|
||||||
if (req.file) {
|
if (req.file) {
|
||||||
for (let z = 0; z <= 10; z++) {
|
for (let z = 0; z <= 21; z++) {
|
||||||
await initialImage(req.file, [extentMinX, extentMinY, extentMaxX, extentMaxY], bottomLeft, topLeft, topRight, bottomRight, z)
|
await generateTilesForZoomLevel(req.file, [extentMinX, extentMinY, extentMaxX, extentMaxY], bottomLeft, topLeft, topRight, bottomRight, z)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user