0%

OpenLayers6实例分析:Magnify(放大镜)

分析 Magnify 这个 demo,官网介绍是:

This example makes use of the postrender event listener to oversample imagery in a circle around the pointer location. Listeners for this event have access to the Canvas context and can manipulate image data.
Move around the map to see the effect. Use the ↑ up and ↓ down arrow keys to the magnified circle size.

本示例使用的是 postrender 事件监听器,在图层中对鼠标位置处的圆圈进行过采样实现的,这个事件的监听器可以访问 Canvas 上下文并且可以处理图像数据。

在地图上移动鼠标来查看效果。使用 ↑ 上键 和 ↓ 下键来调整放大镜圆圈的大小。

Magnify

定义基本结构

在这里应用了必应地图的航拍地图(Aerial),必应地图的source定义多了一个必填的key,这个key是用 Bing 地图的开发者账号来申请的,申请链接:http://www.bingmapsportal.com/。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.1.1/css/ol.css"
type="text/css"
/>
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.1.1/build/ol.js"></script>
<title>Magnify(放大镜)</title>
<style>
html,
body,
.map {
height: 100%;
width: 100%;
}
</style>
</head>

<body>
<div id="map" class="map"></div>
<script>
// 必应地图开发者key
let key = "Your Bing Maps Key from http://www.bingmapsportal.com/ here";
// 必应地形图层
let imagery = new ol.layer.Tile({
source: new ol.source.BingMaps({ key: key, imagerySet: "Aerial" })
});

let container = document.getElementById("map");

let map = new ol.Map({
layers: [imagery],
target: container,
view: new ol.View({
center: ol.proj.fromLonLat([-109, 46.5]),
zoom: 6
})
});
</script>
</body>
</html>

map中设置viewcenter的时候,使用了ol.proj.fromLonLat,如下图所示,可以把经纬度转化为指定坐标系下的坐标,默认为EPSG:3857坐标系。

监听鼠标和键盘事件

修改放大镜大小

首先给放大镜设置一个初始大小 75px,然后监听键盘按下(keydown)事件,同时通过被按下键which属性,也就是keyCode或者charCode来判断是哪一个键(which是一个已经从 web 标准中弃用的事件属性,推荐使用key或者code代替)。

在计算radius(半径)的时候,同时给radius限制了最大值和最小值。在修改了radius后重新渲染地图并阻止按键的默认事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 键盘事件,控制圆圈的放大和缩小
let radius = 75;
document.addEventListener("keydown", evt => {
// 当按↑键时放大圆圈
if (evt.which === 38) {
radius = Math.min(radius + 5, 150);
// 重新渲染地图并阻止默认事件
map.render();
evt.preventDefault();
// 当按↓键时缩小圆圈
} else if (evt.which === 40) {
radius = Math.max(radius - 5, 25);
map.render();
evt.preventDefault();
}
});

保存鼠标位置

因为需要让放大镜跟随鼠标移动,所以需要监听鼠标位置的变化。如代码所示,监听mousemove,同时利用getEventPixel来获取浏览器事件相对于视区的像素位置,也就是鼠标相对于地图的位置,最后再渲染地图。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 鼠标像素坐标
let mousePosition = null;
// 鼠标事件,鼠标移动的时候保存鼠标像素坐标
container.addEventListener("mousemove", event => {
mousePosition = map.getEventPixel(event);
// 渲染地图
map.render();
});
// 鼠标事件,鼠标移出地图时将mousePosition置空
container.addEventListener("mouseout", () => {
mousePosition = null;
map.render();
});

绘制放大镜

准备工作

如代码所示,首先给图层添加一个 postrender 事件,当图层渲染完毕时触发,此时若 mousePosition 有值,也就是鼠标在地图上时,执行放大镜逻辑。

使用ol.render.getRenderPixel()函数,这个函数的功能是把传入的位置转换为 event 中的 canvas 上下文的对应位置,所以 pixel 是鼠标在 canvas 上下文中的位置,offset 是鼠标在 x 方向加上半径后在 canvas 上下文中的位置,half 就是两个位置之间的长度。

接下来获取 canvas 上下文,以鼠标为中心绘制一个正方形,[centerX, centerY]就是正方形的中心坐标,[originX, originY]是正方形左上角的坐标,size是正方形的取整边长。使用getImageData(originX, originY, size, size).data获取这个正方形的图像数据,存放到 sourceData 中,同样使用createImageData(size, size)新建一个边长为 size 的ImageData对象,对象数据存放到 destData 中。

