使用Canvas实现室内地图自适应标绘点位

1、背景

在最近做的地图项目中,需要实现室内地图的点位回显,点位绘制,同时在室内地图(缩放)时,点位相对位置不发生改变,由于室内地图没有gis地图数据,用户使用png图片方式上传,综合评估后使用canvas方案实现需求

2 难点

1、canvas宽高自适应图片宽高(图片宽高比固定现实全部图片)

2、canvas平移缩放时点位相对位置计算(平移缩放地图导致位置变化)

3、新增点位坐标最终像素点的计算(平移和缩放后打点导致像素位置变化)

3 需求分析

1、上传的室内地图不规则,宽高比不固定,因此需要动态调整canvas的宽高,在本项目中动态计算scale 来作为缩放比例

scale计算方式:

arduino 复制代码
scale = Math.min(canvas.width / img.width, canvas.height / img.height);

采用最小值,保证图片在canvas中完整显示

2、canvas平移 缩放,点位相对位置不发生改变

点位位置计算方式:

ini 复制代码
   const x = annotation.x * _this.scale +  _this.translateX -  image.naturalWidth / 2;
   const y =   annotation.y * _this.scale +  _this.translateY - image.naturalHeight /2;

其中annotation为点位坐标,scale为缩放比例,translateX为平移参数,naturalWidth 为点位图标宽高

3、室内点位坐标采用经纬度 ,采用像素坐标。

4、实现步骤

1、canvas绘制

2、加载图片

3、调整canvas尺寸

4、回显标注点

5、新建点位

6、处理canvas缩放平移,点位自适应

5 代码实现

1、canvas绘制

javascript 复制代码
 // 初始化 canvas
    initPicture() {
      // 获取canvas元素和上下文
      this.canvas = this.$refs.canvas;
      this.context = this.canvas.getContext('2d');
      // 加载图片
      this.loadImage();
    },

2、加载图片

javascript 复制代码
  // 加载图片
    loadImage() {
      const img = new Image();
      img.src = this.picture;// 图片在线地址
      img.onload = () => {
        this.image = img;
        this.imageWidth = img.naturalWidth;
        this.imageHeight = img.naturalHeight;
        this.adjustCanvasSize(); // 调整canvas尺寸
        this.drawAnnotations(); // 绘制点位
      };
    },

3、调整canvas尺寸

javascript 复制代码
  // 调整canvas尺寸
    adjustCanvasSize() {
      // 1、获取canvas宽高
      const container = document.querySelector('.canvas-container');
      this.canvasWidth = container.clientWidth;
      this.canvasHeight = container.clientHeight;
      // 2  根据  imageWidth imageHeight 获取宽高比 确定缩放sacle值(使用小的那一个)
      this.widthScale = this.canvasWidth / this.imageWidth;
      this.heightScale = this.canvasHeight / this.imageHeight;
      this.sacle =
        this.widthScale < this.heightScale ? this.widthScale : this.heightScale;
      //   3 更改缩放后的图片宽高比
      this.imageWidth = this.imageWidth * this.sacle;
      this.imageHeight = this.imageHeight * this.sacle;

      this.canvas.width = this.canvasWidth;
      this.canvas.height = this.canvasHeight;
      this.drawAnnotations(); // 绘制点位
    },

4、回显标注点 点位坐标计算方式:

(1)、将标注点的 x 和 y 坐标分别乘以缩放比例 _this.scale,得到标注点相对于图片的放大或缩小后的坐标。

(2)、将放大或缩小后的坐标分别加上平移量 _this.translateX 和 _this.translateY,得到标注点相对于画布的坐标。

(3)、将标注点相对于画布的坐标分别减去图片的宽度和高度的一半,得到标注点在画布上的位置。

context.drawImage() 方法绘制图片时,图片的左上角坐标是 (x, y),而图片的中心坐标是 (x + image.width / 2, y + image.height / 2)。因此,为了使标注点居中显示,需要将标注点的坐标减去图片宽度和高度的一半。

