Tile generation

This commit is contained in:
cracklesparkle
2024-09-09 17:49:32 +09:00
parent ddacbcd837
commit 8c8c619143
5 changed files with 361 additions and 68 deletions

View File

@ -0,0 +1,12 @@
import { Extent } from "../interfaces/map"
const epsg3857extent = [
-20037508.342789244,
-20037508.342789244,
20037508.342789244,
20037508.342789244
] as Extent
export {
epsg3857extent
}

View File

@ -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);
// // 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');
}
});
// // 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);
}
} else {
if (provider !== 'custom') {
try {
const tileData = await fetchTileFromAPI(provider, z, x, y);
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')
}
}
});

View File

@ -2,4 +2,11 @@ export interface SatelliteMapsProviders {
google: 'google';
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]