五十六、openlayers官网示例Magnify解析——在地图上实现放大镜效果

官网demo地址:

Magnify 

 这篇讲了如何在地图上添加放大镜效果。

首先加载底图

   const layer = new TileLayer({
      source: new StadiaMaps({
        layer: "stamen_terrain_background",
      }),
    });
    const container = document.getElementById("map");

    const map = new Map({
      layers: [layer],
      target: container,
      view: new View({
        center: fromLonLat([-109, 46.5]),
        zoom: 6,
      }),
    });

鼠标移动的时候,调用render方法,触发postrender事件。

container.addEventListener("mousemove", function (event) {
      mousePosition = map.getEventPixel(event);
      map.render();
    });

    container.addEventListener("mouseout", function () {
      mousePosition = null;
      map.render();
    });

postrender事件中可以获取到鼠标移动的位置,实时绘制圆形和放大后的图像。

先用getRenderPixel将地理坐标转换为屏幕坐标,通过勾股定理(直角三角形的两条直角边的平方和等于斜边的平方)算出半径。         

 layer.on("postrender", function (event) {
      if (mousePosition) {
        const pixel = getRenderPixel(event, mousePosition);
        const offset = getRenderPixel(event, [
          mousePosition[0] + radius,
          mousePosition[1],
        ]);
        //计算半径
        const half = Math.sqrt(
          Math.pow(offset[0] - pixel[0], 2) + Math.pow(offset[1] - pixel[1], 2)
        );
      }
    });

获取放大镜范围内所需要的图像。

 //从画布上下文中提取放大镜区域的图像数据:
 const context = event.context;
 const centerX = pixel[0];
 const centerY = pixel[1];
 //正方形左边的顶点
 const originX = centerX - half;
 const originY = centerY - half;
 //计算直径
 const size = Math.round(2 * half + 1);
 const sourceData = context.getImageData(
    originX,
    originY,
    size,
    size
  ).data;
 //获取正方形范围下所有的像素点
 const dest = context.createImageData(size, size);
 const destData = dest.data;

 然后开始创建放大后的图像数据。

// 创建放大后的图像数据
        for (let j = 0; j < size; ++j) {
          for (let i = 0; i < size; ++i) {
            //dI 和 dJ 是相对于中心的偏移
            const dI = i - half;
            const dJ = j - half;
            //点到中心的距离
            const dist = Math.sqrt(dI * dI + dJ * dJ);
            let sourceI = i;
            let sourceJ = j;
            //如果 dist 小于 half,根据偏移和缩放因子计算新的像素位置
            if (dist < half) {
              sourceI = Math.round(half + dI / 2);
              sourceJ = Math.round(half + dJ / 2);
            }
            const destOffset = (j * size + i) * 4;
            const sourceOffset = (sourceJ * size + sourceI) * 4;
            destData[destOffset] = sourceData[sourceOffset];
            destData[destOffset + 1] = sourceData[sourceOffset + 1];
            destData[destOffset + 2] = sourceData[sourceOffset + 2];
            destData[destOffset + 3] = sourceData[sourceOffset + 3];
          }
        }

要看懂这段代码我们需要来好好分析一下。

放大的关键在于 dI / 2dJ / 2 的计算。实际上是将像素距离中心点的偏移量减半,从而将像素“拉近”到中心点。放大镜区域内的像素将被集中在更小的区域内,看起来像是被放大了 。

简单来说,如果我们的圆形下本来有16个像素格子,每个格子展示不同的像素,放大效果就是让两个、三个、或者四个格子都展示同一个像素,那看起来中间部分就会比较大。

我们通过一个简单的 4x4 像素的例子来详细说明这段代码是如何实现放大镜效果的。

假设这是图像的像素点。

1  2  3  4
5  6  7  8
9  10 11 12
13 14 15 16

每个点用坐标表示就是这样:

(0,0) (1,0) (2,0) (3,0)
(0,1) (1,1) (2,1) (3,1)
(0,2) (1,2) (2,2) (3,2)
(0,3) (1,3) (2,3) (3,3)

 for (let j = 0; j < size; ++j) {
   for (let i = 0; i < size; ++i) {
       const dI = i - half;
       const dJ = j - half;
       const dist = Math.sqrt(dI * dI + dJ * dJ);
       let sourceI = i;
       let sourceJ = j;
       if (dist < half) {
          sourceI = Math.round(half + dI / 2);
          sourceJ = Math.round(half + dJ / 2);
        }
      }
   }

假设 half 是 2,我们要遍历 4x4 区域的所有像素,计算每个像素在放大镜效果下的新位置。 

