Node 切片拼接及地图导出实例详解

目录
  • 概述
  • 实现效果
  • 实现
    • 1. 初始化工程
    • 2. 编写工具类

概述

本文讲述在node中,使用canvas实现根据出图范围和级别,拼接瓦片并叠加geojson矢量数据,并导出成图片。

实现效果

实现

1. 初始化工程

通过命令npm init -y初始化工程并添加对应的依赖,最终的package.json文件如下:

{
  "name": "map",
  "version": "1.0.0",
  "description": "",
  "main": "map.js",
  "scripts": {
    "map": "node ./map.js"
  },
  "keywords": ["canvas", "map"],
  "author": "lzugis<niujp08@qq.com>",
  "license": "ISC",
  "dependencies": {
    "canvas": "^2.9.3",
    "proj4": "^2.8.0",
    "ora": "^5.4.0"
  }
}

2. 编写工具类

canvas.js,canvas操作工具,主要实现canvas画布初始化,并实现了添加图片 、绘制点、绘制线、绘制面等方法。

const { createCanvas, loadImage } = require('canvas')
class CanvasUtil {
  constructor(width = 1000, height = 1000) {
    this.canvas = createCanvas(width, height)
    this.ctx = this.canvas.getContext('2d')
  }
  /**
   * 绘制多个图片
   * @param imgsData, [{url: '', x: '', y: ''}]
   * @return {Promise<unknown>}
   */
  drawImages(imgsData) {
    const that = this
    let promises = []
    imgsData.forEach(data => {
      promises.push(new Promise(resolve => {
        loadImage(data.url).then(img => {
            resolve({
              ...data,
              img
            })
        })
      }))
    })
    return new Promise(resolve => {
      Promise.all(promises).then(imgDatas => {
        imgDatas.forEach(imgData => {
          that.drawImage(imgData.img, imgData.x, imgData.y)
        })
        resolve(imgDatas)
      })
    })
  }
  /**
   * 绘制一张图片
   * @param image
   * @param x
   * @param y
   * @param width
   * @param height
   */
  drawImage(image, x, y, width, height) {
    const that = this
    width = width || image.width
    height = height || image.height
    that.ctx.drawImage(image, x, y, width, height)
  }
  /**
   * 绘制多个点
   * @param pointsData,[{type: 'circle', size: 4, x: 100, y: 100, icon: ''}]
   */
  drawPoints(pointsData = []) {
    const that = this
    return new Promise(resolve => {
      let promises = []
      pointsData.forEach(pointData => {
        that.ctx.beginPath()
        that.ctx.save()
        that.ctx.fillStyle = pointData.color || 'rgba(255, 0, 0, 1)'
        const type = pointData.type || 'circle'
        const size = pointData.size || 4
        let {x, y} = pointData
        pointData.x = x
        pointData.y = y
        switch (type) {
          case "rect": {
            x -= size
            y -= size
            that.ctx.fillRect(x, y, size * 2, size * 2)
            promises.push(Promise.resolve(pointData))
            break
          }
          case "circle": {
            that.ctx.arc(x, y, size, 0, Math.PI * 2)
            that.ctx.fill()
            promises.push(Promise.resolve(pointData))
            break
          }
          case "marker": {
            promises.push(new Promise(resolve1 => {
              loadImage(pointData.icon).then(img => {
                const w = img.width * pointData.size
                const h = img.height * pointData.size
                x -= w / 2
                y -= h / 2
                that.drawImage(img, x, y, w, h)
                resolve(pointData)
              })
            }))
            break
          }
        }
        that.ctx.restore()
      })
      Promise.all(promises).then(res => {
        resolve({
          code: '200'
        })
      })
    })
  }
  /**
   * 绘制线
   * @param linesData [{}]
   * @return {Promise<unknown>}
   */
  drawLines(linesData) {
    const that = this
    return new Promise(resolve => {
      linesData.forEach(lineData => {
        that.ctx.beginPath()
        that.ctx.save()
        that.ctx.strokeStyle = lineData.color || 'red'
        that.ctx.lineWidth = lineData.width || 2
        that.ctx.setLineDash(lineData.dasharray || [5, 0]);
        lineData.coords.forEach((coord, index) => {
          const [x, y] = coord
          index === 0 ? that.ctx.moveTo(x, y) : that.ctx.lineTo(x, y)
        })
        that.ctx.stroke()
        that.ctx.restore()
      })
      resolve({
        code: '200'
      })
    })
  }
  /**
   * 绘制多边形
   * @param polygonsData
   * @return {Promise<unknown>}
   */
  drawPolygons(polygonsData) {
    const that = this
    return new Promise(resolve => {
      polygonsData.forEach(polygonData => {
        that.ctx.beginPath()
        that.ctx.save()
        polygonData.coords.forEach((coord, index) => {
          const [x, y] = coord
          index === 0 ? that.ctx.moveTo(x, y) : that.ctx.lineTo(x, y)
        })
        that.ctx.closePath()
        if(polygonData.isFill) {
          that.ctx.fillStyle = polygonData.fillStyle || 'rgba(255, 0, 0,  0.2)'
          that.ctx.fill()
        }
        if(polygonData.isStroke) {
          that.ctx.strokeStyle = polygonData.strokeStyle || 'red'
          that.ctx.lineWidth =  polygonData.lineWidth || 2
          that.ctx.setLineDash(polygonData.lineDash || [5, 0]);
          that.ctx.stroke()
        }
        that.ctx.restore()
      })
      resolve({
        code: '200'
      })
    })
  }
  /**
   * 获取canvas数据
   * @return {string}
   */
  getDataUrl() {
    return this.canvas.toDataURL().replace(/^data:image\/\w+;base64,/, '')
  }
  /**
   * 添加标题
   * @param title
   */
  addTitle(title) {
    this.ctx.save()
    this.ctx.strokeStyle = '#fff'
    this.ctx.lineWidth = 3
    this.ctx.fillStyle = '#fff'
    let x = 20, y = 20, offset = 8
    let h = 32
    this.ctx.font = `bold ${h}px 微软雅黑`
    this.ctx.textAlign = 'left'
    this.ctx.textBaseline = 'top'
    let w = this.ctx.measureText(title).width
    // 外边框
    this.ctx.strokeRect(x, y, offset * 4 + w, offset * 4 + h)
    // 内边框
    this.ctx.strokeRect(x + offset, y + offset, offset * 2 + w, offset * 2 + h)
    // 文字
    this.ctx.fillText(title, x + offset * 2, y + offset * 2)
    this.ctx.restore()
  }
}
module.exports = CanvasUtil