javascript 复制代码
    // 绘制标注点
    drawAnnotations() {
        // 清除画布
      if (this.context) {
        this.context.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
        this.context.save(); // 保存当前状态
      }
      // 缩放和平移画布
      this.context.translate(this.translateX, this.translateY);
      this.context.scale(this.scale, this.scale);
      // 绘制图片
      if (this.image) {
        this.context.drawImage(
          this.image,
          0,
          0,
          this.imageWidth,
          this.imageHeight
        );
      }
      // 绘制标注点 annotations(点位列表)
      const _this = this;
      this.annotations.forEach(annotation => {
        const image = new Image();
        let srcObject = _this.point.find(
          item => item.deviceType == annotation.deviceType
        );
        if (srcObject && srcObject.pointLabel) {
          image.src = srcObject.pointLabel;
          image.onload = () => {
            const x = annotation.x * _this.scale +  _this.translateX -  image.naturalWidth / 2;
            const y =   annotation.y * _this.scale +  _this.translateY - image.naturalHeight /2;
            _this.context.drawImage(image, x, y);
          };
        }
      });
      this.context.restore(); // 恢复之前状态
    },

5、新建点位

javascript 复制代码
    // 处理鼠标按下事件
    checkMouseDown(event) {
        // 业务逻辑 是否允许新增点位
      if (this.allowAddPoint) {
        this.addAnnotation(event);
      } else {
        // 下面是点击点位逻辑 不新增点位
        if (event.button === 0) {
          this.isMouseDown = true;
          this.startMousePos.x = event.clientX;
          this.startMousePos.y = event.clientY;
        //   获取点击的像素位置 
          const rect = this.canvas.getBoundingClientRect();
        //    最终像素位置需要通过translateX 平移参数和scale缩放参数,计算出未缩放和未平移的原始坐标
          const pointX =
            (event.clientX - rect.left - this.translateX) / this.scale;
          const pointY =
            (event.clientY - rect.top - this.translateY) / this.scale;
          this.annotations.map(item => {
            item.x = parseInt(item.x);
            item.y = parseInt(item.y);
            //  判断新增的点位是否在原有点位附近,有的话不新增,范围为40
            if (
              item.x <= pointX + 40 &&
              item.x >= pointX - 40 &&
              item.y <= pointY + 40 &&
              item.y >= pointY - 40
            ) {
              this.clickAnnotation = item;
              this.$emit('clickPicturePoint', item);
            }
          });
        }
      }
    },
    // 添加标注点 在地图上显示
    addAnnotation(event) {
      const rect = this.canvas.getBoundingClientRect();
    //   最终像素位置需要通过translateX 平移参数和scale缩放参数,计算出未缩放和未平移的原始坐标
      const x = (event.clientX - rect.left - this.translateX) / this.scale;
      const y = (event.clientY - rect.top - this.translateY) / this.scale;
      this.annotations.push({ x, y });
      this.drawAnnotations();
      const data = {
        x: x,
        y: y
      };
      this.$emit('addPoint', data);
    },

6、 处理鼠标平移 滚动事件

javascript 复制代码
     // 处理鼠标滚轮事件
    handleWheel(event) {
      // 阻止浏览器默认的滚轮事件行为,防止页面滚动。
      event.preventDefault();
      // 获取滚轮事件的方向,向上滚动为正,向下滚动为负。
      const delta = event.wheelDelta ? event.wheelDelta : -event.deltaY;
      // 根据缩放比例调整画布。通过this.scale *= 1.1或this.scale /= 1.1来调整画布的缩放比例。1.1是一个缩放系数,可以根据需要调整。
      if (delta > 0) {
        this.scale *= 1.1;
      } else {
        this.scale /= 1.1;
      }

      this.drawAnnotations();
    }

6 全部代码