循环第一行 (i = 0, j = 0 到 3)
  • (0, 0)

    • dI = 0 - 2 = -2
    • dJ = 0 - 2 = -2
    • dist = Math.sqrt((-2)^2 + (-2)^2) = Math.sqrt(8) ≈ 2.83
    • 因为 dist > 2,所以 sourceI = 0sourceJ = 0
    • 拷贝 (0, 0) 位置的像素数据
  • (1, 0)

    • dI = 1 - 2 = -1
    • dJ = 0 - 2 = -2
    • dist = Math.sqrt((-1)^2 + (-2)^2) = Math.sqrt(5) ≈ 2.24
    • 因为 dist > 2,所以 sourceI = 1sourceJ = 0
    • 拷贝 (1, 0) 位置的像素数据
  • (2, 0)

    • dI = 2 - 2 = 0
    • dJ = 0 - 2 = -2
    • dist = Math.sqrt(0^2 + (-2)^2) = Math.sqrt(4) = 2
    • 因为 dist <= 2,所以 sourceI = Math.round(2 + 0 / 2) = 2
    • sourceJ = Math.round(2 + (-2) / 2) = 1
    • 拷贝 (2, 1) 位置的像素数据
  • (3, 0)

    • dI = 3 - 2 = 1
    • dJ = 0 - 2 = -2
    • dist = Math.sqrt(1^2 + (-2)^2) = Math.sqrt(5) ≈ 2.24
    • 因为 dist > 2,所以 sourceI = 3sourceJ = 0
    • 拷贝 (3, 0) 位置的像素数据
循环第二行 (i = 0, j = 1 到 3)
  • (0, 1)

    • dI = 0 - 2 = -2
    • dJ = 1 - 2 = -1
    • dist = Math.sqrt((-2)^2 + (-1)^2) = Math.sqrt(5) ≈ 2.24
    • 因为 dist > 2,所以 sourceI = 0sourceJ = 1
    • 拷贝 (0, 1) 位置的像素数据
  • (1, 1)

    • dI = 1 - 2 = -1
    • dJ = 1 - 2 = -1
    • dist = Math.sqrt((-1)^2 + (-1)^2) = Math.sqrt(2) ≈ 1.41
    • 因为 dist <= 2,所以 sourceI = Math.round(2 + (-1) / 2) = 1.5 ≈ 2
    • sourceJ = Math.round(2 + (-1) / 2) = 1.5 ≈ 2
    • 拷贝 (2, 2) 位置的像素数据
  • (2, 1)

    • dI = 2 - 2 = 0
    • dJ = 1 - 2 = -1
    • dist = Math.sqrt(0^2 + (-1)^2) = Math.sqrt(1) = 1
    • 因为 dist <= 2,所以 sourceI = Math.round(2 + 0 / 2) = 2
    • sourceJ = Math.round(2 + (-1) / 2) = 1.5 ≈ 2
    • 拷贝 (2, 2) 位置的像素数据
  • (3, 1)

    • dI = 3 - 2 = 1
    • dJ = 1 - 2 = -1
    • dist = Math.sqrt(1^2 + (-1)^2) = Math.sqrt(2) ≈ 1.41
    • 因为 dist <= 2,所以 sourceI = Math.round(2 + 1 / 2) = 2.5 ≈ 3
    • sourceJ = Math.round(2 + (-1) / 2) = 1.5 ≈ 2
    • 拷贝 (3, 2) 位置的像素数据

通过这种方式,我们得到新的像素点坐标

(0,0) (1,0) (2,1) (3,0)
(0,1) (2,2) (2,2) (3,2)
(1,2) (2,2) (2,2) (3,2)
(0,3) (2,3) (2,3) (3,3)

跟原坐标对比下:

(0,0) (1,0) (2,0) (3,0)
(0,1) (1,1) (2,1) (3,1)
(0,2) (1,2) (2,2) (3,2)
(0,3) (1,3) (2,3) (3,3)

对比之下发现(2,2)坐标下的像素由原本的一个点展示变成了四个点展示,周围的像素点也发生了一些变化,由此,中间部分就被放大了。

接下里就是把像素点放进新数组中。

const destOffset = (j * size + i) * 4;
const sourceOffset = (sourceJ * size + sourceI) * 4;
destData[destOffset] = sourceData[sourceOffset];  //r
destData[destOffset + 1] = sourceData[sourceOffset + 1];  //g
destData[destOffset + 2] = sourceData[sourceOffset + 2];  //b
destData[destOffset + 3] = sourceData[sourceOffset + 3];  //a

因为图像数据在数组中的存储规则是:

[r,g,b,a,r,g,b,a,r,g,b,a,r,g,b,a...]

