Openlayers
Openlayers
介绍
特点
下面是四个不同的框架的对比:
| 地图框架 | 基本信息 | 优缺点 |
|---|---|---|
| Cesium | WebGL渲染机制、二三维一体化可视化表达;经纬度坐标系、支持球体; | 优点:唯一开源的WebGIS三维引擎;适用于Web强三维应用场景 |
| Mapbox | WebGL渲染机制、二三维一体化;三维方面存在一定争议,有人认为3D有的认为是2.5D;墨卡托坐标系,不支持球体 | 优点:最具美感的专题地图缺点:没有球体运用于互联网场景复杂地理信息表达,追求地图可视化效果 |
| Openlayers | 仅支持二维表达;不限制坐标系; | 优点:二维GIS功能最丰富全面缺点:地图样式简单,难以定制高颜值的可视化效果适用于传统地理信息强GIS的二维数据Web维护和展示 |
| Leaflet | Canvas渲染机制;仅支持二维表达;墨卡托投影; | 优点:入手简单缺点:不支持Webgl渲染性能有瓶颈适用于轻量级简单地理信息主题可视化 |
核心方法
原生页面
初始化地图
<!DOCTYPE html>
<html lang="en">
<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">
<title>Simple Map</title>
<link rel="stylesheet" href="https://openlayers.org/en/v4.6.5/css/ol.css" type="text/css">
<script src="https://openlayers.org/en/v4.6.5/build/ol.js" type="text/javascript"></script>
<style>
#map {
height: 400px;
width: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
let map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
],
view: new ol.View({
projection: 'EPSG:4326',
center: [0, 0],
zoom: 0
})
});
</script>
</body>
</html>
Vue工程
Hello Openlayer-vue2
## ------------ 创建vue2工程
# 安装命令
npm i -g @vue/cli
# 查看vue版本
vue -V
# 创建工程
vue create openlayers # 选择vue2
# 安装ol依赖
npm install ol
编写BaseMap.vue
<template>
<div id="map"></div>
</template>
<script>
import Map from "ol/Map.js";
import OGCMapTile from "ol/source/OGCMapTile.js";
import TileLayer from "ol/layer/Tile.js";
import View from "ol/View.js";
export default {
mounted() {
new Map({
target: "map", // 挂载点
layers: [ // 图层
new TileLayer({ // 瓦片图层
source: new OGCMapTile({ // OSM 官方提供的底图
url: "https://maps.gnosis.earth/ogcapi/collections/blueMarble/map/tiles/WebMercatorQuad",
}),
}),
],
view: new View({ // 操作视图
center: [0, 0], // 中心点
zoom: 1, // 缩放级别
}),
});
},
};
</script>
<style scoped>
#map {
width: 100%;
height: 100%;
position: absolute;
inset: 0;
}
</style>
在App.vue引入即可
考虑到后面很多组件都会引入相同资源,将这些资源抽到main.js
import Vue from 'vue'
import App from './App.vue'
//引入 css 文件,后续不必重复引入
import "ol/ol.css";
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
支持 web 墨卡托(EPSG:3857)和 wgs84 坐标系(EPSG:4326),严格意 义上来说 ol 不仅支持这两个坐标系,而是支持很多的坐标系,不过其他的坐标系需要开发 者来自己定义并转换,比如我们国家的 2000 坐标系
ol 默认是 web 墨卡托坐标系,如果各位想使用 wgs84 坐标系(经纬度坐标系),可以在 view 对象中设置属性 projection: 'EPSG::4326'
view: new View({ projection: "EPSG:4326", center: [101.73497559, 27.50030558], zoom: 2, maxZoom: 8, }),
底图渲染
天地图提供了两种方式的地图服务
一种是 xyz 的方式
一种是 WMTS 的方式
- new TileLayer( )是所有瓦片图层的总类,无论是XYZ还是WMTS都要用到
XYZ加载底图
import Map from "ol/Map.js";
import XYZ from "ol/source/XYZ.js";
import TileLayer from "ol/layer/Tile.js";
import View from "ol/View.js";
const token = "";
export default {
mounted() {
new Map({
target: "map",
layers: [
new TileLayer({
source: new XYZ({
url:
"https://t0.tianditu.gov.cn/DataServer?T=img_w&x={x}&y={y}&l={z}&tk=" + token,
}),
}),
],
view: new View({
center: [472202, 7530279],
zoom: 5,
}),
});
},
};
WMTS加载底图
import Map from "ol/Map.js";
import WMTS from "ol/source/WMTS.js";
import WMTSTileGrid from "ol/tilegrid/WMTS.js";
import { get as getProjection } from "ol/proj.js";
import { getTopLeft, getWidth } from "ol/extent.js";
import TileLayer from "ol/layer/Tile.js";
import View from "ol/View.js";
const token = "";
const projection = getProjection("EPSG:3857"); // 定义坐标系,web 墨卡托投影坐标
// 计算瓦片层级以及行列号的部分 ------ start
// 根据分辨率和瓦片的尺寸,动态的算出了对应层级之下应该请求的瓦片的行列号
const projectionExtent = projection.getExtent();
const size = getWidth(projectionExtent) / 256;
const resolutions = new Array(19); // 分辨率
const matrixIds = new Array(19); // 矩阵
for (let z = 0; z < 19; ++z) {
resolutions[z] = size / Math.pow(2, z);
matrixIds[z] = z;
}
// 计算瓦片层级以及行列号的部分 ------ end
export default {
mounted() {
new Map({
layers: [
new TileLayer({
source: new WMTS({
// 天地图官方提供的 WMTS 的地址为:http://t0.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=您的密钥,可以看到 new WMTS 里面填的参数除了是计算出来的行列号以及投影信息之外,都可以在这个 url 中找到。对应填上就可以。
url: "http://t0.tianditu.gov.cn/img_w/wmts?tk=" + token,
layer: "img",
matrixSet: "w",
format: "tiles",
projection: projection,
tileGrid: new WMTSTileGrid({
origin: getTopLeft(projectionExtent),
resolutions: resolutions,
matrixIds: matrixIds,
}),
style: "default",
wrapX: true,
}),
}),
],
target: "map",
view: new View({
center: [-11158582, 4813697],
zoom: 4,
}),
});
},
};
矢量图层加载
VectorLayer()涵盖了kml、gml、wkt、geojson等所有矢量数据类型
ol 版本更新到 8.2.0 之后。style 的写法也借鉴了 mapbox 那一套,开始使用表达式的方式进行编写了,在这之前,style 只能通过自己指定边界样式 stroke,填充样式 fill 等写法来编
const vectorLayer = new VectorLayer({ background: "#1a2b39", source: new VectorSource({ url: "https://openlayers.org/data/vector/ecoregions.json", format: new GeoJSON(), }), style: { "fill-color": ["string", ["get", "COLOR"], "#eee"], }, });
在这个示例中,每个要素 (feature)对应的是杭州市的每个区县,那么我们就可以在回调函数中来操作这个 feature 或者是根据条件来判断 feature,根据不同的条件设置不同的颜色并且返回对应的颜色,
通过 feature.getProperties( )方法获取到 geojson 数据中的要素属性,我 们的数据中有 value 字段来表示产值,那么我们获取到产值之后可以对产值做一个阶段性划 分,并且给每个阶段赋上相应的颜色,最后我们再返回一个 new Style( )实例,这个实例中 的 color 色值会根据 switch 的情况返回对应的颜色,这样就形成了我们的分类着色
添加文字需要在 styleFunction 中获取到要素的属性,然后再返回一个 text 样式即可
import { hangzhou } from "../assets/hangzhou";
import GeoJSON from "ol/format/GeoJSON.js";
import Map from "ol/Map.js";
import View from "ol/View.js";
import { Fill, Stroke, Style, Text } from "ol/style.js";
import { Vector as VectorSource } from "ol/source.js";
import { Vector as VectorLayer } from "ol/layer.js";
//方式一读取本地静态文件进行加载
const vectorSource = new VectorSource({
features: new GeoJSON().readFeatures(hangzhou),
});
//方式二加载在线geojson数据
// const vectorSource = new VectorSource({
// url: "https://openlayers.org/data/vector/ecoregions.json",
// format: new GeoJSON(),
// });
const vectorLayer = new VectorLayer({
source: vectorSource,
//给图层设置样式
style: styleFunction,
});
function styleFunction(feature) {
// ol 会把每个要素作为参数返回给我们
let value = feature.getProperties().value;
let color = "";
switch (true) {
case 1000 < value && value < 2000:
color = "rgb(255, 184, 48)";
break;
case 2000 < value && value < 3000:
color = "rgb(255, 114, 73)";
break;
case 3000 < value && value < 4000:
color = "rgb(255, 83, 73)";
break;
case 4000 < value && value < 5000:
color = "rgb(177, 50, 84)";
break;
case value > 5000:
color = "rgb(71, 19, 55)";
break;
default:
color = "rgb(255, 184, 48)";
}
return new Style({
stroke: new Stroke({
color: "rgb(213, 217, 224)",
width: 2,
}),
fill: new Fill({
color: color,
}),
text: new Text({
font: "16px sans-serif",
fill: new Fill({
color: "black",
}),
text: feature.get("name"),
}),
});
}
export default {
mounted() {
const map = new Map({
layers: [vectorLayer], // 或者 map.addlayer(vectorLayer)
target: "map",
view: new View({
center: [0, 0],
zoom: 2,
}),
});
//定位聚焦到加载的矢量图层,参数是投影坐标系的范围和浏览器尺寸
map.getView().fit(vectorSource.getExtent(), { size: map.getSize() });
},
};
加载wkt矢量数据
在 ol 中加载 wkt 格式的矢量数据其核心做法就是 转换,即把 wkt 转换成内部支持的(例如 geojson)等可以被框架本身所解析的数据格式, 然后在对其进行加载,所以这当中就用到了非常关键的 new WKT( ),这个实例中有可以用 于读取 wkt 字符串的 readFeature( )方法。执行这个方法的返回值就是 ol 所能够支持的矢量 数据,后面的就和加载geojson一样了
import Map from "ol/Map.js";
import View from "ol/View.js";
import WKT from "ol/format/WKT.js";
import { Vector as VectorSource } from "ol/source.js";
import { Vector as VectorLayer } from "ol/layer.js";
import { Fill, Stroke, Style } from "ol/style.js";
const wkt =
"POLYGON((120.29126154543951 30.340552956159428,120.28428156767069 " +
"30.21655254907006, 120.40792688811769 30.213967541831877,120.4757323864253 " +
"30.32161859274214, 120.2912615454395130.340552956159428))";
const format = new WKT();
const feature = format.readFeature(wkt);
const vector = new VectorLayer({
source: new VectorSource({
features: [feature],
}),
style: new Style({
stroke: new Stroke({
color: "rgb(213, 217, 224)",
width: 2,
}),
fill: new Fill({
color: "red",
}),
}),
});
export default {
mounted() {
const map = new Map({
layers: [vector],
target: "map",
view: new View({
center: [0, 0],
zoom: 4,
}),
});
map.getView().fit(vector.getSource().getExtent(), { size: map.getSize() });
},
};
栅格图层加载
上面底图渲染就是栅格图层图层加载的一部分了,因为 WMTS 服务本身 也属于栅格数据服务
下面实现一个需求:客户要求我们展示一张非常炫酷的拥有 3D 立体效果的某个 地区的影像图,作为数字大屏的门面。可以在这张地图上 展示一些其他的数据,类似于点位,地块等等。那么我们就要确保加载到地图上的这张图片 是有经纬度坐标信息的,这样才能与其他的数据进行比对,才能将其加载到精准的位置
- 和 leaflet 以及 mapbox 等框架其实没 有太大区别,核心思路都是一样的,先知道图片四个角的经纬度,然后在加载图片
- 不过不同的是,在 ol 中写法确实复杂了很多,因为 ol 中坐标系和投影参数是在view中定义的
- 所以在这里我们必须首先把 view 的 projection 改为 EPSG:4326.同时我们也限制了投影的 边界范围为图片的四个角的经纬度范围。
- 在加载图片的时候我们用到了 imageLayer 这 个类,然后填入图片的 url 以及 projection 还有图片的经纬度范围即可
- 四个角的经纬度该如何获得?
- 我们可 以现在地图上添加一个矢量图形,然后采用掩膜裁剪的技术将我们的行政 区部分提取出来
- 然后将这张图片给到 UI 同事,那么这张图片的四个角的经纬度就是这个行政区的边界经纬度
- 行政区边界经纬度的获取:通过 getBounds( )或者借助 turf.js 等等手段获取一个矢量图层的 bounds
- 然后将这个边界范围 用在上述代码中,如果你觉得可能会有不准和误差,那么你可以将行政区和图片同时添加到地图上观察二者的重合程度,二者完全重合的时候,这张图片的位置就没有任何问题
import ImageLayer from "ol/layer/Image.js";
import Map from "ol/Map.js";
import Projection from "ol/proj/Projection.js";
import Static from "ol/source/ImageStatic.js";
import View from "ol/View.js";
import { getCenter } from "ol/extent.js";
import image from "../assets/xc.png";
const extent = [101.73497559, 27.50030558, 102.43557243, 28.18097996];
const projection = new Projection({
code: "EPSG:4326",
extent: extent,
});
export default {
mounted() {
new Map({
layers: [
new ImageLayer({
source: new Static({
url: image,
projection: projection,
imageExtent: extent,
}),
}),
],
target: "map",
view: new View({
projection: projection,
center: getCenter(extent),
zoom: 1,
}),
});
},
};
marker和DOM
在 leaflet 和 mapbox 中有专门的一个类别叫做 marker,但是在 ol 中,抱歉,对不起, 没有。ol 中只有一个 overlay 可以使用,
ol 中的 overlay 可以高度自定义 html,我在 overlay 里面写 img 元素,然后把这个图标以 overlay 的形式展现在地 图上不就可以了嘛?是的可以,但是这样做不好,因为这样做实际上相当于是把很多 dom 元素叠加到 canvas 元素之上,少量的几个图标还好,如果几千个上万个的时候。这样的操 作会把页面卡爆的同学,因此不建议在 ol 中使用 overlay 操作图标
下面这个示例是对json转geojson的封装来展示一些点位
import GeoJSON from "ol/format/GeoJSON.js";
import Map from "ol/Map.js";
import View from "ol/View.js";
import { Icon, Style } from "ol/style.js";
import { Vector as VectorSource } from "ol/source.js";
import { Vector as VectorLayer } from "ol/layer.js";
import a from "../assets/a.png";
import b from "../assets/b.png";
import c from "../assets/c.png";
export default {
data() {
return {
poi: [
{ name: "希望小学", lon: "124.789412", lat: "47.347325", type: "school" },
{ name: "蜜雪冰城", lon: "103.007012", lat: "39.53535", type: "school" },
{ name: "天猫超市", lon: "117.872028", lat: "27.100653", type: "market" },
{ name: "如家酒店", lon: "124.642233", lat: "48.042886", type: "market" },
{ name: "人民医院", lon: "87.038159", lat: "31.42261", type: "hospital" },
{ name: "和平饭店", lon: "117.038159", lat: "31.62261", type: "school" },
{ name: "银泰城", lon: "121.038159", lat: "29.42261", type: "market" },
{ name: "海底捞", lon: "111.038159", lat: "28.42261", type: "school" },
],
};
},
methods: {
// json转geojson
json2Geojson(json, lngField, latField) {
const result = { type: "FeatureCollection", features: [] };
json.forEach((j) => {
const feature = {
type: "Feature",
properties: {},
geometry: { type: "Point", coordinates: [] },
};
feature.properties = j;
feature.geometry.coordinates = [j[lngField], j[latField]];
result.features.push(feature);
});
return result;
},
// 样式
styleFunction(feature) {
let type = feature.getProperties().type;
let icon = void 0;
switch (type) {
case "school":
icon = a;
break;
case "market":
icon = b;
break;
default:
icon = c;
}
return new Style({
image: new Icon({
crossOrigin: "anonymous",
src: icon,
scale: 0.6,
}),
});
},
},
mounted() {
let geojson_poi = this.json2Geojson(this.poi, "lon", "lat");
const vectorSource = new VectorSource({
features: new GeoJSON().readFeatures(geojson_poi),
});
const vectorLayer = new VectorLayer({
source: vectorSource,
//给图层设置样式
style: this.styleFunction,
});
//构建地图对象,并把刚才定义好的layer加入到图层数组中
const map = new Map({
layers: [vectorLayer],
target: "map",
view: new View({
center: [0, 0],
zoom: 2,
}),
});
//定位聚焦到加载的矢量图层
map.getView().fit(vectorSource.getExtent(), { size: map.getSize() });
},
};
overlay 这个类的核心作用就是在地图上展示 dom 元素(div,img,span 等等),最常用的地方其实就是点击弹窗
<template>
<div>
<div id="map"></div>
<div id="info"></div>
</div>
</template>
<script>
import Map from "ol/Map.js";
import View from "ol/View.js";
import Overlay from "ol/Overlay";
import XYZ from "ol/source/XYZ.js";
import TileLayer from "ol/layer/Tile.js";
const token = '';
export default {
mounted() {
let container = document.getElementById("info");
let overlay = new Overlay({
element: container,
positioning: "center-center",
autoPan: {
animation: {
duration: 250,
},
},
});
const map = new Map({
target: "map",
layers: [
new TileLayer({
source: new XYZ({
url: "https://t0.tianditu.gov.cn/DataServer?T=img_w&x={x}&y={y}&l={z}&tk=" + token,
}),
}),
],
overlays: [overlay],
view: new View({
center: [0, 0],
zoom: 1,
}),
});
// 绑定一个事件
map.on("singleclick", (e) => {
container.innerText = e.coordinate;
overlay.setPosition(e.coordinate);
});
}
}
</script>
<style scoped>
#map {
width: 100%;
height: 100%;
position: absolute;
inset: 0;
}
#info {
padding: 5px 2px 5px 2px;
border-radius: 10px;
background: white;
font-size: 14px;
}
</style>
这个e是一个MapBrowserEvent对象
- coordinate属性是坐标
- pixel属性是像素点的位置
添加Overlay
- Map.addOverlay(overlay)
交互与事件
示例一:当缩放层级小于 6 时我们展示天地图影像图,反之我们显示天地图的矢量风格地图
两个图层都加载到map,通过setVisible( )方法,传参 true 代表显示,false 代表隐藏
import Map from "ol/Map.js";
import TileLayer from "ol/layer/Tile.js";
import View from "ol/View.js";
import XYZ from "ol/source/XYZ.js";
export default {
data() {
return {
token: "891e71134abd640edcf8b79bcd5999ae",
};
},
mounted() {
const tdt_img = new TileLayer({
source: new XYZ({
url:
"https://t0.tianditu.gov.cn/DataServer?T=img_w&x={x}&y={y}&l={z}&tk=" +
this.token,
}),
visible: false,
});
const tdt_vec = new TileLayer({
source: new XYZ({
url:
"https://t0.tianditu.gov.cn/DataServer?T=vec_w&x={x}&y={y}&l={z}&tk=" +
this.token,
}),
visible: true,
});
const map = new Map({
target: "map",
layers: [tdt_img, tdt_vec],
view: new View({
center: [472202, 7530279],
zoom: 0,
}),
});
map.on("moveend", () => {
const zoom = map.getView().getZoom();
if (zoom > 6) {
tdt_img.setVisible(true);
tdt_vec.setVisible(false);
} else {
tdt_img.setVisible(false);
tdt_vec.setVisible(true);
}
});
},
}
示例二:鼠标悬浮,要素高亮,单击弹窗
map.forEachFeatureAtPixel(鼠标位置,要素为参数的回调函数):根据位置查询矢量要素,
- 传入鼠标点所在的像素,ol 框架内部会根据这个像素去比对当前地图上这个像素位置是否有矢量图层要素,如果有,将会把这个要素 feature 返回给我们
map.getFeaturesAtPixel(鼠标位置),得到一个要素数组,里面有所有当前鼠标位置下的要素(因为会有多个图层重叠的情况发生。所以框架本身会穿透拾取要素返回给我们),因此我们可以判断返回来的数组长度,如果是0 则证明没有拾取到要素,
<template>
<div>
<div id="map"></div>
<div id="popup"></div>
</div>
</template>
<script>
import { hangzhou } from "../assets/hangzhou";
import GeoJSON from "ol/format/GeoJSON.js";
import Map from "ol/Map.js";
import View from "ol/View.js";
import { Fill, Stroke, Style, Text } from "ol/style.js";
import { Vector as VectorSource } from "ol/source.js";
import { Vector as VectorLayer } from "ol/layer.js";
import Overlay from "ol/Overlay";
export default {
methods: {
styleFunction(feature) {
let value = feature.getProperties().value;
let color = "";
switch (true) {
case 1000 < value && value < 2000:
color = "rgb(255, 184, 48)";
break;
case 2000 < value && value < 3000:
color = "rgb(255, 114, 73)";
break;
case 3000 < value && value < 4000:
color = "rgb(255, 83, 73)";
break;
case 4000 < value && value < 5000:
color = "rgb(177, 50, 84)";
break;
case value > 5000:
color = "rgb(71, 19, 55)";
break;
default:
color = "rgb(255, 184, 48)";
}
return new Style({
stroke: new Stroke({
color: "rgb(213, 217, 224)",
width: 2,
}),
fill: new Fill({
color: color,
}),
text: new Text({
font: "16px sans-serif",
fill: new Fill({
color: "black",
}),
text: feature.get("name"),
}),
});
},
},
mounted() {
const vectorSource = new VectorSource({
features: new GeoJSON().readFeatures(hangzhou),
});
const vectorLayer = new VectorLayer({
source: vectorSource,
//给图层设置样式
style: this.styleFunction,
});
let container = document.getElementById("popup");
let overlay = new Overlay({
element: container,
positioning: "bottom-center",
autoPan: {
animation: {
duration: 250,
},
},
});
//构建地图对象,并把刚才定义好的layer加入到图层数组中
const map = new Map({
layers: [vectorLayer],
target: "map",
overlays: [overlay],
view: new View({
center: [0, 0],
zoom: 2,
projection: "EPSG:4326",
}),
});
//定位聚焦到加载的矢量图层
map.getView().fit(vectorSource.getExtent(), { size: map.getSize() });
let selected = null;
// 绑定鼠标悬浮事件
map.on("pointermove", (e) => {
// 上次选中的要素的样式需要恢复原状
if (selected !== null) {
selected.setStyle(undefined);
selected = null;
}
map.forEachFeatureAtPixel(e.pixel, (f) => {
selected = f;
f.setStyle(
new Style({
stroke: new Stroke({
color: "rgb(13, 247, 219)",
width: 2,
}),
fill: new Fill({ color: [13, 247, 219, 0.2] }),
text: new Text({
font: "16px sans-serif",
fill: new Fill({
color: "black",
}),
text: f.get("name"),
}),
})
);
return true;
});
});
// 绑定单击事件
map.on("singleclick", (e) => {
const feature = map.getFeaturesAtPixel(e.pixel);
if (feature.length > 0) {
let f = feature[0];
container.style.display = "block";
container.innerHTML =
"<div>名称:" +
f.get("name") +
"</div>" +
"<div>代码:" +
f.get("adcode") +
"</div>";
overlay.setPosition(e.coordinate);
} else {
container.style.display = "none";
}
});
},
};
</script>
<style scoped>
#map {
width: 100%;
height: 100%;
position: absolute;
inset: 0;
}
#popup {
width: 150px;
padding: 5px 2px 5px 2px;
border-radius: 10px;
background: white;
font-size: 14px;
}
</style>
图形绘制与测量
在官网案例部分搜索“draw features”,即可看到官方为大家准备的丰富绘制案例,可绘制点线面等多种类型的要素
https://openlayers.org/en/latest/examples/draw-features.html
示例
index.html
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<!-- 引入bootstrap -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
AddDraw.vue
<template>
<div>
<div id="map" class="map"></div>
<div class="row">
<div class="col-auto">
<span class="input-group">
<label class="input-group-text" for="type">几何形状类型:</label>
<select class="form-select" id="type">
<option value="Point">点</option>
<option value="LineString">线</option>
<option value="Polygon">面</option>
<option value="Circle">圆</option>
<option value="None">None</option>
</select>
<input class="form-control" type="button" value="撤回" id="undo">
</span>
</div>
</div>
</div>
</template>
<script>
import Map from 'ol/Map.js';
import View from 'ol/View.js';
import Draw from 'ol/interaction/Draw.js';
import TileLayer from 'ol/layer/Tile.js';
import VectorLayer from 'ol/layer/Vector.js';
import OSM from 'ol/source/OSM.js';
import VectorSource from 'ol/source/Vector.js';
export default {
data() {
return {
token: "891e71134abd640edcf8b79bcd5999ae",
};
},
mounted() {
const raster = new TileLayer({
source: new OSM(),
});
const source = new VectorSource({ wrapX: false });
const vector = new VectorLayer({
source: source,
});
const map = new Map({
layers: [raster, vector],
target: 'map',
view: new View({
center: [-11000000, 4600000],
zoom: 4,
}),
});
const typeSelect = document.getElementById('type');
let draw; // global so we can remove it later
function addInteraction() {
const value = typeSelect.value;
if (value !== 'None') {
draw = new Draw({
source: source,
type: typeSelect.value,
});
map.addInteraction(draw);
}
}
/**
* Handle change event.
*/
typeSelect.onchange = function () {
map.removeInteraction(draw);
addInteraction();
};
document.getElementById('undo').addEventListener('click', function () {
draw.removeLastPoint();
});
addInteraction();
},
}
</script>
<style scoped>
#map {
width: 100%;
height: 100%;
position: absolute;
inset: 0;
}
</style>
🔹 补充方法:
draw.removeLastPoint();移除最后一个点
map.removeInteraction(draw);移除画笔(形状还在)
map.removeLayer(vector);移除图层(形状不在)
🔹 事件列表
事件名 触发时机 回调参数 ( event)典型用途 drawstart当用户开始绘制(第一次点击地图时触发) event.feature(即将绘制的Feature对象)初始化绘制状态、显示提示信息 drawend当用户完成绘制(双击结束或闭合多边形时触发) event.feature(最终生成的Feature对象)获取最终图形、存入数据库、检查图形合法性 drawabort当绘制被取消(如按下 ESC键或调用draw.abortDrawing())event.feature(未被添加到source的废弃Feature)清理临时数据、提示用户取消操作 change:active当绘制的启用状态改变( draw.setActive(true/false)时触发)event.oldValue(之前的状态)、event.target(Draw交互对象)控制关联 UI 控件(如启用/禁用绘制按钮) 🔹 事件监听示例
以下代码展示了如何监听所有
Draw事件:import Draw from 'ol/interaction/Draw'; const draw = new Draw({ source: vectorSource, // 绘制结果的存放源 type: 'Polygon', // 类型:Point、LineString、Polygon、Circle 等 }); // 监听绘制开始 draw.on('drawstart', (event) => { console.log('开始绘制:', event.feature); }); // 监听绘制完成 draw.on('drawend', (event) => { const feature = event.feature; console.log('绘制完成:', feature.getGeometry().getCoordinates()); }); // 监听绘制取消 draw.on('drawabort', (event) => { console.log('用户取消了绘制', event.feature); }); // 监听绘制激活状态变化 draw.on('change:active', (event) => { console.log(`绘制状态变为: ${event.target.getActive()}`); }); // 将 Draw 交互添加到地图 map.addInteraction(draw);其他示例:
1. 绘制过程中实时显示顶点坐标
draw.on('drawstart', (event) => { const sketchFeature = event.feature; const geom = sketchFeature.getGeometry(); // 监听临时图形的变化(移动鼠标时) geom.on('change', () => { const coords = geom.getCoordinates(); // 当前所有顶点 console.log('当前顶点:', coords); }); });2. 检查多边形是否合法(如面积、闭合)
draw.on('drawend', (event) => { const polygon = event.feature.getGeometry(); if (polygon.getType() === 'Polygon') { const area = polygon.getArea(); // 计算面积(投影坐标系) const isClosed = polygon.getCoordinates()[0][0][0] === polygon.getCoordinates()[0].slice(-1)[0][0]; if (area < 1000) alert('面积太小!'); if (!isClosed) alert('多边形未闭合!'); } });3. 限制绘制行为(如禁止自相交多边形)
draw.on('drawend', (event) => { import { intersects } from 'ol/extent'; const geometry = event.feature.getGeometry(); // 使用 JSTS 库检测自相交(需额外引入) if (geometry.getType() === 'Polygon' && isSelfIntersecting(geometry)) { source.removeFeature(event.feature); // 删除非法图形 alert('禁止绘制自相交多边形!'); } });🔹 注意事项
坐标系问题:
getCoordinates()返回的是地图投影坐标(如EPSG:3857),如需经纬度,用ol/proj转换:import { toLonLat } from 'ol/proj'; const lonLatCoords = geometry.getCoordinates().map(coord => toLonLat(coord));性能优化:
- 对复杂多边形,可在
drawend后调用geometry.simplify(tolerance)减少顶点数量。动态修改:
如果启用
Modify交互,需监听其modifyend事件获取编辑后的坐标:modifyInteraction.on('modifyend', (event) => { event.features.forEach(f => console.log('修改后:', f.getGeometry().getCoordinates())); });📌 总结
OpenLayers 的
Draw交互提供四个关键事件:
drawstart→ 开始绘制drawend→ 成功完成drawabort→ 用户取消change:active→ 启用/禁用状态变化利用这些事件,可以实现 绘制过程监控、数据校验、动态提示 等功能。对于复杂需求(如拓扑检查),可结合 JSTS 等库扩展功能。
// 监听绘制完成事件 draw.on('drawend', function (event) { const feature = event.feature; const geometry = feature.getGeometry(); // 获取坐标数组(格式取决于图形类型) const coordinates = geometry.getCoordinates(); console.log('图形坐标:', coordinates); });
样式示例:https://openlayers.org/en/latest/examples/draw-features-style.html
其实任何框架的绘制原理都是一样的,都是几个状态之间的记录,
- 比如当鼠标第一次落于地图之上我们需要记录鼠标的位置作为几何图形的起点,
- 当然如果只是绘制一个点,那么这个记录的结果就是最终结果了。
- 如果是线或者面,那么这个点只是起点。随着鼠标的移动。还需要记录移动过过程中鼠标当前的位置,虽然这个位置不需要记录,但是要在这个位置和起点之间连成一条线,以提醒用户,现在所画的位置和方向。
- 我在这里讲的位置其实就是指经纬度,或者是投影坐标系下的坐标。
- 都是一个道理。然后伴随着鼠标第二次,第三次,第四次……落下,分别记录每次落点的位置,直到最后一次鼠标落下(一般是双击结束绘制),我们把记录过的位置拼起来即能够形成一条线或者是一个多边形。
关于测量,其实官方也为大家封装好了成熟可用的代码,就差给各位界面让各位当作组件直接去使用了,同样的各位也可以在官网案例中直接搜索“measure”即可,代码都很成熟,几乎不需要改。即拿即用
WebGL
在 OpenLayers 中使用 WebGL 渲染可以大幅提升矢量图层(特别是大量几何图形/点)的绘制性能。以下是详细的 WebGL 使用方法和优化建议:
WebGL 基本使用
OpenLayers 从 v6.0 开始支持 WebGL 渲染(通过 WebGLVectorLayer 或 VectorLayer 的 WebGL 渲染器),适用于大量静态或动态矢量数据。
示例代码
import Map from 'ol/Map';
import View from 'ol/View';
import { Vector as VectorLayer } from 'ol/layer';
import { Vector as VectorSource } from 'ol/source';
import { LineString } from 'ol/geom';
import Feature from 'ol/Feature';
import { Stroke, Style } from 'ol/style';
// 1. 创建 WebGL 优化的矢量图层
const layer = new VectorLayer({
source: new VectorSource(),
style: new Style({
stroke: new Stroke({
color: '#ff0000',
width: 2,
}),
}),
// 关键:启用 WebGL 渲染
renderMode: 'webgl', // OpenLayers 6+
});
// 2. 添加线要素(假设有大量点)
const line = new LineString(coordinates); // coordinates 是坐标数组
const feature = new Feature({ geometry: line });
layer.getSource().addFeature(feature);
// 3. 添加到地图
const map = new Map({
target: 'map',
layers: [layer],
view: new View({
center: [0, 0],
zoom: 2,
}),
});
专用 WebGL 图层
如果数据量极大(如数十万点),可以使用 WebGLPointsLayer 或自定义 WebGL 着色器。
示例:WebGLPointsLayer(适用于海量点)
import { WebGLPoints as WebGLPointsLayer } from 'ol/layer';
import { fromLonLat } from 'ol/proj';
const layer = new WebGLPointsLayer({
source: new VectorSource({
features: [/* 大量点要素 */],
}),
style: {
'circle-radius': 4,
'circle-fill-color': 'red',
},
});
map.addLayer(layer);
性能优化关键点
(1) 减少顶点数据传递
WebGL 的优势是直接操作 GPU,但要避免频繁的数据更新:
- 静态数据:一次性加载所有坐标。
- 动态数据:用
feature.getGeometry().setCoordinates()更新而非重新创建 Feature。
(2) 启用 WebGL 的声明式样式
在图层级别定义样式(而非每个 Feature 单独设置):
const layer = new VectorLayer({
renderMode: 'webgl',
style: {
'stroke-color': '#ff0000',
'stroke-width': 2,
},
});
(3) 简化几何图形
对大数据量使用简化算法(如 ol/format/GeoJSON + 简化工具库):
import { simplify } from 'ol/geom/flat/simplify';
const simplifiedCoords = simplify(coordinates, 0.01); // 阈值根据需求调整
4. 浏览器兼容性检查
WebGL 需要浏览器支持:
if (ol.has.WEBGL) {
console.log("支持 WebGL 渲染");
} else {
console.log("回滚到 Canvas 2D 渲染");
}
注意事项
- 动态数据性能:WebGL 适合静态或低频更新的数据,高频更新(如每秒多次)仍需优化或考虑 Canvas 2D。
- 移动端适配:移动设备 GPU 内存有限,需测试大数据量下的表现。
- 调试工具:使用 Chrome DevTools 的 WebGL Inspector 分析渲染性能。
路径渲染优化
export function drawLine(map, coordinates, color, lineDash, width) {
// 1. 直接创建完整 LineString
const line = new LineString(coordinates);
const feature = new Feature({ geometry: line });
// 2. 使用 WebGL 渲染的图层
const layer = new VectorLayer({
source: new VectorSource({ features: [feature] }),
style: new Style({
stroke: new Stroke({ color, width, lineDash }),
}),
renderMode: 'webgl', // 关键!
});
map.addLayer(layer);
return layer;
}
通过 WebGL 渲染,理论上可以支持 百万级点 的流畅展示(需配合简化策略)。如果仍有卡顿,可能需要降级到 Canvas 2D 或分割数据为多图层。
