概述
随着数据经度的提升,18级的切片有些场景已经不够用了,但是大部分在线的栅格切片最大级别还是18级,如果地图继续放大,有的框架(leaflet会,openlayers和mapboxGL不会)会存在没有底图的情况。为处理这种情况,本文通过node实现在级别大于18级的时候将18级的切片进行裁切,解决没有底图的问题。
实现效果
实现代码
获取切片图片,如果z大于18,则取18级的切片进行切割;否则直接返回。
getTileData(z, x, y) {
return new Promise(resolve => {
let url = '', extent = [], xy18 = []
if(z > 18 ) {
extent = this.getTileExtent(z, x, y)
const [minX, minY, maxX, maxY] = extent
// 获取18级对应的索引
xy18 = this.getTileIndexByCoords((minX + maxX) / 2, (minY + maxY) / 2)
const [x18, y18] = xy18
url = `https://webrd01.is.autonavi.com/appmaptile?style=8&lang=zh_cn&size=1&scale=1&x=${x18}&y=${y18}&z=18`
} else {
url = `https://webrd01.is.autonavi.com/appmaptile?style=8&lang=zh_cn&size=1&scale=1&x=${x}&y=${y}&z=${z}`
}
loadImage(url).then(image => {
this.ctx.clearRect(0, 0, this.TILE_SIZE, this.TILE_SIZE)
if(z > 18) {
const [minX, minY, maxX, maxY] = extent
const [x18, y18] = xy18
const [minX18, minY18, maxX18, maxY18] = this.getTileExtent(18, x18, y18)
const [srcx18, srcy18] = this.toScreen(minX18, maxY18)
const [srcxmin, srcymin] = this.toScreen(minX, maxY)
const [srcxmax, srcymax] = this.toScreen(maxX, minY)
const scrx = Math.round(srcxmin - srcx18),
scry = Math.round(srcymin - srcy18)
const width = Math.round(srcxmax - srcx18 - scrx),
height = Math.round(srcymax - srcy18 - scry)
this.ctx.drawImage(image, scrx, scry, width, height, 0, 0, this.TILE_SIZE, this.TILE_SIZE)
} else {
this.ctx.drawImage(image, 0, 0, this.TILE_SIZE, this.TILE_SIZE)
}
resolve(this.canvas.toBuffer('image/png'))
})
})
}
getTileExtent
为根据切片索引获取切片范围,其实现如下:
getResolution(z) {
return (this.TILE_ORIGIN * 2) / (Math.pow(2, z) * this.TILE_SIZE)
}
/**
* 获取切片范围
* @param {number} z
* @param {number} x
* @param {number} y
* @returns {number}
*/
getTileExtent(z, x, y) {
const res = this.getResolution(z)
const minX = x * this.TILE_SIZE * res - this.TILE_ORIGIN
const maxX = (x + 1) * this.TILE_SIZE * res - this.TILE_ORIGIN
const minY = this.TILE_ORIGIN - (y + 1) * this.TILE_SIZE * res
const maxY = this.TILE_ORIGIN - y * this.TILE_SIZE * res
return [minX, minY, maxX, maxY]
}
其中:
TILE_SIZE
,切片大小,值为256;TILE_ORIGIN
,切片原点,值为20037508.34;
getTileIndexByCoords
为根据坐标获取切片索引,实现代码如下:
getTileIndexByCoords(x, y) {
const res18 = this.getResolution(18) * this.TILE_SIZE
return [
Math.floor((x + this.TILE_ORIGIN) / res18),
Math.floor((this.TILE_ORIGIN - y) / res18)
]
}
toScreen
实现将地理坐标转换为屏幕坐标。
toScreen(x, y) {
const res18 = this.getResolution(18)
return [
(x + this.TILE_ORIGIN) / res18,
(this.TILE_ORIGIN - y) / res18
]
}
完整代码如下:
import { createCanvas, loadImage } from 'canvas'
import express from 'express'
console.time('app')
const app = express()
// 自定义跨域中间件
const allowCors = function (req, res, next) {
res.header('Access-Control-Allow-Origin', req.headers.origin);
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type');
res.header('Access-Control-Allow-Credentials', 'true');
next();
};
app.use(allowCors);// 使用跨域中间件
app.use(express.static('public'))
class TileUtil {
constructor() {
this.TILE_ORIGIN = 20037508.34 // 切片原点
this.TILE_SIZE = 256; // 切片大小
this.canvas = createCanvas(this.TILE_SIZE, this.TILE_SIZE)
this.ctx = this.canvas.getContext('2d')
}
/**
* 计算分辨率
* @param {number} z - 缩放级别
* @returns {number}
*/
getResolution(z) {
return (this.TILE_ORIGIN * 2) / (Math.pow(2, z) * this.TILE_SIZE)
}
/**
* 获取切片范围
* @param {number} z
* @param {number} x
* @param {number} y
* @returns {number}
*/
getTileExtent(z, x, y) {
const res = this.getResolution(z)
const minX = x * this.TILE_SIZE * res - this.TILE_ORIGIN
const maxX = (x + 1) * this.TILE_SIZE * res - this.TILE_ORIGIN
const minY = this.TILE_ORIGIN - (y + 1) * this.TILE_SIZE * res
const maxY = this.TILE_ORIGIN - y * this.TILE_SIZE * res
return [minX, minY, maxX, maxY]
}
/**
* 将地理坐标转换为屏幕坐标
* @param {number} x
* @param {number} y
* @returns {number}
*/
toScreen(x, y) {
const res18 = this.getResolution(18)
return [
(x + this.TILE_ORIGIN) / res18,
(this.TILE_ORIGIN - y) / res18
]
}
/**
* 获取切片图片,如果z大于18,则取18级的切片进行切割;否则直接返回
* @param {number} z
* @param {number} x
* @param {number} y
* @returns {Buffer<Image>}
*/
getTileData(z, x, y) {
return new Promise(resolve => {
let url = '', extent = [], xy18 = []
if(z > 18 ) {
extent = this.getTileExtent(z, x, y)
const [minX, minY, maxX, maxY] = extent
// 获取18级对应的索引
xy18 = this.getTileIndexByCoords((minX + maxX) / 2, (minY + maxY) / 2)
const [x18, y18] = xy18
url = `https://webrd01.is.autonavi.com/appmaptile?style=8&lang=zh_cn&size=1&scale=1&x=${x18}&y=${y18}&z=18`
} else {
url = `https://webrd01.is.autonavi.com/appmaptile?style=8&lang=zh_cn&size=1&scale=1&x=${x}&y=${y}&z=${z}`
}
loadImage(url).then(image => {
this.ctx.clearRect(0, 0, this.TILE_SIZE, this.TILE_SIZE)
if(z > 18) {
const [minX, minY, maxX, maxY] = extent
const [x18, y18] = xy18
const [minX18, minY18, maxX18, maxY18] = this.getTileExtent(18, x18, y18)
const [srcx18, srcy18] = this.toScreen(minX18, maxY18)
const [srcxmin, srcymin] = this.toScreen(minX, maxY)
const [srcxmax, srcymax] = this.toScreen(maxX, minY)
const scrx = Math.round(srcxmin - srcx18),
scry = Math.round(srcymin - srcy18)
const width = Math.round(srcxmax - srcx18 - scrx),
height = Math.round(srcymax - srcy18 - scry)
this.ctx.drawImage(image, scrx, scry, width, height, 0, 0, this.TILE_SIZE, this.TILE_SIZE)
} else {
this.ctx.drawImage(image, 0, 0, this.TILE_SIZE, this.TILE_SIZE)
}
resolve(this.canvas.toBuffer('image/png'))
})
})
}
/**
* 根据坐标获取切片索引
* @param {number} x
* @param {number} y
* @returns {[<number>, <number>]}
*/
getTileIndexByCoords(x, y) {
const res18 = this.getResolution(18) * this.TILE_SIZE
return [
Math.floor((x + this.TILE_ORIGIN) / res18),
Math.floor((this.TILE_ORIGIN - y) / res18)
]
}
}
const util = new TileUtil()
app.get('/tile/:z/:x/:y', (req, res) => {
const { z, x, y } = req.params
util.getTileData(Number(z), Number(x), Number(y)).then(data => {
res.setHeader('Expires', new Date(Date.now() + 30 * 1000).toUTCString())
res.writeHead(200, {
"Content-Type": "image/png",
});
res.end(data);
})
})
app.get('/tile-bbox/:z/:x/:y', (req, res) => {
const { z, x, y } = req.params
const TILE_SIZE = 256;
const canvas = createCanvas(TILE_SIZE, TILE_SIZE)
const ctx = canvas.getContext('2d')
ctx.fillStyle = '#f00'
ctx.strokeStyle = '#f00'
ctx.lineWidth = 2
ctx.textAlign = "center";
ctx.textBaseline = "middle"
ctx.font = "bold 18px 微软雅黑";
ctx.strokeRect(0, 0, TILE_SIZE, TILE_SIZE)
ctx.fillText(`${z}-${x}-${y}`, TILE_SIZE / 2, TILE_SIZE / 2)
res.setHeader('Expires', new Date(Date.now() + 30 * 1000).toUTCString())
res.writeHead(200, {
"Content-Type": "image/png",
});
res.end(canvas.toBuffer('image/png'));
})
app.listen(18089, () => {
console.timeEnd('app')
console.log('express server running at http://127.0.0.1:18089')
})