因此通过计算得到像素点在数组中的位置destOffset,而sourceOffset 则是计算的偏移后的数组位置。

最后再将放大镜的圆形绘制到地图上就可以了。

  //绘制圆形 
  context.beginPath();
  context.arc(centerX, centerY, half, 0, 2 * Math.PI);
  context.lineWidth = (3 * half) / radius;
  context.strokeStyle = "rgba(255,255,255,0.5)";
  context.putImageData(dest, originX, originY);
  context.stroke();
  context.restore();

完整代码:

<template>
  <div class="box">
    <h1>Magnify</h1>
    <div id="map" class="map"></div>
  </div>
</template>

<script>
import Map from "ol/Map.js";
import TileLayer from "ol/layer/Tile.js";
import View from "ol/View.js";
import XYZ from "ol/source/XYZ.js";
import { fromLonLat } from "ol/proj.js";
import { getRenderPixel } from "ol/render.js";
import StadiaMaps from "ol/source/StadiaMaps.js";
export default {
  name: "",
  components: {},
  data() {
    return {
      map: null,
    };
  },
  computed: {},
  created() {},
  mounted() {
    const layer = new TileLayer({
      source: new StadiaMaps({
        layer: "stamen_terrain_background",
      }),
    });
    const container = document.getElementById("map");

    const map = new Map({
      layers: [layer],
      target: container,
      view: new View({
        center: fromLonLat([-109, 46.5]),
        zoom: 6,
      }),
    });

    let radius = 75;
    document.addEventListener("keydown", function (evt) {
      if (evt.key === "ArrowUp") {
        radius = Math.min(radius + 5, 150);
        map.render();
        evt.preventDefault();
      } else if (evt.key === "ArrowDown") {
        radius = Math.max(radius - 5, 25);
        map.render();
        evt.preventDefault();
      }
    });

    // get the pixel position with every move
    let mousePosition = null;

    container.addEventListener("mousemove", function (event) {
      mousePosition = map.getEventPixel(event);
      map.render();
    });

    container.addEventListener("mouseout", function () {
      mousePosition = null;
      map.render();
    });

    layer.on("postrender", function (event) {
      if (mousePosition) {
        const pixel = getRenderPixel(event, mousePosition);
        const offset = getRenderPixel(event, [
          mousePosition[0] + radius,
          mousePosition[1],
        ]);
        //计算半径
        const half = Math.sqrt(
          Math.pow(offset[0] - pixel[0], 2) + Math.pow(offset[1] - pixel[1], 2)
        );

        //从画布上下文中提取放大镜区域的图像数据:
        const context = event.context;
        const centerX = pixel[0];
        const centerY = pixel[1];
        //正方形左边的顶点
        const originX = centerX - half;
        const originY = centerY - half;
        //计算直径
        const size = Math.round(2 * half + 1);
        const sourceData = context.getImageData(
          originX,
          originY,
          size,
          size
        ).data;
        //获取正方形范围下所有的像素点
        const dest = context.createImageData(size, size);
        const destData = dest.data;

        // 创建放大后的图像数据
        for (let j = 0; j < size; ++j) {
          for (let i = 0; i < size; ++i) {
            //dI 和 dJ 是相对于中心的偏移
            const dI = i - half;
            const dJ = j - half;
            //点到中心的距离
            const dist = Math.sqrt(dI * dI + dJ * dJ);
            let sourceI = i;
            let sourceJ = j;
            //如果 dist 小于 half,根据偏移和缩放因子计算新的像素位置
            if (dist < half) {
              sourceI = Math.round(half + dI / 2);
              sourceJ = Math.round(half + dJ / 2);
            }
            const destOffset = (j * size + i) * 4;
            const sourceOffset = (sourceJ * size + sourceI) * 4;
            destData[destOffset] = sourceData[sourceOffset];
            destData[destOffset + 1] = sourceData[sourceOffset + 1];
            destData[destOffset + 2] = sourceData[sourceOffset + 2];
            destData[destOffset + 3] = sourceData[sourceOffset + 3];
          }
        }
        //绘制圆形 
        context.beginPath();
        context.arc(centerX, centerY, half, 0, 2 * Math.PI);
        context.lineWidth = (3 * half) / radius;
        context.strokeStyle = "rgba(255,255,255,0.5)";
        context.putImageData(dest, originX, originY);
        context.stroke();
        context.restore();
      }
    });
  },
  methods: {},
};
</script>

<style lang="scss" scoped>
#map {
  width: 100%;
  height: 500px;
  position: relative;
}
.box {
  height: 100%;
}

</style>



 

  • 12
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值