javascript 复制代码
<template>
  <div class="canvas-container">
    <canvas
      ref="canvas"
      @mousedown="checkMouseDown"
      @wheel="handleWheel"
    ></canvas>
  </div>
</template>
  <script>
export default {
  props: {
    point: {
      type: Array,
      default: () => {
        reutrn([]);
      }
    }
  },
  data() {
    return {
      canvas: null,
      picture:
        'https://image.cool-de.com/data/attachment/forum/202012/17/112455l11vsszflz1l4a4k.jpg', // 替换成室内地图
      context: null,
      allowAddPoint: false,
      annotations: [], // 标注点数据
      isMouseDown: false,
      startMousePos: { x: 0, y: 0 },
      image: null, // 图片对象
      imageWidth: 0,
      cursorStyle: '',
      imageHeight: 0,
      scale: 1,
      canvasWidth: 0,
      canvasHeight: 0,
      translateX: 0,
      translateY: 0,
      clickAnnotation: null,
      widthScale: 0,
      heightScale: 0
    };
  },
  watch: {
    annotations: {
      handler(newValue, oldValue) {
        this.drawAnnotations();
      }
    }
  },
  mounted() {
    // 监听窗口大小变化,重新调整canvas尺寸
    window.addEventListener('resize', this.adjustCanvasSize);
    this.initPicture();
  },
  beforeDestroy() {
    // 移除窗口大小变化的监听事件
    window.removeEventListener('resize', this.adjustCanvasSize);
  },
  methods: {
    // 初始化 canvas
    initPicture() {
      // 获取canvas元素和上下文
      this.canvas = this.$refs.canvas;
      this.context = this.canvas.getContext('2d');
      // 加载图片
      this.loadImage();
    },
    // 加载图片
    loadImage() {
      const img = new Image();
      img.src = this.picture;
      console.log(img.src);
      img.onload = () => {
        this.image = img;
        this.imageWidth = img.naturalWidth;
        this.imageHeight = img.naturalHeight;
        this.adjustCanvasSize();
        this.drawAnnotations();
      };
    },
    // 调整canvas尺寸
    adjustCanvasSize() {
      // 1、获取canvas宽高
      const container = document.querySelector('.canvas-container');
      this.canvasWidth = container.clientWidth;
      this.canvasHeight = container.clientHeight;
      // 2  根据  imageWidth imageHeight 获取宽高比 确定缩放sacle值(使用小的那一个)
      this.widthScale = this.canvasWidth / this.imageWidth;
      this.heightScale = this.canvasHeight / this.imageHeight;
      this.sacle =
        this.widthScale < this.heightScale ? this.widthScale : this.heightScale;
      //   3 更改缩放后的图片宽高比
      this.imageWidth = this.imageWidth * this.sacle;
      this.imageHeight = this.imageHeight * this.sacle;

      this.canvas.width = this.canvasWidth;
      this.canvas.height = this.canvasHeight;
      this.drawAnnotations();
    },
    // 绘制标注点
    drawAnnotations() {
        // 清除画布
      if (this.context) {
        this.context.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
        this.context.save(); 
      }
      // 缩放和平移画布
      this.context.translate(this.translateX, this.translateY);
      this.context.scale(this.scale, this.scale);
      // 绘制图片
      if (this.image) {
        this.context.drawImage(
          this.image,
          0,
          0,
          this.imageWidth,
          this.imageHeight
        );
      }
      // 绘制标注点 annotations(点位列表)
      const _this = this;
      this.annotations.forEach(annotation => {
        const image = new Image();
        let srcObject = _this.point.find(
          item => item.deviceType == annotation.deviceType
        );
        if (srcObject && srcObject.pointLabel) {
          image.src = srcObject.pointLabel;
          image.onload = () => {
            const x = annotation.x * _this.scale +  _this.translateX -  image.naturalWidth / 2;
            const y =   annotation.y * _this.scale +  _this.translateY - image.naturalHeight / 2;
            _this.context.drawImage(image, x, y);
          };
        }
      });
      this.context.restore();
    },

    // 处理鼠标按下事件
    checkMouseDown(event) {
      console.log('sfsdfds');
      if (this.allowAddPoint) {
        this.addAnnotation(event);
      } else {
        if (event.button === 0) {
          this.isMouseDown = true;
          this.startMousePos.x = event.clientX;
          this.startMousePos.y = event.clientY;
          const rect = this.canvas.getBoundingClientRect();
          const pointX =
            (event.clientX - rect.left - this.translateX) / this.scale;
          const pointY =
            (event.clientY - rect.top - this.translateY) / this.scale;
          this.annotations.map(item => {
            item.x = parseInt(item.x);
            item.y = parseInt(item.y);
            if (
              item.x <= pointX + 40 &&
              item.x >= pointX - 40 &&
              item.y <= pointY + 40 &&
              item.y >= pointY - 40
            ) {
              this.clickAnnotation = item;
              this.$emit('clickPicturePoint', item);
            }
          });
        }
      }
    },
    // 添加标注点
    addAnnotation(event) {
      const rect = this.canvas.getBoundingClientRect();
      const x = (event.clientX - rect.left - this.translateX) / this.scale;
      const y = (event.clientY - rect.top - this.translateY) / this.scale;
      this.annotations.push({ x, y });
      this.drawAnnotations();
      const data = {
        x: x,
        y: y
      };
      this.$emit('addPoint', data);
    },
    // 处理鼠标滚轮事件
    handleWheel(event) {
      // 阻止浏览器默认的滚轮事件行为,防止页面滚动。
      event.preventDefault();

      // 获取滚轮事件的方向,向上滚动为正,向下滚动为负。
      const delta = event.wheelDelta ? event.wheelDelta : -event.deltaY;
      // 根据缩放比例调整画布。通过this.scale *= 1.1或this.scale /= 1.1来调整画布的缩放比例。1.1是一个缩放系数,可以根据需要调整。
      if (delta > 0) {
        this.scale *= 1.1;
      } else {
        this.scale /= 1.1;
      }

      this.drawAnnotations();
    }
  }
};
</script>
  <style>