tile.js,切片操作工具,提供了坐标转换的方法、获取范围内的切片的行列范围、地理坐标转换为屏幕坐标等方法。

const proj4 = require('proj4')
const { randomNum } = require('./common')
class TileUtil {
  constructor(tileSize = 256) {
    this.tileSize = tileSize
    this.origin = 20037508.34
    this.resolutions = []
    let resolution = (this.origin * 2) / this.tileSize
    for (let i = 0; i < 23; i++) {
      this.resolutions.push(resolution)
      resolution /= 2
    }
    this.tileUrl = 'https://webst0{domain}.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}'
  }
  /**
   * 4326转3857
   * @param lonlat
   * @return {*}
   */
  fromLonLat(lonlat) {
    return proj4('EPSG:4326', 'EPSG:3857', lonlat)
  }
  /**
   * 3857转4326
   * @param coords
   * @return {*}
   */
  toLonLat(coords) {
    return proj4('EPSG:3857', 'EPSG:4326', coords)
  }
  /**
   * 获取范围内的切片的行列号的范围
   * @param zoom
   * @param extent
   * @return {number[]}
   */
  getTilesInExtent(zoom, extent) {
    extent = this.getExtent(extent)
    const [xmin, ymin, xmax, ymax] = extent
    const res = this.resolutions[zoom] * 256
    const xOrigin = -this.origin, yOrigin = this.origin
    const _xmin = Math.floor((xmin - xOrigin) / res)
    const _xmax = Math.ceil((xmax - xOrigin) / res)
    const _ymin = Math.floor((yOrigin - ymax) / res)
    const _ymax = Math.ceil((yOrigin - ymin) / res)
    return [_xmin, _ymin, _xmax, _ymax]
  }
  /**
   * 获取切片地址
   * @param x
   * @param y
   * @param z
   * @return {string}
   */
  getTileUrl(x, y, z) {
    let url = this.tileUrl.replace(/\{x\}/g, x)
    url = url.replace(/\{y\}/g, y)
    url = url.replace(/\{z\}/g, z)
    return url.replace(/\{domain\}/g, randomNum())
  }
  /**
   * 获取切片大小
   * @return {number}
   */
  getTileSize() {
    return this.tileSize
  }
  /**
   * 地理坐标转换为屏幕坐标
   * @param extent
   * @param zoom
   * @param lonLat
   * @return {*[]}
   */
  project(extent, zoom, lonLat) {
    const [xmin, ymin, xmax, ymax] = this.getTilesInExtent(zoom, extent)
    const res = this.resolutions[zoom]
    const resMap = this.tileSize * res
    const topLeft = [
      resMap * xmin - this.origin,
      this.origin - resMap * ymin
    ]
    const coords = this.fromLonLat(lonLat)
    const x = (coords[0] - topLeft[0]) / res
    const y = (topLeft[1] - coords[1]) / res
    return [x, y]
  }
  /**
   * 处理四至
   * @param extent
   * @return {*[]}
   */
  getExtent(extent) {
    if(Boolean(extent)) {
      const min = this.fromLonLat([extent[0], extent[1]])
      const max = this.fromLonLat([extent[2], extent[3]])
      extent = [...min, ...max]
    } else {
      extent = [-this.origin, -this.origin, this.origin, this.origin]
    }
    return extent
  }
  /**
   * 判断是否在范围内
   * @param extent
   * @param lonLat
   * @return {boolean}
   */
  isInExtent(extent, lonLat) {
    const [xmin, ymin, xmax, ymax] = extent
    const [lon, lat] = lonLat
    return lon >= xmin && lon <= xmax && lat >=ymin && lat <= ymax
  }
}
module.exports = TileUtil

