切片不够技术来凑

2023-10-25 14:50:16 浏览数 (2)

概述

随着数据经度的提升,18级的切片有些场景已经不够用了,但是大部分在线的栅格切片最大级别还是18级,如果地图继续放大,有的框架(leaflet会,openlayers和mapboxGL不会)会存在没有底图的情况。为处理这种情况,本文通过node实现在级别大于18级的时候将18级的切片进行裁切,解决没有底图的问题。

实现效果

实现代码

获取切片图片,如果z大于18,则取18级的切片进行切割;否则直接返回。

代码语言:javascript复制
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为根据切片索引获取切片范围,其实现如下:

代码语言:javascript复制
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为根据坐标获取切片索引,实现代码如下:
代码语言:javascript复制
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实现将地理坐标转换为屏幕坐标。

代码语言:javascript复制
toScreen(x, y) {
    const res18 = this.getResolution(18)
    return [
        (x   this.TILE_ORIGIN) / res18,
        (this.TILE_ORIGIN - y) / res18
    ]
}

完整代码如下:

代码语言:javascript复制
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')
})

0 人点赞