接下来就是对每一个像素进行迭代处理。迭代处理完成后就开始进行 canvas 上下文的绘制,设置 stroke 的颜色和宽度,并使用putImageData(dest, originX, originY)destData 里的数据绘制到正方形中,这样就实现了放大镜。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 给imagery添加一个postrender事件
imagery.on("postrender", event => {
// 当mousePosition有值的时候才执行,所以鼠标移出地图后放大镜就消失
if (mousePosition) {
// 把CSS坐标转化为event事件的canvas context的坐标
let pixel = ol.render.getRenderPixel(event, mousePosition);
// offset是x方向加上半径以后的context坐标
let offset = ol.render.getRenderPixel(event, [
mousePosition[0] + radius,
mousePosition[1]
]);
// 计算出在context中的半径
let half = Math.sqrt(
Math.pow(offset[0] - pixel[0], 2) + Math.pow(offset[1] - pixel[1], 2)
);
let context = event.context;
let centerX = pixel[0];
let centerY = pixel[1];
// 正方形左上角坐标[originX, originY]
let originX = centerX - half;
let originY = centerY - half;
// 在context中的边长,加1是为了防止出现bug
let size = Math.round(2 * half + 1);
// sourceData存放原正方形的数据,destData存放新正方形的数据
let sourceData = context.getImageData(originX, originY, size, size).data;
let dest = context.createImageData(size, size);
let destData = dest.data;

// 迭代每一个像素
// ......
// ......
// ......
// 迭代每一个像素

// 开始绘制
context.beginPath();
// 画圆圈
context.arc(centerX, centerY, half, 0, 2 * Math.PI);
// stroke的样式
context.lineWidth = (3 * half) / radius;
context.strokeStyle = "rgba(255,255,255,0.5)";
// 把生成的dest对象绘制到context中
context.putImageData(dest, originX, originY);
context.stroke();
context.restore();
}
});

逐像素处理

迭代处理每一个像素的代码如下所示,其实就是使原正方形和新正方形内坐标产生一个对应关系,使原数据 sourceOffset 和新数据 destOffset 位置处的数据一致,产生一个过采样的放大效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 迭代每一个像素
for (let j = 0; j < size; ++j) {
for (let i = 0; i < size; ++i) {
//
let dI = i - half;
let dJ = j - half;
let 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);
}
let destOffset = (j * size + i) * 4;
let 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];
}
}

全部代码

全部代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.1.1/css/ol.css"
type="text/css"
/>
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.1.1/build/ol.js"></script>
<title>Magnify(放大镜)</title>
<style>
html,
body,
.map {
height: 100%;
width: 100%;
}
</style>
</head>

<body>
<div id="map" class="map"></div>
<script>
// 必应地图开发者key
let key = "Your Bing Maps Key from http://www.bingmapsportal.com/ here";
// 必应地形图层
let imagery = new ol.layer.Tile({
source: new ol.source.BingMaps({ key: key, imagerySet: "Aerial" })
});

let container = document.getElementById("map");

let map = new ol.Map({
layers: [imagery],
target: container,
view: new ol.View({
center: ol.proj.fromLonLat([-109, 46.5]),
zoom: 6
})
});
// 键盘事件,控制圆圈的放大和缩小
let radius = 75;
document.addEventListener("keydown", evt => {
// 当按↑键时放大圆圈
if (evt.which === 38) {
radius = Math.min(radius + 5, 150);
// 重新渲染地图并阻止默认事件
map.render();
evt.preventDefault();
// 当按↓键时缩小圆圈
} else if (evt.which === 40) {
radius = Math.max(radius - 5, 25);
map.render();
evt.preventDefault();
}
});

// 鼠标像素坐标
let mousePosition = null;
// 鼠标事件,鼠标移动的时候保存鼠标像素坐标
container.addEventListener("mousemove", event => {
mousePosition = map.getEventPixel(event);
// 渲染地图
map.render();
});
// 鼠标事件,鼠标移出地图时将mousePosition置空
container.addEventListener("mouseout", () => {
mousePosition = null;
map.render();
});

// 给imagery添加一个postrender事件
imagery.on("postrender", event => {
// 当mousePosition有值的时候才执行,所以鼠标移出地图后放大镜就消失
if (mousePosition) {
// 把CSS坐标转化为event事件的canvas context的坐标
let pixel = ol.render.getRenderPixel(event, mousePosition);
// offset是x方向加上半径以后的context坐标
let offset = ol.render.getRenderPixel(event, [
mousePosition[0] + radius,
mousePosition[1]
]);
// 计算出在context中的半径
let half = Math.sqrt(
Math.pow(offset[0] - pixel[0], 2) +
Math.pow(offset[1] - pixel[1], 2)
);
let context = event.context;
let centerX = pixel[0];
let centerY = pixel[1];
// 正方形左上角坐标[originX, originY]
let originX = centerX - half;
let originY = centerY - half;
// 在context中的边长,加1是为了防止出现bug
let size = Math.round(2 * half + 1);
let sourceData = context.getImageData(originX, originY, size, size)
.data;
let dest = context.createImageData(size, size);
let destData = dest.data;
// 迭代每一个像素
for (let j = 0; j < size; ++j) {
for (let i = 0; i < size; ++i) {
//
let dI = i - half;
let dJ = j - half;
let 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);
}
let destOffset = (j * size + i) * 4;
let 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);
// stroke的样式
context.lineWidth = (3 * half) / radius;
context.strokeStyle = "rgba(255,255,255,0.5)";
// 把生成的dest对象绘制到context中
context.putImageData(dest, originX, originY);
context.stroke();
context.restore();
}
});
</script>
</body>
</html>
👆 全文结束,棒槌时间到 👇