|
|
@ -9,12 +9,14 @@ import pump from 'pump' |
|
|
|
import md5 from 'md5' |
|
|
|
import bodyParser from 'body-parser'; |
|
|
|
import cors from 'cors' |
|
|
|
import { Coordinate, Extent } from './interfaces/map'; |
|
|
|
import { epsg3857extent } from './constants'; |
|
|
|
|
|
|
|
const app = express(); |
|
|
|
const PORT = process.env.EMS_PORT || 5000; |
|
|
|
|
|
|
|
const tileFolder = path.join(__dirname, '..', 'public', 'tile_data'); |
|
|
|
const uploadDir = path.join(__dirname, '..', 'public', 'uploads'); |
|
|
|
const uploadDir = path.join(__dirname, '..', 'public', 'temp'); |
|
|
|
|
|
|
|
interface UploadProgress { |
|
|
|
receivedChunks: Set<number>; |
|
|
@ -23,53 +25,55 @@ interface UploadProgress { |
|
|
|
|
|
|
|
const uploadProgress: Record<string, UploadProgress> = {}; |
|
|
|
|
|
|
|
app.use(bodyParser.raw({ |
|
|
|
type: 'application/octet-stream', |
|
|
|
limit: '100mb' |
|
|
|
})) |
|
|
|
|
|
|
|
app.use(cors()) |
|
|
|
|
|
|
|
app.use(bodyParser.json()) |
|
|
|
|
|
|
|
app.use(bodyParser.urlencoded({ extended: true })); |
|
|
|
|
|
|
|
// Upload chunk handler
|
|
|
|
app.post('/upload', (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; |
|
|
|
// 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'); |
|
|
|
} |
|
|
|
// 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 }); |
|
|
|
} |
|
|
|
// 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); |
|
|
|
// // 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'); |
|
|
|
} |
|
|
|
}); |
|
|
|
// // 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> { |
|
|
@ -95,15 +99,185 @@ async function assembleChunks(fileId: string, chunkDir: string): Promise<void> { |
|
|
|
|
|
|
|
const storage = multer.diskStorage({ |
|
|
|
destination: function (req, file, cb) { |
|
|
|
cb(null, path.join(__dirname, '..', 'public', 'uploads')) |
|
|
|
cb(null, path.join(__dirname, '..', 'public', 'temp')) |
|
|
|
}, |
|
|
|
filename: function (req, file, cb) { |
|
|
|
cb(null, file.originalname) |
|
|
|
cb(null, Date.now() + path.extname(file.originalname)) |
|
|
|
} |
|
|
|
}) |
|
|
|
|
|
|
|
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 url = provider === 'google' |
|
|
|
? `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) => { |
|
|
|
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'); |
|
|
|
} |
|
|
|
|
|
|
|
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)) { |
|
|
|
return res.sendFile(tilePath); |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
const tileData = await fetchTileFromAPI(provider, z, x, y); |
|
|
|
} else { |
|
|
|
if (provider !== 'custom') { |
|
|
|
try { |
|
|
|
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); |
|
|
|
} catch (error) { |
|
|
|
console.error('Error fetching tile from API:', error); |
|
|
|
res.status(500).send('Error fetching tile from API'); |
|
|
|
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'); |
|
|
|
} |
|
|
|
} else { |
|
|
|
res.status(404).send('Tile is not generated or not provided') |
|
|
|
} |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|