多源数据加载功能
一、技术栈、预览
Vue 3 + Typescript +Openlayers 7 + GeoServer + Element plus
二、GeoServer安装
1、GeoServer简介
GeoServer 是一个采用 Java EE 技术,且低成本、易扩展、功能完整的开源地图服务器,支持与 OpenLayers 前端框架高效融合,GeoServer 是一个符合开源地理信息系统网络服务规范的 GIS 服务器,它遵循 OGC(Open Geospatial Consortium)规范,允许用户发布符合 WCS(Web Coverage Service)、WMS(Web Map Service)或 WFS(Web Feature Service)标准的地理信息服务。用户可以通过向 GeoServer 发送请求来获取各种类型的地图和要素等服务。作为一个开放的社区项目,GeoServer 提供了开放的源代码,使用户能够根据自己的需求对源代码进行扩展和定制。用户可以使用 GeoServer 提供的各种服务来添加、修改和删除数据,发布地图、地图数据和地图瓦片。此外,GeoServer 还提供了便捷的功能,使用户能够快速共享地理空间信息。通过 GeoServer,用户可以轻松地在用户之间共享地理空间数据,促进地理信息的交流和共享。
2、GeoServer下载安装
-
GeoServer官网:http://geoserver.org/
-
GeoServer下载:
-
进入官网,进入官网点击Stable版本标签进入下载页面;
-
选择二进制文件进行下载;
-
下载后进行解压;
-
修改GeoServer端口号,找到解压目录下start.ini文件,右键编辑,找到jetty.port,将端口号修改为8000;
-
启动GeoServer,找到bin目录下的startup.bat文件,双击启动,如果控制台没有报错,则保持命令行开启。
-
登录GeoServer,打开浏览器,在地址栏输入:http://localhost:8000/GeoServer,用户名:admin,密码:geoserver
-
三、GeoServe发布地图服务
1、新建工作区
-
点击工作区->添加新的工作区;
-
配置工作区信息;
注意:工作区名称是描述项目的标识符。它不能超过10个字符或包含空格。名称空间URI(统一资源标识符)通常可以是与项目相关联的URL,其中添加了一个表示工作区的尾随标识符。归档的名称空间URI不需要解析为实际的有效web地址。
2、添加数据存储
- 点击数据存储->添加新的数据存储;
- 选择数据类型;
- 选择要发布的数据;
- 修改字符集;
3、发布图层
-
在新建图层页面,点击上面新建图层,然后点击添加图层的下拉框,选择上面新建的数据源,然后在下面会出现图层,点击后面的发布,会进入图层发布前的编辑
-
更改发布设置;
4、图层预览
- 找到菜单中Layer Preview,找到上面刚保存的图层,点击后面的OpenLayers
2、进入预览页;
其地址栏中的URL可以用来给前端使用。
四、组件编写
1、SpatialAnalysis2D页面代码
-
使用element container布局容器完成页面布局
<template> <div class="common-layout"> <el-container> <el-header> <Header></Header> </el-header> <el-container> <el-aside :width="asideWidth ? '65px' : '160px'"> <Menu></Menu> </el-aside> <el-main> <OLBaseMap></OLBaseMap> <OLDataLoading></OLDataLoading> </el-main> </el-container> </el-container> </div> </template>
-
导入menu状态仓库,监听state的变化,实现侧边栏的关闭与开启。
import { menuStore } from "../store/menu"; const menustore = menuStore(); //接收pinia仓库 const asideWidth = ref(false); //aside宽度的状态 //监听订阅state的变化 const subscribe = menustore.$subscribe( (mutation, state) => { asideWidth.value = menustore.$state.isCollapse; }, { detached: false } );
-
完整代码如下:
<template> <div class="common-layout"> <el-container> <el-header> <Header></Header> </el-header> <el-container> <el-aside :width="asideWidth ? '65px' : '160px'"> <Menu></Menu> </el-aside> <el-main> <OLBaseMap></OLBaseMap> <OLDataLoading></OLDataLoading> </el-main> </el-container> </el-container> </div> </template> <script setup lang="ts"> import Header from "../components/Header.vue"; import Menu from "../components/Menu.vue"; import OLBaseMap from "../components/OLBaseMap.vue"; import OLDataLoading from "../components/OLDataLoading.vue"; import { ref } from "vue"; import { menuStore } from "../store/menu"; const menustore = menuStore(); //接收pinia仓库 const asideWidth = ref(false); //aside宽度的状态 //监听订阅state的变化 const subscribe = menustore.$subscribe( (mutation, state) => { asideWidth.value = menustore.$state.isCollapse; }, { detached: false } ); </script> <style scoped> /*深度选择器*/ :deep(.el-header) { --el-header-padding: 0px; --el-header-height: 50px; } :deep(.el-main) { --el-main-padding: 0px; } </style>
注意:vue 3中使用:deep()深度选择器实现样式穿透。当
<style>
标签中有scoped
属性时,它的CSS只作用于当前组件中的元素,父组件的样式将不会渗透到子组件,如果希望scoped
中的样式的一个选择器能作用的更深,例如影响子组件,可以使用深度选择器。
2、OLBaseMap组件代码
-
定义一个地图容器。
<template> <div id="mapContent" class="mapContent"></div> </template>
-
引入openlayers的Map对象。
import { Map, View } from "ol";
-
创建初始化地图实例。
注意:
- 无论是Cesium还是Openlayers,其创建的地图实例均不采用响应式,因为Vue会对其进行监听,而地图实例的属性几乎一直在变化,每次变化都会触发Vue的监听事件,造成性能降低。
- 地图容器必须有具体的大小尺寸,否则会报错或者无内容显示。
- 因为本地图采用的EPSG:4548坐标系,所以地图中心的的坐标经纬度也需要投影到EPSG:4548。
从https://epsg.io/上获取坐标系定义信息。
//坐标系EPSG:4548 proj4.defs( "EPSG:4548", "+proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +type=crs" ); //注册EPSG:4548 register(proj4); let map:Map//定义一个Map类型的变量,接收地图实例 //定义初始化地图实例函数 const initMap=()=>{ map = new Map({ target:"mapContent",//绑定地图容器 view: new View({ //将坐标从经度/纬度转换为其他投影。 center: fromLonLat([108.84, 34.26], "EPSG:4548"), zoom: 4.5, //缩放级别 projection: "EPSG:4548", //设置投影坐标为自定义坐标EPSG:4548 }) , controls: [],//地图控件 }) }
-
利用OpenLayers的API进行图层创建与添加。
//利用ol自带的接口进行属性添加 //矢量图层 //GeoJSON数据加载 const geojsonMap = new VectorLayer({ source: new Vectorsource({ url: "/geoserver/Dome/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=Dome%3Acity_CS&maxFeatures=50&outputFormat=application%2Fjson", //湖南省GeoJSON format: new GeoJSON(), //指定格式 wrapX: true, }), style: new Style({ fill: new Fill({ color: "rgba(255, 255, 255, 0.2)", }), stroke: new Stroke({ color: "#ffcc33", width: 2, }), }), visible: false, properties: { title: "GeoJSON", name: "GeoJSON图层", type: "VectorLayer", }, }); //WFS的GML2格式数据加载 const wfsVectoreSource = new Vectorsource({ url: "/geoserver/Dome/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=Dome%3Acity_GZ&outputFormat=text%2Fxml%3B%20subtype%3Dgml%2F2.1.2", format: new GML2(), //指定格式 strategy: bbox, }); //将GML2数据源传入矢量图层 const wfsMap = new VectorLayer({ source: wfsVectoreSource, style: new Style({ image: new Circle({ radius: 5, fill: new Fill({ color: "#3885ff", }), }), stroke: new Stroke({ color: "blue", width: 2, }), }), visible: false, properties: { title: "WFS", name: "WFS图层", type: "VectorLayer", }, }); //WKT图层 const wktFormat = new WKT(); //需要进行坐标转换 const wktFearure = wktFormat.readFeature(HKWKT, { dataProjection: "EPSG:4326",//读取的数据的投影 featureProjection: "EPSG:4548",//要素几何的投影 }); const wktVectorSource = new Vectorsource({ features: [wktFearure], }); const wktMap = new VectorLayer({ source: wktVectorSource, style: new Style({ fill: new Fill({ color: "rgba(255, 255, 255, 0.2)", }), stroke: new Stroke({ color: "#ab4bfe", width: 2, }), }), visible: false, properties: { title: "WKT", name: "WKT图层", type: "VectorLayer", }, }); // 栅格图层 // 计算EPSG:3857矩阵集 const projectionExtent = get("EPSG:3857")?.getExtent(); const size = getWidth(projectionExtent as Extent) / 256; const resolutions: number[] = []; const matrixIds: string[] = []; for (let z = 0; z < 9; ++z) { resolutions[z] = size / Math.pow(2, z); matrixIds[z] = "EPSG:3857:" + z; } //WMTS图层 //此处采用从GetCapabilities中响应访问WMTS的配置。 fetch("/geoserver/gwc/service/wmts?Request=GetCapabilities") .then((res) => res.text()) .then((txt) => new WMTSCapabilities().read(txt)) .then((capabilities) => optionsFromCapabilities(capabilities, { layer: "Dome:tiff_GX", //在Geoserver预览页面,右键进入源代码进行查看配置信息。 }) ) .then((options) => { const wmtsMap = new TileLayer({ source: new WMTS(options as Options), properties: { title: "WMTS", name: "WMTS图层", type: "VectorLayer", }, visible:false, }); map.addLayer(wmtsMap); }); //WMS图层 const wmsMap = new ImageLayer({ //参数直接查看geoserver预览查看源码 source: new ImageWMS({ url: "/geoserver/Dome/wms", //访问参数采用KVP params: { FORMAT: "image/png", VERSION: "1.1.1", STYLES: "", LAYERS: "Dome:city_WH", exceptions: "application/vnd.ogc.se_inimage", }, ratio: 1, serverType: "geoserver", }), visible: false, properties: { title: "WMS", name: "WMS图层", type: "ImageLayer", }, }); //底图 const tdtSource = new XYZ({ url: "http://t0.tianditu.com/DataServer?T=vec_w&x={x}&y={y}&l={z}&tk=9f9a02030489ce88dd322ca2ebe77cd2", //网址模板 wrapX: true, }); const tdtMapLayer = new TileLayer({ source: tdtSource, visible: true, properties: { title: "tdtMapLayer", name: "天地图底图", type: "TileLayer", }, }); //注记底图 const tdtMapNote = new TileLayer({ source: new XYZ({ url: "http://t0.tianditu.com/DataServer?T=cva_w&x={x}&y={y}&l={z}&tk=9f9a02030489ce88dd322ca2ebe77cd2", //网址模板 wrapX: true, }), visible: true, properties: { title: "tdtMapNote", name: "天地图标注", type: "TileLayer", }, }); //底图图层组 const baseLayerGroup = new LayerGroup({ layers: [tdtMapLayer, tdtMapNote], }); //矢量图层组 const vectorLayerGroup = new LayerGroup({ layers: [geojsonMap, wfsMap, wktMap], }); //栅格图层组 const gridLayerGroup = new LayerGroup({ layers: [wmsMap], });
注意:
-
直接使用geoserver的地图服务URL会存在跨域问题。可以在Geoserver中进行配置,也可以在vite.config.ts中配置Proxy代理。例如:
import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; import { vitePluginMars3d } from "vite-plugin-mars3d"; import path from "path"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue(), vitePluginMars3d()], //配置Proxy跨域 server: { proxy: { "/geoserver": { target: "http://localhost:8000/geoserver", //将原链接上的geoserver替换为空 rewrite: (path) => path.replace(/^\/geoserver/, ""), }, }, }, resolve: { alias: { "@": path.resolve(__dirname, "src"), //定义路径别名 }, }, });
-
-
完整代码:
<template> <div id="mapContent" class="mapContent"></div> <OLPopup></OLPopup> </template> <script setup lang="ts"> import OLPopup from "./OLPopup.vue"; //弹窗组件 import { onMounted } from "vue"; import { Map, View } from "ol"; import { Tile as TileLayer, Image as ImageLayer } from "ol/layer"; import VectorLayer from "ol/layer/Vector.js"; import XYZ from "ol/source/XYZ"; import Vectorsource from "ol/source/Vector"; import { fromLonLat, get } from "ol/proj"; import { register } from "ol/proj/proj4"; import GeoJSON from "ol/format/GeoJSON"; import GML2 from "ol/format/GML2.js"; import LayerGroup from "ol/layer/Group.js"; import { bbox } from "ol/loadingstrategy"; import { Style, Stroke, Circle, Fill } from "ol/style"; import bus from "@/utils/bus.ts"; //定义声明文件再引入 import { Extent, getWidth } from "ol/extent"; import WMTS from "ol/source/WMTS"; import WMTSCapabilities from "ol/format/WMTSCapabilities"; import { optionsFromCapabilities } from "ol/source/WMTS"; import { Options } from "ol/source/WMTS"; import ImageWMS from "ol/source/ImageWMS.js"; import { OverviewMap } from "ol/control.js"; import HKWKT from "../static/data/HKWKT.txt?raw"; //引入海南WKT文本 import WKT from "ol/format/WKT.js"; import proj4 from "proj4"; //坐标系EPSG:4548 proj4.defs( "EPSG:4548", "+proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +type=crs" ); register(proj4); let map: Map; //存放地图实例 //利用ol自带的接口进行属性添加 //矢量图层 const geojsonMap = new VectorLayer({ //GeoJSON数据加载 source: new Vectorsource({ url: "/geoserver/Dome/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=Dome%3Acity_CS&maxFeatures=50&outputFormat=application%2Fjson", //湖南省GeoJSON format: new GeoJSON(), //指定格式 wrapX: true, }), style: new Style({ fill: new Fill({ color: "rgba(255, 255, 255, 0.2)", }), stroke: new Stroke({ color: "#ffcc33", width: 2, }), }), visible: false, properties: { title: "GeoJSON", name: "GeoJSON图层", type: "VectorLayer", }, }); const wfsVectoreSource = new Vectorsource({ //WFS的GML2格式数据加载 url: "/geoserver/Dome/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=Dome%3Acity_GZ&outputFormat=text%2Fxml%3B%20subtype%3Dgml%2F2.1.2", format: new GML2(), //指定格式 strategy: bbox, }); const wfsMap = new VectorLayer({ //将GML2数据源传入矢量图层 source: wfsVectoreSource, style: new Style({ image: new Circle({ radius: 5, fill: new Fill({ color: "#3885ff", }), }), stroke: new Stroke({ color: "blue", width: 2, }), }), visible: false, properties: { title: "WFS", name: "WFS图层", type: "VectorLayer", }, }); //WKT图层 // const hnWKT = HNWKT(); const wktFormat = new WKT(); const wktFearure = wktFormat.readFeature(HKWKT, { dataProjection: "EPSG:4326", featureProjection: "EPSG:4548", }); const wktVectorSource = new Vectorsource({ features: [wktFearure], }); const wktMap = new VectorLayer({ source: wktVectorSource, style: new Style({ fill: new Fill({ color: "rgba(255, 255, 255, 0.2)", }), stroke: new Stroke({ color: "#ab4bfe", width: 2, }), }), visible: false, properties: { title: "WKT", name: "WKT图层", type: "VectorLayer", }, }); // 栅格图层 // 计算EPSG:3857矩阵集 const projectionExtent = get("EPSG:3857")?.getExtent(); const size = getWidth(projectionExtent as Extent) / 256; const resolutions: number[] = []; const matrixIds: string[] = []; for (let z = 0; z < 9; ++z) { resolutions[z] = size / Math.pow(2, z); matrixIds[z] = "EPSG:3857:" + z; } //WMTS图层 fetch("/geoserver/gwc/service/wmts?Request=GetCapabilities") .then((res) => res.text()) .then((txt) => new WMTSCapabilities().read(txt)) .then((capabilities) => optionsFromCapabilities(capabilities, { layer: "Dome:tiff_GX", }) ) .then((options) => { const wmtsMap = new TileLayer({ source: new WMTS(options as Options), properties: { title: "WMTS", name: "WMTS图层", type: "VectorLayer", }, visible:false, }); map.addLayer(wmtsMap); }); //WMS图层 const wmsMap = new ImageLayer({ //参数直接查看geoserver预览查看源码 source: new ImageWMS({ url: "/geoserver/Dome/wms", //访问参数采用KVP params: { FORMAT: "image/png", VERSION: "1.1.1", STYLES: "", LAYERS: "Dome:city_WH", exceptions: "application/vnd.ogc.se_inimage", }, ratio: 1, serverType: "geoserver", }), visible: false, properties: { title: "WMS", name: "WMS图层", type: "ImageLayer", }, }); //底图 const tdtSource = new XYZ({ url: "http://t0.tianditu.com/DataServer?T=vec_w&x={x}&y={y}&l={z}&tk=9f9a02030489ce88dd322ca2ebe77cd2", //网址模板 wrapX: true, }); const tdtMapLayer = new TileLayer({ source: tdtSource, visible: true, properties: { title: "tdtMapLayer", name: "天地图底图", type: "TileLayer", }, }); //注记底图 const tdtMapNote = new TileLayer({ source: new XYZ({ url: "http://t0.tianditu.com/DataServer?T=cva_w&x={x}&y={y}&l={z}&tk=9f9a02030489ce88dd322ca2ebe77cd2", //网址模板 wrapX: true, }), visible: true, properties: { title: "tdtMapNote", name: "天地图标注", type: "TileLayer", }, }); //底图图层组 const baseLayerGroup = new LayerGroup({ layers: [tdtMapLayer, tdtMapNote], }); //矢量图层组 const vectorLayerGroup = new LayerGroup({ layers: [geojsonMap, wfsMap, wktMap], }); //栅格图层组 const gridLayerGroup = new LayerGroup({ layers: [wmsMap], }); //控件:鹰眼图 const overviewMapControl = new OverviewMap({ //鹰眼控件的样式名称 className: "olOverviewmap", view: new View({ center: fromLonLat([112.994818, 28.131583], "EPSG:4548"), zoom: 1, minZoom: 1, projection: "EPSG:4548", }), layers: [ new TileLayer({ source: tdtSource, }), ], //鹰眼控件一开始展开方式 collapsed: false, }); //初始化地图实例 const initMap = () => { map = new Map({ target: "mapContent", //地图容器 view: new View({ center: fromLonLat([112.994818, 28.131583], "EPSG:4548"), //将坐标从经度/纬度转换为其他投影。 zoom: 16, //缩放级别 projection: "EPSG:4548", //设置投影坐标为自定义坐标EPSG:4548 }), controls: [], }); map.addLayer(baseLayerGroup); map.addLayer(vectorLayerGroup); map.addLayer(gridLayerGroup); map.addControl(overviewMapControl); }; const sendMitt = () => { //将map提交到事件总线 bus.emit("mapToChecked", map); }; onMounted(() => { initMap(); sendMitt(); }); </script> <style> .mapContent { /* flex: 1; */ width: 100vw; height: calc(100vh - 50px); } .olPopup { position: absolute; background-color: white; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); padding: 15px; border-radius: 10px; border: 1px solid #cccccc; bottom: 12px; left: -50px; min-width: 350px; } .olPopup:after, .olPopup:before { top: 100%; border: solid transparent; content: " "; height: 0; width: 0; position: absolute; pointer-events: none; } .olPopup:after { border-top-color: white; border-width: 10px; left: 48px; margin-left: -10px; } .olPopup:before { border-top-color: #cccccc; border-width: 11px; left: 48px; margin-left: -11px; } .olPopupCloser { text-decoration: none; position: absolute; top: 2px; right: 8px; } .olPopupCloser:after { content: "✖"; } /*自定义鹰眼样式*/ .olOverviewmap { position: absolute; bottom: 5px; left: 5px; } /*鹰眼控件地图容器边框样式*/ .ol-overviewmap-map { border: 1px solid #083a20; } /*鹰眼控件中显示当前窗口区域的边框样式*/ .ol-overviewmap-box { border: 2px solid red; } /*鹰眼控件展开时其控件按钮图标的样式*/ .olOverviewmap:not(.ol-collapsed) button { display: none; } .ol-overviewmap-map { width: 150px; height: 200px; } </style>
注意:其中将map提交到事件总线是为了方便其他子组件之间进行传值,确保操作的是同一个地图实例。按以下方式实现bus.ts。
- 安装mitt;
npm install mitt -S
- 封装mitt;
//封装mitt import mitt from 'mitt' const bus = mitt() export default bus
- 在需要用的地方按需引入使用;
//将map提交到事件总线 bus.emit("mapToChecked", map);
//监听map数据 bus.on("mapToChecked", (res: Map) => { // 传参由回调函数中的形参接受 mapCopy = res; });
//移除指定事件 bus.off("mapToChecked")
3、OLDataLoading组件代码
1、完整代码:
<template>
<div class="controlDiv">
<el-button class="dataButton" @click="dialog = true">加载数据源</el-button>
<OLLayerManage></OLLayerManage>
<OLSearch></OLSearch>
<OLDraw></OLDraw>
<OLAnalyst></OLAnalyst>
</div>
<el-drawer v-model="dialog" direction="rtl" size="20%">
<h2>数据类型</h2>
<div class="sidebar" v-for="(item, index) in listData" :key="index">
<input
type="checkbox"
name="gridLayerCheckBox"
v-model="checkedArr"
:value="item.name"
/>{{ item.name }}<br />
</div>
<el-button @click="loading">更新</el-button>
</el-drawer>
</template>
<script lang="ts" setup>
import OLAnalyst from "./OLAnalyst.vue"; //缓冲区分析功能
import OLDraw from "./OLDraw.vue"; //绘制几何功能
import OLSearch from "./OLSearch.vue"; //矢量要素搜素功能
import OLLayerManage from "./OLLayerManage.vue"; //图层管理功能
import { ref } from "vue";
import { Map } from "ol";
import bus from "@/utils/bus.ts";
let mapCopy: Map; //接收地图实例的Map对象
let checkedArr = ref<String[]>([]); //响应式数组,接收复选框传递的值
const dialog = ref<boolean>(false);
//图层名称与id
type ListData = {
id: number;
name: string;
};
//复选框的值
const listData: ListData[] = [
{
id: 1,
name: "GeoJSON",
},
{
id: 2,
name: "WFS",
},
{
id: 3,
name: "WKT",
},
{
id: 4,
name: "WMTS",
},
{
id: 5,
name: "WMS",
},
];
//监听map数据
bus.on("mapToChecked", (res: Map) => {
// 传参由回调函数中的形参接受
mapCopy = res;
});
//图层加载
const loading = () => {
let length: number = mapCopy.getAllLayers().length; //获取map图层数组长度
//遍历mapCopy图层
for (let i = 0; i < length; i++) {
let mapCopyLayerName = mapCopy.getAllLayers()[i].get("title"); //通过getAllLayers方法获取图层title
if (mapCopyLayerName != "tdtMapLayer" && mapCopyLayerName != "tdtMapNote") {
//底图不进入循环
if (checkedArr.value.length == 0) {
//复选框都未选中状态不进入循环
mapCopy.getAllLayers()[i].setVisible(false);
}
for (let checkedLayerName of checkedArr.value) {
//遍历复选框状态
if (checkedLayerName === mapCopyLayerName) {
//如果选中的复选框的值与地图图层title相同,就将图层可视性设为true
mapCopy.getAllLayers()[i].setVisible(true);
break; //if判断成功则跳出for of循环
} else {
mapCopy.getAllLayers()[i].setVisible(false);
}
}
}
}
};
</script>
<style scoped>
.controlDiv {
position: absolute;
display: flex;
top: 50px;
align-items: flex-start;
}
</style>