map.js,实现地图导出,会用到前面提到的两个工具类。

const fs = require('fs');
const ora = require('ora'); // loading
const TileUtil = require('./utils/tile')
const CanvasUtil = require('./utils/canvas')
const spinner = ora('tile joint').start()
const tileUtil = new TileUtil()
const z = 5
// const extent = undefined
const extent = [73.4469604492187500,6.3186411857604980,135.0858306884765625,53.5579261779785156]
const [xmin, ymin, xmax, ymax] = tileUtil.getTilesInExtent(z, extent)
const width = (xmax - xmin) * tileUtil.getTileSize()
const height = (ymax - ymin) * tileUtil.getTileSize()
const canvasUtil = new CanvasUtil(width, height)
let urls = []
for(let i = xmin; i < xmax; i++) {
  const x = (i - xmin) * tileUtil.getTileSize()
  for(let j = ymin; j < ymax; j++) {
    const y = (j - ymin) * tileUtil.getTileSize()
    const url = tileUtil.getTileUrl(i, j, z)
    urls.push({
      i, j, x, y, url
    })
  }
}
// 添加点数据
function addCapitals() {
  let geojson = fs.readFileSync('./data/capital.json')
  geojson = JSON.parse(geojson)
  let pointsData = []
  geojson.features.forEach(feature => {
    const coords = feature.geometry.coordinates
    if(!extent || tileUtil.isInExtent(extent, coords)) {
      const [x, y] = tileUtil.project(extent, z, coords)
      const { name } = feature.properties
      if(name === '北京') {
        pointsData.push({type: 'marker', size: 0.35, x, y, icon: './icons/icon-star.png'})
      } else {
        pointsData.push({type: 'rect', size: 4, x, y, color: '#ff0'})
      }
    }
  })
  return canvasUtil.drawPoints(pointsData)
}
// 添加线数据
function addLines() {
  let geojson = fs.readFileSync('./data/boundry.json')
  geojson = JSON.parse(geojson)
  let linesData = []
  geojson.features.forEach(feature => {
    const {type, coordinates} = feature.geometry
    if(type === 'LineString') {
      linesData.push({
        width: 2,
        color: 'rgba(255,0,0,0.8)',
        coords: coordinates.map(coords => {
          return tileUtil.project(extent, z, coords)
        })
      })
    } else {
      coordinates.forEach(_coordinates => {
        linesData.push({
          width: 2,
          color: 'rgba(255,0,0,0.8)',
          coords: _coordinates.map(coords => {
            return tileUtil.project(extent, z, coords)
          })
        })
      })
    }
  })
  return canvasUtil.drawLines(linesData)
}
// 添加面数据
function addPolygons() {
  let geojson = fs.readFileSync('./data/province.geojson')
  geojson = JSON.parse(geojson)
  let polygonsData = []
  geojson.features.forEach(feature => {
    const { name } = feature.properties
    const {type, coordinates} = feature.geometry
    if(type === 'Polygon') {
      const coords = coordinates[0].map(coords => {
        return tileUtil.project(extent, z, coords)
      })
      polygonsData.push({
        isStroke: true,
        isFill: true,
        lineWidth: 1,
        lineDash: [5, 5],
        strokeStyle: 'rgb(240,240,240)',
        fillColor: 'rgba(255, 0, 0,  0.1)',
        coords,
        name
      })
    } else {
      coordinates[0].forEach(_coordinates => {
        const coords = _coordinates.map(coords => {
          return tileUtil.project(extent, z, coords)
        })
        polygonsData.push({
          isStroke: true,
          isFill: true,
          lineWidth: 1,
          lineDash: [5, 5],
          strokeStyle: 'rgb(240,240,240)',
          fillStyle: 'rgba(255, 0, 0,  0.1)',
          coords,
          name
        })
      })
    }
  })
  return canvasUtil.drawPolygons(polygonsData)
}
// 1.拼接切片,2.添加面数据,3.添加点数据,4.添加线数据,5.导出图片
canvasUtil.drawImages(urls).then(() => {
  addPolygons().then((a) => {
    addCapitals().then(() => {
      addLines().then(() => {
        canvasUtil.addTitle('中国省级区划图')
        let base64Data = canvasUtil.getDataUrl()
        let dataBuffer = Buffer.from(base64Data, 'base64')
        fs.writeFileSync(`./result/map${z}.png`, dataBuffer)
        spinner.succeed()
        spinner.color = 'green'
      })
    })
  })
})

