这一系列翻译自openlayers官网的WorkShop。OL官网提供了多个系列教程供开发者学习参考,其中QuickStart是面向初学者的hello world,Tutorials提供了构建OL应用的一些基础知识,WorkShop(本系列)详细介绍了一些入门向的高阶应用,最后是APIDocs,适合开发时查阅接口。教程中需要下载的资源可以在WorkShop原网站获得链接。
在这一章节,我们将编写一个矢量数据编辑工具。该工具可以供使用者引入数据,绘制新的矢量要素,修改现有矢量要素,以及导出结果。本章教程的样例数据采用的是GeoJSON格式,不过OpenLayers也支持大量其他的矢量数据格式。
渲染GeoJSON
在实现矢量编辑之前,我们先引入矢量数据,使用VectorSource和Layer将其渲染在地图上。教程包含了一个 countries.json GeoJSON文件,我们首先将他加载到地图上。
首先,html页面需要一个用以承载地图的div。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>OpenLayers</title>
<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>
</body>
</html>
现在我们将导入处理矢量数据的三个重要组件:
- 用于读取和写入序列化数据的格式(format)
- 用于获取数据和管理要素空间索引的矢量数据源(source)
- 用于在地图上渲染要素的矢量图层(layer)
JavaScript代码如下
import 'ol/ol.css';
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
})
});
完成后结果应该是下图这样
//
由于我们将大量重新加载页面,如果地图停留在我们重新加载时离开的位置,那就太好了。我们可以引入ol-hashed来完成这项工作。此软件包已作为教程依赖项的一部分安装。如果尚未包含,您可以使用npm install ol-hashed。
然后在js里导入
import sync from 'ol-hashed';
现在我们需要将地图分配给一个变量(命名map
),以便我们可以将该变量传递给sync
函数
const map = new Map({
现在我们可以使用sync同步我们的地图
sync(map);
现在页面重新加载后地图视图应该会保持原样,后退操作也能如期进行。
拖拽加载数据
对于我们的要素编辑工具,我们希望用户能够导入自己的数据进行编辑。我们将使用DragAndDrop添加这种交互。和以前一样,我们将使用 GeoJSON 格式来解析要素,但其他矢量格式也可以进行下面的操作。
我们将在本章节中将地图传递给许多其他组件,因此请确保您已将地图分配给名为map的变量。
首先在js里引入DragAndDrop模块
import DragAndDrop from 'ol/interaction/DragAndDrop';
接下来,我们将创建一个没有初始数据的矢量数据源。该数据源将存储用户拖放到地图上的要素,而不是像前面的示例那样加载外部数据。
const source = new VectorSource();
现在使用新的数据源创建一个矢量图层,将其添加到地图上
const layer = new VectorLayer({
source: source
});
map.addLayer(layer);
最后,我们将创建一个拖放交互,将矢量数据源配置进去,并将其添加到地图中
map.addInteraction(new DragAndDrop({
source: source,
formatConstructors: [GeoJSON]
}));
现在应该可以将GeoJSON 文件拖放到地图上直接加载
编辑要素
现在用户可以将数据拖拽加载到编辑工具中,我们希望用户可以编辑要素。我们将引入Modify这个交互组件,将刚才的矢量数据源配置进去
首先引入Modify
import Modify from 'ol/interaction/Modify';
接下来,为地图对象添加一个连接到矢量数据源的交互
map.addInteraction(new Modify({
source: source
}));
将数据添加到地图后,可以通过拖动要素节点来修改位置,以及Alt+Click来删除节点
绘制新要素
目前我们的要素编辑工具可以加载并编辑要素,接下来添加Draw组件用以绘制新要素并添加到数据源中。
首先在js里引入Draw
import Draw from 'ol/interaction/Draw';
然后为地图对象添加绘制交互,将数据源配置进去
map.addInteraction(new Draw({
type: 'Polygon',
source: source
}));
Draw对象的type属性控制该交互绘制什么类型的几何。该值可以是任何GeoJSON几何类型。这里也可以引入OL的几何类型枚举(import GeometryType from 'ol/geom/GeometryType';),然后使用GeometryType.POLYGON代替上边的polygon字符串。
现在应该可以使用绘制功能在数据源中添加新的要素。
边缘吸附
您可能已经注意到,绘制与现有要素不一致的新要素很容易。此外,在修改要素时,我们可以打破拓扑——在之前相邻的多边形之间添加空隙。
首先引入Snap交互
import Snap from 'ol/interaction/Snap';
和之前的交互一样,添加到地图上
map.addInteraction(new Snap({
source: source
}));
在绘制、修改和吸附交互都激活的情况下,可以在保持拓扑结构的同时编辑要素
下载
上传数据并编辑后,我们想让用户可以下载结果。为此需要将矢量要素序列化为GeoJSON并在html里创建<a>标签让用户下载。同时,在页面加个按钮让用户可以清除现有要素重新绘制。
html里添加下面
<div id="tools">
<a id="clear">Clear</a>
<a id="download" download="features.json">Download</a>
</div>
为上边的按钮添加样式
#tools {
position: absolute;
top: 1rem;
right: 1rem;
}
#tools a {
display: inline-block;
padding: 0.5rem;
background: white;
cursor: pointer;
}
要素的清除可以通过source.clear() 方法,为clear按钮添加监听触发清除函数
const clear = document.getElementById('clear');
clear.addEventListener('click', function() {
source.clear();
});
为了序列化我们的要素数据以供下载,我们将使用GeoJSON格式(format,之前引用的)。这里监听了数据源的变化,随时重写下载的uri
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;
});
修改样式
此时,我们有一个具有基本导入、编辑和导出功能的要素编辑器。但是我们没有花时间让它看起来好看。在 OpenLayers 中创建矢量图层时,会默认添加一组样式。编辑交互(绘制和修改)也有自己的默认样式。下面通过样式(Style)修改默认的图层样式。
首先引入样式(style),填充(fill)和路径(stroke)
import {Style, Fill, Stroke} from 'ol/style';
静态样式
如果想给一个图层里所有要素同样的样式,可以这样配置
const layer = new VectorLayer({
source: source,
style: new Style({
fill: new Fill({
color: 'red'
}),
stroke: new Stroke({
color: 'white'
})
})
});
也可以将style设置为样式数组,形成比如下边一条宽线上边一条窄线这种样式。
动态样式
若想根据要素的属性字段或当前视图分辨率等来配置不同的样式,可以使用样式函数配置矢量图层。这个函数在每个要素的渲染帧都会被调用,所以如果你有很多要素属性并且想要保持良好的渲染性能,那么编写一个高效的函数很重要。
这里示例了根据‘name’属性的开头是‘A-M’还是‘N-Z’来渲染不同的样式
const layer = new VectorLayer({
source: source,
style: function(feature, resolution) {
const name = feature.get('name').toUpperCase();
return name < "N" ? style1 : style2; // 假设这两个style是在别处创建的
}
});
根据几何面积样式化
为了了解动态样式的工作原理,我们将创建一个样式函数,该函数根据几何面积呈现特征。为此,我们将使用npm 上的一个colorMap包,安装(npm install colormap
)后导入js,使用ol/sphere进行球面面积计算
import {getArea} from 'ol/sphere';
import colormap from 'colormap';
下面的函数用以根据几何图形的面积确定颜色
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 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)'
})
});
}
});