.canvas-container {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

canvas {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  cursor: move;
}
</style>
  
  

8 总结

通过以上步骤,实现了一个基于Canvas的室内地图自适应标绘点位的功能。用户可以通过鼠标拖拽、滚轮缩放和点击添加标注点,实现室内地图的自适应标绘。同时也提供了点击标注点时的事件处理,方便用户进行后续操作。

需要注意的点:

1 canvas宽高随图片宽高自适应,才能现实全部图片。

2 点位的绘制时需要考虑平移和缩放情况,否则缩放平移后会导致点位显示,标注位置不对。

3 点位的位置数据采用像素点数据存储,不能采用经纬度。

4 最终保存的点位像素点数据也需要计算还原只未缩放平移之前的数据保存,否则会导致下次回显错误。

相关推荐
ze_juejin14 分钟前
Fetch API 详解
前端
用户669820611298223 分钟前
js今日理解 blob和arrayBuffer 二进制数据
前端·javascript
想想肿子会怎么做25 分钟前
Flutter 环境安装
前端·flutter
断竿散人26 分钟前
Node 版本管理工具全指南
前端·node.js
转转技术团队27 分钟前
「快递包裹」视角详解OSI七层模型
前端·面试
1024小神32 分钟前
Ant Design这个日期选择组件最大值最小值的坑
前端·javascript
卸任33 分钟前
Electron自制翻译工具:自动更新
前端·react.js·electron
安禅不必须山水34 分钟前
Express+Vercel+Github部署自己的Mock服务
前端
哈撒Ki37 分钟前
快速入门zod4
前端·node.js
用户游民1 小时前
Flutter 项目热更新方案对比与实现指南
前端