本文源代码请扫描下面二维码或直接前往仓库获取。

以上就是Node 切片拼接及地图导出实例详解的详细内容,更多关于Node 切片拼接 地图导出的资料请关注我们其它相关文章!

(0)

相关推荐

  • Node.js使用gm拼装sprite图片

    从设计图切图得到了12个小图标,是按钮的两种状态,然后我就寻思着把他们拼成一张sprite图片. 之前用过gulp的sprite插件,但这次我不想搞的太隆重.拼图我知道有个很好用的命令行工具 GraphicsMagick 及配套的nodejs包gm 首先说下我要拼的图片,我打算将正常状态作为第1行,激活状态作为第2行.这样可以少计算一些background-position. 折腾过程比较痛苦,本来我打算看一下GraphicsMagick与gm的官方文档,结果好多生单词,最后还是放弃了.下面说答

  • AngularJS + Node.js + MongoDB开发的基于高德地图位置的通讯录

    一.闲扯 有一天班长说了,同学们希望我开发一个可以共享位置的通讯录,于是自己简单设计了下功能.包括用户角色.发表微博.共享位置等等.这次也是有点私心的,为了锻炼最近看的angularjs,于是果断选择Node.js + MongoDB + angular.js的方案.当然,开发Node.js的体会越来越深刻.记得,去年leader告诉我说尽量让node的每一个服务只支撑一个业务功能,这样才能更方便的维护.当时特别想把一个Node服务做的特别强大.现在看来leader的做法是对的,我更加倾向于把n

  • node实现封装一个图片拼接插件

    目录 前言 插件效果 1.横向拼接两张图片 2.纵向拼接两张图片 3.批量拼接 3.1 横向拼接长图 3.2 纵向拼接长图 4.自定义拼接矩阵 插件实现 1.单张图片拼接 2.量拼接 3.自定义矩阵拼接 插件使用 1.依赖引入 2.横向拼接两张图片 2.1参数说明 2.2示例代码 2.纵向拼接两张图片 3.1参数说明 3.2示例代码 4.批量拼接 4.1参数说明 4.2示例代码 5.自定义拼接矩阵 5.1参数说明 5.2示例代码 前言 平时我们拼接图片的时候一般都要通过ps或者其他图片处理工具来

  • nodejs根据ip数组在百度地图中进行定位

    利用node接收到的ip数组组装url后对百度地图api发送请求并返回请求结果数组给前端 1. 前端代码部分(jquery) 重要步骤: 1> 引用百度地图 2> 实例化百度地图,添加相关缩放控件,设置主图 3> 重写http请求,设置contentType并对请求数据作转化为json对象处理 4> 发送请求数据,将请求结果转化成可处理对象 5> 根据响应结果的经纬度进行定位,添加默认覆盖物和iplabel <script type="text/javascr

  • NodeJs生成sitemap站点地图的方法示例

    如果博客是使用Hexo管理的,sitemap可以使用插件来生成.但对于一个内容管理网站,后端可能是express.koa之类的框架,这时sitemap就需要自己来生成了 什么是sitemap Sitemap可方便网站管理员通知搜索引擎他们网站上有哪些可供抓取的网页.最简单的Sitemap形式,就是XML文件,在其中列出网站中的网址以及关于每个网址的其他元数据(上次更新的时间.更改的频率以及相对于网站上其他网址的重要程度为何等),以便搜索引擎可以更加智能地抓取网站. sitemap结构 <url>

  • Node 切片拼接及地图导出实例详解

    目录 概述 实现效果 实现 1. 初始化工程 2. 编写工具类 概述 本文讲述在node中,使用canvas实现根据出图范围和级别,拼接瓦片并叠加geojson矢量数据,并导出成图片. 实现效果 实现 1. 初始化工程 通过命令npm init -y初始化工程并添加对应的依赖,最终的package.json文件如下: { "name": "map", "version": "1.0.0", "description&

  • 微信小程序 开发MAP(地图)实例详解

    微信小程序 开发MAP(地图)实例详解 在创建MAP(地图)前,请各位小伙伴们认真的去了解微信小程序开发的说明. https://mp.weixin.qq.com/debug/wxadoc/dev/component/map.html#map 了解完MAP(地图)里的属性之后,接下来我们就来创建一个简单的MAP(地图)控件. 第一步:肯定是创建项目.起项目名.项目地址 PS:我这里以index的文件为名 第二步:我们来写 index.wxml 文件的代码 WXML文件代码: <map id=&quo

  • 微信小程序 地图map实例详解

    微信小程序 地图map实例详解 wxml: class="button" bindtap="getlocation" style="margin-top:30px" markers="{{markers}}">定位 longitude="{{longitude}}" latitude="{{latitude}}" markers="{{markers}}" co

  • Python pyecharts实现绘制中国地图的实例详解

    目录 实例演示 1.pyecharts 1.9.1 版本安装与数据准备 2.添加数据项,默认中国地图显示 常用配置项及参数解析 1.设置是否默认选中 2.设置地图颜色类型是否分段显示 3.缩放平移配置 4.启用和关闭图形标记 5.关闭标签名称显示 6.颜色设置:标签颜色.区域颜色.边框颜色 实例演示 先给大家看下效果图哈. 1.pyecharts 1.9.1 版本安装与数据准备 首先需要安装 pyecharts 库,直接 pip install pyecharts 就好了. 新版本的话不需要单独

  • 微信小程序JS加载esmap地图的实例详解

    一.在微信小程序里显示室内三维地图 需要满足的两个条件 调用ESMap室内地图需要用到小程序web-view组件,想要通过 web-view 调用ESMap室内地图需要满足以下 2 个条件: 1. 小程序是企业主体,微信 web-view 组件不对个人类型的小程序开放. 2. 您需要有一个自己的域名,在嵌入网页的时候需要在微信后台验证域名(只有自己域名下的网页才能被正确地显示哦,不能随便找一个公开链接). 二.具体实现步骤 1.域名验证: 由于微信平台的规定,web-view 指向的地址,必须是

  • Oracle数据泵的导入与导出实例详解

    前言 今天王子要分享的内容是关于Oracle的一个实战内容,Oracle的数据泵. 网上有很多关于此的内容,但很多都是复制粘贴别人的,导致很多小伙伴想要使用的时候不能直接上手,所以这篇文章一定能让你更清晰的理解数据泵. 开始之前王子先介绍一下自己的环境,这里使用的是比较常用的WIN10系统,Oracle数据库也是安装在本机上的,环境比较简单. 数据泵的导入 导入的数据文件可能是别人导出给你的,也可能是你自己导出的,王子这里就是别人导出的,文件名字是YD.DMP. 在进行操作之前,一定要问清楚表空

  • Java字符串拼接的优雅方式实例详解

    目录 背景 String底层原理 拼接的方法 经典但有时不优雅的 + 优点 缺点 业务一 万能的StringBuilder 线程安全的StringBuffer 灵活的String.format() 有点绿色的concat JDK1.8优雅写法 经典的Guava 总结 背景 字符串拼接不管是在业务上,还是写算法时都会频繁使用到.对于Java来说,字符串拼接有着很多种方式,他们之间的区别是什么,对应不同的业务哪种更好用呢. String底层原理 在讨论字符串拼接时,首先需要知道String的底层原理

  • jsp按格式导出doc文件实例详解

    jsp按格式导出doc文件实例详解 原理:doc文件其实可以保存为xml文件,该xml文件用字符串表示了doc文件的表现形式,我们只需要用Java将那些要填的内容替换掉然后下载给客户就行了. 1.首先是按照你的文档填写好数据. 2.将文档另存为xml文件,然后编辑该xml文件,将填好的内容用某种格式替换,如:将名字张三替换成${name} 3.读取文件,将文件中的${name}替换成真正的名字. 4.下载. 接下来看代码: 首先是那个转换类 package com.my.util; import

  • C++ 中动态链接库--导入和导出的实例详解

    C++ 中动态链接库--导入和导出的实例详解 __declspec(dllexport)和__declspec(dllimport): __declspec(dllexport):编译器看到一个变量.函数或者C++类被它修饰,那么它就知道应该在生成的DLL 模块中导出该变量.函数或C++类. __declspec(dllimport):编译器看到一个变量.函数或者C++类被它修饰,那么它就知道可执行文件或DLL的源文件需要从其它DLL模块中导入一些变量和函数. DLL的导入段: 构建可执行模块时

  • Linux使用Node.js建立访问静态网页的服务实例详解

    Linux使用Node.js建立访问静态网页的服务实例详解 一.安装node.js运行所需要的环境,:http://www.jb51.net/article/79536.htm 二.创建node目录(/node/www),并在目录下创建node.js服务文件server.js var http = require('http'); var fs = require('fs');//引入文件读取模块 var documentRoot = '/node/www';//需要访问的文件的存放目录 var

随机推荐