简介
ImJoy有一个很有用的插件或独立应用,叫做Kaibu,它可以展示普通的位图、矢量图及vtk、stl等3D格式的数据。
比如如下展示:

其就是位图(png格式)、矢量图(json格式)、3D模型(stl格式)的一个叠加。
Kaibu主要用了两个JS库,一个是OpenLayers,一个是ITK-VTK,前者用于展示矢量图形、普通位图等数据,且对地图的展示异常强大,后者用于展示在医疗及科学计算中常用的3D图像、网格、点集等。
这一篇主要介绍OpenLayers的相关知识。
配置环境
从OpenLayers workshop releases里下载最新的资料包。
安装依赖:
启动:
这会启动一个开发服务器。可以通过
http://localhost:1234
查看一个“欢迎”的弹出窗口,以及
http://localhost:1234/doc/
查看说明文档。
开发入门
这一部分会通过OpenLayers map来创建一个简单的web页面。
在OpenLayers中,一个map是在web页面中被渲染的一系列“层”layers的集合。OpenLayers支持很多种layers:
(1)针对平铺光栅切片数据的Tile layer;
(2)针对位图图像的Image layer;
(3)针对矢量数据的Vector layer;
(4)针对平铺矢量切片数据的Vector tile layer。
除了这些layers,一个map还可以通过一系列的控制(即在map上面的UI元素)和交互(即与map进行交互反馈的部件)来进行配置。
为了创建一个map,需要通过HTML中的元素来创建(如一个<div>
元素),以及一些样式来指定合适的尺寸。
HTML页面
将项目根目录中的index.html
里的内容替换为如下代码(注释写在了代码中):
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
| <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>OpenLayers</title> <style> @import "node_modules/ol/ol.css"; </style> <style> html, body, #map-container { margin: 0; height: 100%; width: 100%; font-family: sans-serif; } </style> </head> <body> <!-- 该div标签是map的渲染容器 --> <div id="map-container"></div> <!-- 引入相关的js代码 --> <script src="./main.js" type="module"></script> </body> </html>
|
具体应用
将项目根目录中的main.js
里的内容替换为如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import OSM from 'ol/source/OSM'; import TileLayer from 'ol/layer/Tile'; import {Map, View} from 'ol'; import {fromLonLat} from 'ol/proj';
new Map({ target: 'map-container', layers: [ new TileLayer({ source: new OSM(), }), ], view: new View({ center: fromLonLat([0, 0]), zoom: 2, }), });
|
效果
此时打开http://localhost:1234
,会看到世界地图:

矢量数据
在这一部分,将会创建一个可以操作矢量数据的编辑器,使得用户可以导入数据、绘制形状、修改已有形状及导出结果等。
本部分会使用GeoJSON数据,不过OpenLayers支持其他大量的矢量数据格式。
渲染GeoJSON
在开发编辑功能之前,先看一下基本的对矢量数据的渲染功能。
在项目的data路径下有一个名为countries.json
的GeoJSON文件,这里将加载该数据并在地图上渲染出来。
首先,编辑一下刚才的index.html
,这里新加一行控制背景颜色的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>OpenLayers</title> <style> @import "node_modules/ol/ol.css"; </style> <style> html, body, #map-container { margin: 0; height: 100%; width: 100%; font-family: sans-serif; background-color: #04041b; } </style> </head> <body> <div id="map-container"></div> <script src="./main.js" type="module"></script> </body> </html>
|
然后将
main.js
中的内容替换为如下代码:
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
| import GeoJSON from 'ol/format/GeoJSON'; import Map from 'ol/Map'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; import View from 'ol/View';
new Map({ target: 'map-container', layers: [ new VectorLayer({ source: new VectorSource({ format: new GeoJSON(), url: './data/countries.json', }), }), ], view: new View({ center: [0, 0], zoom: 2, }), });
|
效果如下:
因为我们会重载这个页面很多次,目前代码下每次重载页面都会回到初始的view方式,即初始的中心点和缩放大小。如果能每次重载都能保持map在相同的位置就能节省很多人力。
此时可以借助ol-hashed
包实现,修改代码如下:
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
| import GeoJSON from 'ol/format/GeoJSON'; import Map from 'ol/Map'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; import View from 'ol/View';
import sync from 'ol-hashed';
const map = new Map({ target: 'map-container', layers: [ new VectorLayer({ source: new VectorSource({ format: new GeoJSON(), url: './data/countries.json', }), }), ], view: new View({ center: [0, 0], zoom: 2, }), });
sync(map);
|
此时,你会发现,将地图移动和缩放到某一特定程度后,下次重新载入代码仍然保持该视角不变。
拖放
对于要实现的编辑器,想要允许用户能够导入自己的数据进行编辑。为此,这里将添加DragAndDrop
功能。
跟以前一样,这里仍只处理GeoJSON这种数据,不过该交互也支持其他类型的数据格式。
修改main.js
为:
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
| import GeoJSON from 'ol/format/GeoJSON'; import Map from 'ol/Map'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; import View from 'ol/View'; import sync from 'ol-hashed';
import DragAndDrop from 'ol/interaction/DragAndDrop';
const map = new Map({ target: 'map-container', view: new View({ center: [0, 0], zoom: 2, }), });
sync(map);
const source = new VectorSource();
const layer = new VectorLayer({ source: source, });
map.addLayer(layer);
map.addInteraction( new DragAndDrop({ source: source, formatConstructors: [GeoJSON], }) );
|
此时就能将GeoJSON文件拖放到该页面上,从而进行渲染。
修改特征
现在可以将数据拖放到编辑器中,下面是添加“修改”功能。
实现方式是使用Modify
交互。
修改main.js
为:
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
| import DragAndDrop from 'ol/interaction/DragAndDrop'; import GeoJSON from 'ol/format/GeoJSON'; import Map from 'ol/Map';
import Modify from 'ol/interaction/Modify'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; import View from 'ol/View'; import sync from 'ol-hashed';
const map = new Map({ target: 'map-container', view: new View({ center: [0, 0], zoom: 2, }), });
sync(map);
const source = new VectorSource();
const layer = new VectorLayer({ source: source, }); map.addLayer(layer);
map.addInteraction( new DragAndDrop({ source: source, formatConstructors: [GeoJSON], }) );
map.addInteraction( new Modify({ source: source, }) );
|
此时就可以拖动顶点来修改特征。也可以使用
Alt+Click
来删除顶点。
绘制特征
接下来添加Draw
交互来使得用户可以绘制新的特征,并添加到数据中。
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
| import DragAndDrop from 'ol/interaction/DragAndDrop';
import Draw from 'ol/interaction/Draw'; import GeoJSON from 'ol/format/GeoJSON'; import Map from 'ol/Map'; import Modify from 'ol/interaction/Modify'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; import View from 'ol/View'; import sync from 'ol-hashed';
const map = new Map({ target: 'map-container', view: new View({ center: [0, 0], zoom: 2, }), });
sync(map);
const source = new VectorSource();
const layer = new VectorLayer({ source: source, }); map.addLayer(layer);
map.addInteraction( new DragAndDrop({ source: source, formatConstructors: [GeoJSON], }) );
map.addInteraction( new Modify({ source: source, }) );
map.addInteraction( new Draw({ type: 'Polygon', source: source, }) );
|
自动吸附
上面的绘制功能添加后,可以发现,当绘制图形时,很难沿着之前的图形进行精确绘制。
此时可以添加snap
功能,当鼠标移动到某个像素一定范围内时,就能自动吸附到该像素,从而完成精确绘制。
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
| import DragAndDrop from 'ol/interaction/DragAndDrop'; import Draw from 'ol/interaction/Draw'; import GeoJSON from 'ol/format/GeoJSON'; import GeometryType from 'ol/geom/GeometryType'; import Map from 'ol/Map'; import Modify from 'ol/interaction/Modify';
import Snap from 'ol/interaction/Snap'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; import View from 'ol/View'; import sync from 'ol-hashed';
const map = new Map({ target: 'map-container', view: new View({ center: [0, 0], zoom: 2, }), });
sync(map);
const source = new VectorSource();
const layer = new VectorLayer({ source: source, }); map.addLayer(layer);
map.addInteraction( new DragAndDrop({ source: source, formatConstructors: [GeoJSON], }) );
map.addInteraction( new Modify({ source: source, }) );
map.addInteraction( new Draw({ source: source, type: GeometryType.POLYGON, }) );
map.addInteraction( new Snap({ source: source, }) );
|
下载特征
当上传数据,且对其编辑后,希望能下载特征。
为了能实现这个功能,这里将特征数据序列化为GeoJSON数据,然后创建一个带download
属性的<a>
元素,这样就能触发浏览器的文件保存对话框。
同时,在map上添加一个按钮,可以使得用户清除现在的特征,重新绘制。
修改index.html
为:
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
| <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>OpenLayers</title> <style> @import "node_modules/ol/ol.css"; </style> <style> html, body, #map-container { margin: 0; height: 100%; width: 100%; font-family: sans-serif; background-color: #04041b; } #tools { position: absolute; top: 1rem; right: 1rem; } #tools a { display: inline-block; padding: 0.5rem; background: white; cursor: pointer; } </style> </head> <body> <div id="map-container"></div> <script src="./main.js" type="module"></script> <!-- 新增一个div元素,里面包含了两个a元素 --> <div id="tools"> <a id="clear">Clear</a> <a id="download" download="features.json">Download</a> </div> </body> </html>
|
修改
main.js
为:
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
| import GeoJSON from 'ol/format/GeoJSON'; import GeometryType from 'ol/geom/GeometryType'; import Map from 'ol/Map'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; import View from 'ol/View'; import sync from 'ol-hashed'; import {DragAndDrop, Draw, Modify, Snap} from 'ol/interaction';
const map = new Map({ target: 'map-container', view: new View({ center: [0, 0], zoom: 2, }), });
sync(map);
const source = new VectorSource();
const layer = new VectorLayer({ source: source, }); map.addLayer(layer);
map.addInteraction( new DragAndDrop({ source: source, formatConstructors: [GeoJSON], }) );
map.addInteraction( new Modify({ source: source, }) );
map.addInteraction( new Draw({ source: source, type: GeometryType.POLYGON, }) );
map.addInteraction( new Snap({ source: source, }) );
const clear = document.getElementById('clear');
clear.addEventListener('click', function () { source.clear(); });
const format = new GeoJSON({featureProjection: 'EPSG:3857'});
const download = document.getElementById('download');
source.on('change', function () { const features = source.getFeatures(); const json = format.writeFeatures(features); download.href = 'data:application/json;charset=utf-8,' + encodeURIComponent(json); });
|
效果如下:
配置绘图样式
前面的编辑功能都是使用了默认样式,这里增加更多的属性来使得编辑功能更加强大,比如设置画笔宽度、设置填充颜色等。
静态样式
如果单纯想将样式都调成一个模样,那么可以直接简单地将样式固定即可,如下面代码:
1 2 3 4 5 6 7 8 9 10 11
| const layer = new VectorLayer({ source: source, style: new Style({ fill: new Fill({ color: 'red' }), stroke: new Stroke({ color: 'white' }) }) });
|
即都填充成红色,笔画都是白色。
动态样式
更多情况下,动态样式使用得更多,即按照一定的规则自动设置样式。
如下面:
1 2 3 4 5 6
| constlayer = newVectorLayer({ source: source, style: function(feature, resolution) { constname = feature.get('name').toUpperCase(); returnname < "N"? style1 : style2; });
|
就是根据feature的name来设置样式,如果是
A-M
,就用style1,如果是
N-Z
,则使用style2。
所以设定好规则非常重要。
下面将展示如何根据几何区域设定样式。
修改
main.js
为:
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
| import DragAndDrop from 'ol/interaction/DragAndDrop'; import Draw from 'ol/interaction/Draw'; import GeoJSON from 'ol/format/GeoJSON'; import GeometryType from 'ol/geom/GeometryType'; import Map from 'ol/Map'; import Modify from 'ol/interaction/Modify'; import Snap from 'ol/interaction/Snap'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; import View from 'ol/View'; import sync from 'ol-hashed';
import {Fill, Stroke, Style} from 'ol/style';
import colormap from 'colormap';
import {getArea} from 'ol/sphere';
const min = 1e8; const max = 2e13; const steps = 50; const ramp = colormap({ colormap: 'blackbody', nshades: steps, });
function clamp(value, low, high) { return Math.max(low, Math.min(value, high)); }
function getColor(feature) { const area = getArea(feature.getGeometry()); const f = Math.pow(clamp((area - min) / (max - min), 0, 1), 1 / 2); const index = Math.round(f * (steps - 1)); return ramp[index]; }
const map = new Map({ target: 'map-container', view: new View({ center: [0, 0], zoom: 2, }), });
sync(map);
const source = new VectorSource();
const layer = new VectorLayer({ source: source, style: function (feature) { return new Style({ fill: new Fill({ color: getColor(feature), }), stroke: new Stroke({ color: 'rgba(255,255,255,0.8)', }), }); }, });
map.addLayer(layer);
map.addInteraction( new DragAndDrop({ source: source, formatConstructors: [GeoJSON], }) );
map.addInteraction( new Modify({ source: source, }) );
map.addInteraction( new Draw({ source: source, type: GeometryType.POLYGON, }) );
map.addInteraction( new Snap({ source: source, }) );
const clear = document.getElementById('clear'); clear.addEventListener('click', function () { source.clear(); });
const format = new GeoJSON({featureProjection: 'EPSG:3857'}); const download = document.getElementById('download'); source.on('change', function () { const features = source.getFeatures(); const json = format.writeFeatures(features); download.href = 'data:text/json;charset=utf-8,' + json; });
|
效果如下:
移动端地图和数据集成
这一部分将创建一个移动端的地图来展示用户的GPS位置和朝向。该项目的目的是为了展示怎样将OpenLayers与浏览器的API及第三方工具进行集成。
具体地,仅使用几行代码即可调用浏览器的关于地理位置的API,从而得到GPS位置,以及使用kompas库通过设备的陀螺仪获得朝向。然后,通过使用Vector Layer,就能很轻易地在地图上显示结果。
因为这一部分需要移动端的配合,不再具体分析。
更多用法留坑待填。