Let’s make a feature editor! 让我们做一个功能编辑器吧!
在这个模块中,我们将要创建一个使用矢量数据的基础的编辑器。我们的目标是用户可以导入数据,画出新的特征,修改现有特征,并导出结果。我们在这个模块中将要使用GeoJSON的数据,不过Openlayers可以支持其它很多种类型的数据,感兴趣的话可以去了解一下。
Rendering GeoJSON 渲染GeoJSON数据
在开始编辑前,我们将要看一下使用矢量源和图层的基本特征渲染。workshop的data文件夹下包含有一个countries.json的GeoJSON文件,我们将要加载这个文件的数据并将其渲染到地图上。
首先编辑index.html文件,以渲染一个全屏的地图:
<!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>
现在我们将要导入三个重要的元素以处理矢量数据:
- 一种用于读取和写入序列化数据的格式 (在这个例子里面是GeoJSON)
- 用于获取数据和管理要素空间索引的矢量源
- 用于在地图上渲染要素的矢量图层
在main.js种加载并渲染那个包含GeoJSON特征的数据:
import 'ol/ol.css';
// GeoJSON格式
import GeoJSON from 'ol/format/GeoJSON';
import {Map, View} from 'ol';
// 展示数据的图层
import VectorLayer from 'ol/layer/Vector';
// 获取数据
import VectorSource from 'ol/source/Vector';
import View from 'ol/View';
// 在Map实例的layers属性中使用Vector的layer并指定数据源
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这个包来实现这个需求。这个包已经在workshop的依赖中安装过了,如果要手动安装可以执行:
npm install ol-hashed在main.js种添加这个依赖包:
import sync from 'ol-hashed';现在我们需要将我们的地图分配给一个变量(命名为map),以便我们可以将该变量传递给sync函数:
const map = new Map({
使用方式是用sync()方法对map实例进行调用:
sync(map);这样,在刷新页面后也会停留在上一次访问的位置。
Drag and drop 抓和放
在我们的功能编辑器中,我们想要用户可以导入自己的数据并进行编辑.我们将要使用DragAndDrop进行交互.和以前一样,我们将继续使用 GeoJSON 格式来解析特征,但交互配置可以为任意数量的特征格式使用。
我们将在本练习中将我们的地图传递给许多其他组件,因此请确保您已将地图分配给名为 map 的变量:
const map = new Map({
在main.js中引用一个交互类DragAndDrop:
import DragAndDrop from 'ol/interaction/DragAndDrop';接下来我们将要创建一个没有初始数组的矢量源.与之前从其它的位置加载数据不同,这个源将存储用户拖放到地图上的特征.const source = new VectorSource();
现在删掉原先map中的layers列表,用上面的矢量源创建新的图层,并添加到地图中:
const layer = new VectorLayer({
source: source
});
map.addLayer(layer);
最后,我们创建一个拖放的交互,将其配置为与我们的矢量源一起使用,并将其放到地图中:
map.addInteraction(new DragAndDrop({
source: source,
formatConstructors: [GeoJSON]
}));当拖动data文件夹下的country.json文件到浏览器页面时,就会显示出上文中的地图。
Modifying features 修改特征
现在我们已经有让用户加载数据到编辑器的方法了,现在我们想让他们编辑特征.我们将使用Modify交互来实现这个功能,将其配置为修改矢量源上的特征。
首先在main.js引用Modify交互类:
import Modify from 'ol/interaction/Modify';接下来,创建一个连接到矢量源的新交互并将其添加到地图(在 main.js 的底部):
map.addInteraction(new Modify({
source: source
}));这样就可以在地图进行修改(在边上进行拖拽可以修改图形),按住alt点击一个顶点可以删除之。
Drawing new features 画新的特征
现在我们的特征编辑器已经可以用于加载数据和修改特征了,下一步,我们将加入一个Draw交互类以使得用户可以在地图上画新的特征并将其加入到源中.
首先在main.js中引入Draw交互类:
import Draw from 'ol/interaction/Draw';然后,创建一个绘制交互,配置为绘制多边形并将它们添加到我们的矢量源中:
map.addInteraction(new Draw({
type: 'Polygon',
source: source
}));
绘制交互中的type字段控制了所绘制的何种几何图形,它的取值可以是任意一种GeoJSON的几何图形类型.请注意,我们还可以导入 GeometryType 枚举(从 ‘ol/geom/GeometryType’ 导入 GeometryType;)并使用 GeometryType.POLYGON 代替上面的 ‘Polygon’ 字符串:
import GeometryType from 'ol/geom/GeometryType';
map.addInteraction(new Draw({
type: GeometryType.POLYGON,
source: source
}));
使用时,双击可以开始绘制,想要停止绘制,必须让终点连接到起点以形成闭合的图形。
Snapping 抓取
你可能已经注意到,绘制与现有特征不一致的特征很容易。 此外,在修改要素时,我们可以打破拓扑——在之前相邻的多边形之间添加一个空隙。 捕捉交互可用于帮助在绘制和编辑要素时保留拓扑。
首先,将 Snap 交互类导入 main.js:
import Snap from 'ol/interaction/Snap';与其他编辑交互一样,我们将配置捕捉交互以使用我们的矢量源并将其添加到地图中:
map.addInteraction(new Snap({
source: source
}));
随着绘制、修改和捕捉交互都处于活动状态,我们可以在保持拓扑结构的同时编辑数据。
Downloading features 下载特征结果
上传数据并编辑后,我们想让我们的用户下载结果。 为此,我们将我们的特征数据序列化为 GeoJSON 并创建一个 元素,该元素具有触发浏览器文件保存对话框的下载属性。 同时,我们将在地图上添加一个按钮,让用户清除现有要素并重新开始。
首先,我们需要一些按钮的标签.在index.html的map-container下面添加这些元素:
<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() 方法。 我们希望单击“清除”按钮来调用该方法,因此我们将在 main.js 中添加一个用于单击的侦听器:
const clear = document.getElementById('clear');
clear.addEventListener('click', function() {
source.clear();
});为了序列化我们的特征数据以供下载,我们将使用 GeoJSON 格式。 由于我们希望“下载”按钮在编辑过程中随时可用,我们将序列化源中每个更改事件的功能,并为锚元素的 href 属性构造一个数据 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;
});Making it look nice 让地图好看些
此时,我们有一个具有基本导入、编辑和导出功能的特征编辑器。 但是我们没有花任何时间试图使这些功能看起来不错。 当您在 OpenLayers 中创建矢量图层时,您将获得一组默认样式。 编辑交互(绘制和修改)也带有自己的默认样式。 您可能已经注意到几何图形在编辑过程中如何具有更粗的笔划。 可以通过为矢量图层和编辑交互提供style选项来控制此行为。
首先,我们将导入所需的构造函数:
import {Style, Fill, Stroke} from 'ol/style';Static style 静态的样式
如果我们想给所有特征相同的风格,我们可以像这样配置我们的矢量图层:
const layer = new VectorLayer({
source: source,
style: new Style({
fill: new Fill({
color: 'red'
}),
stroke: new Stroke({
color: 'white'
})
})
});也可以将 style 属性设置为样式数组。 例如,这允许渲染一条套管线(下方的宽笔划和顶部的较窄笔划)。
虽然这里没有很好的理由,但为了这个练习,我们将利用动态样式。
Dynamic 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; // assuming these are created elsewhere
}
});根据这个例子我进行了实践,即按照国家名称,首字母在N之前的用红色,其它的用蓝色:
const style1=new Style({
fill: new Fill({
color: 'red'
}),
stroke: new Stroke({
color: 'white'
})
});
const style2=new Style({
fill: new Fill({
color: 'blue'
}),
stroke: new Stroke({
color: 'white'
})
});
const layer = new VectorLayer({
source: source,
style: function(feature, resolution) {
const name = feature.get('name').toUpperCase();
return name < "N" || name == "TAIWAN" ? style1 : style2;
}
});Styling based on geometry area 基于几何面积的样式
为了了解动态样式的工作原理,我们将创建一个样式函数,该函数根据几何区域呈现特征。 为此,我们将使用 npm 上的colormap package 颜色映射包。 我们可以像这样将它添加到我们的依赖项中:
npm install colormap然后导入colormap类和计算球面积的ol/sphere类:
import {getArea} from 'ol/sphere';
import colormap from 'colormap';然后编写一系列根据几何面积大小来决定颜色的方法:
const min = 1e8; // the smallest area
const max = 2e13; // the biggest area
const steps = 50;
// 一个颜色区间,共有50种颜色
const ramp = colormap({
colormap: 'blackbody',
nshades: steps
});
// 将面积收缩在low和high之间
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];
}现在我们可以添加一个函数来创建一个基于几何区域的填充颜色的样式。 将此函数设置为矢量图层的style属性:
const layer = new VectorLayer({
source: source,
style: function(feature) {
if (feature.get("name")=="Taiwan"){
const cnFeature=source.getFeatureById("CHN");
return new Style({
fill: new Fill({
color: getColor(cnFeature)
}),
stroke: new Stroke({
color: 'rgba(255,255,255,0.8)'
})
});
}
return new Style({
fill: new Fill({
color: getColor(feature)
}),
stroke: new Stroke({
color: 'rgba(255,255,255,0.8)'
})
});
}
});