分析 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 上下文
并且可以处理图像数据。
在地图上移动鼠标来查看效果。使用 ↑ 上键 和 ↓ 下键来调整放大镜圆圈的大小。
定义基本结构 在这里应用了必应地图的航拍地图(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 > 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
中设置view
中center
的时候,使用了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(); }); 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.on("postrender" , event => { if (mousePosition) { let pixel = ol.render.getRenderPixel(event, mousePosition); let offset = ol.render.getRenderPixel(event, [ mousePosition[0 ] + radius, mousePosition[1 ] ]); 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 ]; let originX = centerX - half; let originY = centerY - half; 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; 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(); } });
逐像素处理 迭代处理每一个像素的代码如下所示,其实就是使原正方形和新正方形内坐标产生一个对应关系,使原数据 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 > 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(); }); container.addEventListener("mouseout" , () => { mousePosition = null ; map.render(); }); imagery.on("postrender" , event => { if (mousePosition) { let pixel = ol.render.getRenderPixel(event, mousePosition); let offset = ol.render.getRenderPixel(event, [ mousePosition[0] + radius, mousePosition[1] ]); 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 ]; let originX = centerX - half; let originY = centerY - half; 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); context.lineWidth = (3 * half) / radius; context.strokeStyle = "rgba(255,255,255,0.5)" ; context.putImageData(dest, originX, originY); context.stroke(); context.restore(); } }); </script > </body > </html >