Files
tests/ems/src/utils/tiles.ts
2024-09-26 12:02:01 +09:00

167 lines
7.2 KiB
TypeScript

import sharp from "sharp"
import { epsg3857extent } from "../constants"
import { Coordinate, Extent } from "../interfaces/map"
import path from "path"
import fs from 'fs'
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]
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) {
const deltaX = bottomRight.x - bottomLeft.x
const deltaY = bottomRight.y - bottomLeft.y
const angle = -Math.atan2(deltaY, deltaX)
return angle
}
function roundUpToNearest(number: number, mod: number) {
return Math.floor(number / mod) * mod
}
export async function generateTilesForZoomLevel(uploadDir: string, tileFolder: string, 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 {
let perPixel = width / pixelWidth
// 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
const paddingTopPixel = paddingTop / perPixel
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 });
}
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({
top: Math.ceil(paddingTopPixel),
left: Math.ceil(paddingLeftPixel),
bottom: Math.ceil(paddingBottomPixel),
right: Math.ceil(paddingRightPixel),
background: '#00000000'
})
.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: 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))
}
})
}
} catch (error) {
console.log(error)
}
}