前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >切片不够技术来凑

切片不够技术来凑

作者头像
lzugis
发布2023-10-25 14:50:16
2110
发布2023-10-25 14:50:16
举报

概述

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

实现效果

动画.gif
动画.gif

实现代码

获取切片图片,如果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')
})
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-10-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 概述
  • 实现效果
  • 实现代码
相关产品与服务
消息队列 TDMQ
消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档