diff --git a/ems/src/index.ts b/ems/src/index.ts index faf28a4..8ccc6d9 100644 --- a/ems/src/index.ts +++ b/ems/src/index.ts @@ -4,9 +4,6 @@ import path from 'path'; import axios from 'axios'; import multer from 'multer' import sharp from 'sharp'; -import { pipeline } from 'stream'; -import pump from 'pump' -import md5 from 'md5' import bodyParser from 'body-parser'; import cors from 'cors' 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 uploadDir = path.join(__dirname, '..', 'public', 'temp'); -interface UploadProgress { - receivedChunks: Set; - totalChunks: number; -} - -const uploadProgress: Record = {}; - app.use(cors()) app.use(bodyParser.json()) 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 { - 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({ destination: function (req, file, cb) { 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) { - const tilesPerSide = getTilesPerSide(zoom); + 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 }; + 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) { - // 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); + const deltaX = bottomRight.x - bottomLeft.x + const deltaY = bottomRight.y - bottomLeft.y + const angle = -Math.atan2(deltaY, deltaX) + return angle +} - return angle; +function roundUpToNearest(number: number, mod: number) { + return Math.floor(number / mod) * mod } -async function initialImage(file: Express.Multer.File, polygonExtent: Extent, bottomLeft: Coordinate, topLeft: Coordinate, topRight: Coordinate, bottomRight: Coordinate, zoomLevel: number) { +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) @@ -192,12 +111,14 @@ async function initialImage(file: Express.Multer.File, polygonExtent: Extent, bo const width = Math.abs(newMinX - newMaxX) try { - const imageMetadata = await sharp(file.path).metadata(); - const originalImageWidth = imageMetadata.width - const originalImageHeight = imageMetadata.height + let perPixel = width / pixelWidth - const perPixel = width / pixelWidth - console.log(perPixel) + // 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 @@ -205,17 +126,19 @@ async function initialImage(file: Express.Multer.File, polygonExtent: Extent, bo 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 }); } - await sharp(path.join(uploadDir, file.filename)) + 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({ @@ -225,37 +148,47 @@ async function initialImage(file: Express.Multer.File, polygonExtent: Extent, bo 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 }); - } + .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: 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) + 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) { 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 } if (req.file) { - for (let z = 0; z <= 10; z++) { - await initialImage(req.file, [extentMinX, extentMinY, extentMaxX, extentMaxY], bottomLeft, topLeft, topRight, bottomRight, z) + for (let z = 0; z <= 21; z++) { + await generateTilesForZoomLevel(req.file, [extentMinX, extentMinY, extentMaxX, extentMaxY], bottomLeft, topLeft, topRight, bottomRight, z) } }