暑期项目总结

本文最后更新于:2022年9月21日 晚上

暑期工作内容总结

0.概述

  1. 项目要求:用户上传地理数据,根据用户的需求进行可视化

  2. 项目框架:

    • 前端框架:Vue(暂定)
    • 后端:Nodejs (不会Java Springboot)
    • 地理服务提供商:Geoserver
    • Geoserver操作:Geoserver Rest Api
  3. 具体需求:

    • 用户上传地理数据(前端),自动发布到Geoserver中

      • 矢量数据:shp、kml、geojson(json)
      • 栅格数据:dem、tif
    • 数据发布服务

      • WMS
      • WTF
      • WMTS
      • TMS
    • 数据的可视化

      • 前端可视化:利用leaflet、Cesium自带函数,对请求返回的json数据(WFS服务)进行可视化

      • 后端可视化:根据用户可视化需求生成SLD样式文件,并上传到Geoserver中,应用样式

1.用到的模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const cs = require("child_process");
//用来处理子进程
const axios = require('axios');
//用来进行通信
const fs = require('fs')
//用来读取文件
const AdmZip = require('adm-zip');
//用来处理压缩包任务
const geojson2shp = require('geojson2shp')
//用来处理json与shp文件转换
const tj = require('@mapbox/togeojson')
//用来处理kml与json文件转换
const DOMParser = require('xmldom').DOMParser;
//用来处理dom文件,提取Dom节点(信息)

//用来处理通信路由
const express = require('express')
const router = express.Router()
// 导入用户路由处理函数模块
const Handler = require('../router_handler/formal')
//作为处理文件上传的中间件
const multer = require('multer')
const { route } = require('./test')

2.具体功能的实现

1. (多)文件上传 Upload_many()
  • ==使用到了multer模块==
  • 核心:后端接收文件、判断后缀、格式转化(shp)、文件压缩、文件上传(Geoserver Api 接口)
  • 问题:
    • 文件上传至后端
      • multer模块不进行配置,不能正常获取原文件等名称
      • 多文件上传的时候,每个文件都会调用一次storage,所以stroage中判断文件名称后缀时候不能用数组的方式读取
    • 后端文件格式的处理
      • shp格式的文件:
        • 存在的问题
          1. 由多个文件(shp、shx、prj、dbf等等)构成,需要进行核心文件等判断
      • kml格式的文件:利用togeojson库转为geojson
        • 存在的问题
          1. 数据量大时,转化不完全, 存在矢量缺失的情况
          2. 投影问题较多,例如:只能强制转化为几种常见投影,kml文件缺失投影信息转换容易出错
      • json(geojson)格式的文件:利用geojson2shp库转为shp
        • 存在的问题
          1. 可支持的投影较少,生成的.prj文件或与标准prj文件的格式有所差异
          2. 毕竟是github上抄的库
      • 思路:前端上传文件(input标签),后端使用multer模块接收。接收时,截取上传文件类型,根据对应的格式存放到服务器不同的文件架上。由于矢量文件在Geoserver提供的API中都需要以ZIP的格式上传,所以利用第三方库对shp下的文件进行打包,最后使用clild processexec函数调用curl命令,上传到后台Geoserver中。
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
//后端路由代码
const multer = require('multer')
const storage = multer.diskStorage({
destination: function (request, files, cb) {
if (files.originalname.split('.')[1] != null) {
console.log(files.originalname)
let tpName = files.originalname.split('.')[1]
switch (tpName) {
case 'zip':
cb(null, '../geoserver_rest/uploadfile/upload') // 上传文件的目录
break;
case 'png':
case 'jpg':
case 'svg':
case 'jpeg':
// cb(null, '../geoserver_rest/uploadfile/Image')
cb(null, '../geoserver_rest/public/userImage')
break; // 上传文件的目录
case 'tif':
cb(null, '../geoserver_rest/uploadfile/Tiff')
break; // 上传文件的目录
case 'json':
case 'geojson':
cb(null, '../geoserver_rest/uploadfile/geojson')
break;
case 'kml':
cb(null, '../geoserver_rest/uploadfile/kml')
break;
case 'shp':
case 'shx':
case 'dbf':
case 'cpg':
case 'sbx':
case 'sbn':
case 'qix':
case 'shp.xml':
cb(null, '../geoserver_rest/uploadfile/shpcomponent')
break;
//prj文件单独存,因为栅格和矢量都会用到
case 'prj':
cb(null, '../geoserver_rest/uploadfile/Prj')
break;
case 'asc':
cb(null, '../geoserver_rest/uploadfile/Asc')
break;
default:
console.log('上传文件类型出错,请检查文件类型与后缀')
break;
}
}
},
filename: function (request, files, cb) {
cb(null, files.originalname)
orgName = files.originalname // 上传文件的目录、文件名称保存到全局变量中
}
})

// multer 配置
const upload = multer({
storage
})

/*--------------------------------------------------------------------------------*/

//后端处理函数代码
exports.Upload_many = (req, res) => {
//多文件
if (req.files.length > 1) {
var zip = new AdmZip()
//投影信息存放的文件夹
let prjPath = '../geoserver_rest/uploadfile/Prj/'
let fullName = req.files[0].originalname
let fileName = fullName.split('.')[0]
// 先把所有后缀拿到,放在数组中
let suffixs = []
for (let i = 0; i < req.files.length; i++) {
suffixs.push(req.files[i].originalname.split('.')[1])
}
if (suffixs.includes("shp")) {
// shapefile文件只需要shp shx dbf文件即可生成正常显示,外加上可选的prj文件
let upfilePath = '../geoserver_rest/uploadfile/shpcomponent/'
zip.addLocalFile(upfilePath + fileName + '.shp')
zip.addLocalFile(upfilePath + fileName + '.shx')
zip.addLocalFile(upfilePath + fileName + '.dbf')
zip.addLocalFile(prjPath + fileName + '.prj')
zip.writeZip('../geoserver_rest/uploadfile/upload/' + fileName + '.zip')
// 设置延时,先让zip文件生成
setTimeout(() => {
cs.exec('curl -v -u admin:geoserver -X PUT -H "Content-type:application/zip" --data-binary @' + '../geoserver_rest/uploadfile/upload/' + fileName + '.zip' + ' ' + http_url + '/geoserver/rest/workspaces/' + user_workspace + '/datastores/' + fileName + '/file.shp', (error, stdout, stderr) => {
if (error) {
return res.cc(error);
}
res.send(stdout)
})
}, 200);
}
else if (suffixs.includes("asc")) {
// asc文件只用asc文件和包含投影信息的prj文件
let upfilePath = '../geoserver_rest/uploadfile/Asc/'
zip.addLocalFile(upfilePath + fileName + '.asc')
zip.addLocalFile(prjPath + fileName + '.prj')
zip.writeZip('../geoserver_rest/uploadfile/upload/' + fileName + '.zip')
console.log(fileName)
// 设置延时,先让zip文件生成
setTimeout(() => {
cs.exec('curl -v -u admin:geoserver -X PUT -H "Content-type:application/zip" --data-binary @' + '../geoserver_rest/uploadfile/upload/' + fileName + '.zip' + ' ' + http_url + '/geoserver/rest/workspaces/' + user_workspace + '/coveragestores/' + fileName + '/file.arcgrid', (error, stdout, stderr) => {
if (error) {
return res.cc(error);
}
res.send(stdout)
})
}, 200);
}
else {
res.send("暂不支持该文件类型")
}
}
//单文件
else {
let fullName = req.files[0].originalname
let fileName = fullName.split('.')[0]
let filetype = fullName.split('.')[1]
switch (filetype) {
// shp格式数据
case 'zip':
{
cs.exec('curl -v -u admin:geoserver -X PUT -H "Content-type:application/zip" --data-binary @' + req.files[0].path + ' ' + http_url + '/geoserver/rest/workspaces/' + user_workspace + '/datastores/' + fileName + '/file.shp', (error, stdout, stderr) => {
if (error) {
return res.cc(error);
}
res.send(stdout)
})
}
break;
case 'json':
case 'geojson':
const options = {
layer: fileName,
targetCrs: 4326
}
// Paths
geojson2shp.convert('../geoserver_rest/uploadfile/geojson/' + fullName, '../geoserver_rest/uploadfile/upload/' + fileName + '.zip', options)
setTimeout(() => {
cs.exec('curl -v -u admin:geoserver -X PUT -H "Content-type:application/zip" --data-binary @' + '../geoserver_rest/uploadfile/upload/' + fileName + '.zip' + ' ' + http_url + '/geoserver/rest/workspaces/' + user_workspace + '/datastores/' + fileName + '/file.shp', (error, stdout, stderr) => {
if (error) {
// console.error('error:', error);
return res.cc(error);
}
res.send(stdout)
})
// 注意设置延时时间,要不会产生Error occured unzipping file的错误
// 1 是还没有上传完就执行了在geoserver上传,找不到文件 可以适当延长延时时间
// 2 路径问题, 两者本质上都是文件找不到,所以会产生解压错误
// 后续可以使用promise处理这种响应事件
}, 200);
break;
//kml本质上是先将kml转为json,在把json转为shpzip
case 'kml':
{
var kml = new DOMParser().parseFromString(fs.readFileSync(req.files[0].path, 'utf8'));
let kml2Json = tj.kml(kml)
const options = {
layer: fileName,
targetCrs: 4326
}
geojson2shp.convert(kml2Json, '../geoserver_rest/uploadfile/upload/' + fileName + '.zip', options)
setTimeout(() => {
cs.exec('curl -v -u admin:geoserver -X PUT -H "Content-type:application/zip" --data-binary @' + '../geoserver_rest/uploadfile/upload/' + fileName + '.zip' + ' ' + http_url + '/geoserver/rest/workspaces/' + user_workspace + '/datastores/' + fileName + '/file.shp', (error, stdout, stderr) => {
if (error) {
return res.cc(error);
}
res.send(stdout)
})
}, 500);
}
break;
// 栅格数据的上传
// tif单文件上传
case 'tif':
{
cs.exec('curl -u admin:geoserver -XPUT -H "Content-type:image/tiff" --data-binary @' + req.files[0].path + ' http://localhost:8080/geoserver/rest/workspaces/' + user_workspace + '/coveragestores/' + fileName + '/file.geotiff', (error, stdout, stderr) => {
if (error) {
return res.cc(error);
}
res.send(stdout)
})
}
break;

}
console.log('数据上传响应成功')
}
}
2. 获取工作空间 GetWorkspace()
  • ==使用到了child process模块==
  • 核心:curl调用Geoserver Rest API接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取工作空间名称
exports.GetWorkSpaces = (req, res) => {
const user = req.body
console.log(user)
const result = cs.exec('curl -v -u ' + user.username + ':' + user.password + ' -XGET\
http://localhost:8080/geoserver/rest/workspaces', (error, stdout, stderr) => {
if (error) {
console.error('error:', error);
return;
}
res.send(stdout)
//向前端发送从Geoserver中请求的工作空间信息
})
}
3. 获取图层 GetLayers
  • ==使用到了child process模块==
  • 核心:curl调用Geoserver Rest API接口
1
2
3
4
5
6
7
8
9
10
11
12
13
exports.GetLayers = (req, res) => {
const info = req.body
console.log(info)
cs.exec('curl -v -u ' + info.username + ':' + info.password + ' -X GET http://localhost:8080/geoserver/rest/workspaces/' + req.body.workspace_name + '/layers ', (error, stdout, stderr) => {
if (error) {
console.error('error:', error);
return;
}
console.log("触发了GetLayers函数")
data = JSON.parse(stdout)
res.send(data.layers.layer)
})
}
4. 获取二维WMTS链接
  • ==使用到了axios模块==
  • 核心:链接格式的拼接
  • 切片投影矩阵:WebMercatorQuad(EPSG:3857、EPSG:900913)
1
2
3
4
5
6
7
8
9
10
11
// 2dWMTS
exports.Get2DWMTS = (req, res) => {
console.log(req.body)
let data = {
url: "http://localhost:8080/geoserver/gwc/service/wmts",
layer: workspace_used + ":" + req.body.layer,
tilematrixSet: "WebMercatorQuad",
// tilematrixSet: "EPSG:4326",
}
res.send(data)
}
5. 获取三维WMTS链接(Cesium)
  • ==使用到了axios模块==

  • 核心:链接格式的拼接

  • 注意投影切片矩阵的格式

1
2
3
4
5
6
7
8
9
10
11
12
13
exports.Get3DWMTS = (req, res) => {
console.log(req.body)
let data = {
url: "http://localhost:8080/geoserver/gwc/service/wmts",
layer: workspace_used + req.body.layer,
// tilematrixSet: "WebMercatorQuad",
tilematrixSet: "EPSG:4326",
matrixIds: ['EPSG:4326:0', 'EPSG:4326:1', 'EPSG:4326:2', 'EPSG:4326:3', 'EPSG:4326:4', 'EPSG:4326:5', 'EPSG:4326:6', 'EPSG:4326:7', 'EPSG:4326:8', 'EPSG:4326:9', 'EPSG:4326:10',
'EPSG:4326:11', 'EPSG:4326:12', 'EPSG:4326:13', 'EPSG:4326:14', 'EPSG:4326:15', 'EPSG:4326:16', 'EPSG:4326:17', 'EPSG:4326:18', 'EPSG:4326:19', 'EPSG:4326:20', 'EPSG:4326:21'
]
}
res.send(data)
}
6. 整合图层服务链接
  • ==使用到了axios模块==
  • 整合链接,提供给前端
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
//获取单个图层的WMS WMTS WFS TMS
exports.Get_WWWT = (req, res) => {
const info = req.body
console.log(info)
cs.exec('curl -v -u ' + info.username + ':' + info.password + ' -X GET ' + http_url + '/geoserver/rest/workspaces/' + workspace_used + '/layers ', (error, stdout, stderr) => {
if (error) {
console.error('error:', error);
return;
}
//stdou是string ,转成json, 返回去
let data = JSON.parse(stdout)
const layer = data.layers.layer
console.log(layer)
let Layer_big = []
let types = {}
for (let i = 0; i < layer.length; i++) {
axios.get(http_url + '/geoserver/wfs?service=wfs&version=2.0.0&request=DescribeFeatureType&typeNames=' + workspace_used + ':' + encodeURIComponent(layer[i].name) + '&outputFormat=application%2Fjson').then(response => {
//用字典存,就不存在同异步的问题
console.log(response.data)
//存在两个问题:?????????????????????
//1.通过postgis上传的图层的 "name": "the_geom"所在的位置不是第一个,不能直接用featureTypes[0],可能需要加一层判断
//2.栅格数据不存在此类属性,需要区分讨论
types[layer[i].name] = response.data.featureTypes[0].properties[0].localType
}).catch(err => {
console.log(err)
})
}
setTimeout(() => {
for (let i = 0; i < layer.length; i++) {
let WMS_url = '' + http_url + '/geoserver/' + workspace_used + '/wms?&layer=' + workspace_used + ':' + layer[i].name + '&format=image/png&transparent=true'
let WMS_url2 = '' + http_url + '/geoserver/' + workspace_used + '/wms?&layer=' + workspace_used + ':' + layer[i].name + ''
let WMTS_url = '' + http_url + '/geoserver/gwc/service/wmts?service=WMTS&request=GetTile&version=1.0.0&layer=' + workspace_used + ':' + layer[i].name + '&style=&tilematrixset=WebMercatorQuad&format=image%2Fpng&width=256&height=256&tilematrix={z}&tilerow={y}&tilecol={x}'
let WFS_url = '' + http_url + '/geoserver/wfs?request=GetFeature&version=1.1.0&typeName=' + workspace_used + ':' + layer[i].name + '&maxFeatures=50&outputFormat=application/json'
let TMS_url = '' + http_url + '/geoserver/gwc/service/tms/1.0.0/' + workspace_used + ':' + layer[i].name + '@WebMercatorQuad@png/{z}/{x}/{-y}.png'
let TMS_url2 = '' + http_url + '/geoserver/gwc/service/tms/1.0.0/' + workspace_used + ':' + layer[i].name + '@EPSG:900913@png/{z}/{x}/{reverseY}.png'
let WMTS_url2 = '' + http_url + '/geoserver/gwc/service/wmts/rest/' + workspace_used + ':' + layer[i].name + '/{style}/{TileMatrixSet}/{TileMatrixSet}:{TileMatrix}/{TileRow}/{TileCol}?format=image/png'
let services = [
{
ServiceName: "WMS-二维",
URL: WMS_url
},
{
ServiceName: "WMS-三维",
URL: WMS_url2
},
{
ServiceName: "WFS",
URL: WFS_url
},
{
ServiceName: "TMS-二维",
URL: TMS_url
},
{
ServiceName: "TMS-三维",
URL: TMS_url2
},
{
ServiceName: "WMTS-二维",
URL: WMTS_url
},
{
ServiceName: "WMTS-三维",
URL: WMTS_url2
},
]
let temp = {}
temp.name = layer[i].name
temp.id = i + 1
temp.type = types[layer[i].name]
temp.service = services
Layer_big.push(temp)
}
}, 100);

setTimeout(() => {
res.send(Layer_big)
}, 200);
})
}

//获取单个用户图层的WMS WMTS WFS TMS
exports.Get_WWWT_user = (req, res) => {
const info = req.body
console.log(info)
cs.exec('curl -v -u ' + info.username + ':' + info.password + ' -X GET ' + http_url + '/geoserver/rest/workspaces/' + user_workspace + '/layers ', (error, stdout, stderr) => {
if (error) {
console.error('error:', error);
return;
}
data = JSON.parse(stdout)
// console.log(data.layers.layer)
const layer = data.layers.layer
// console.log(layer)
let Layer_big = []
let types = {}
for (let i = 0; i < layer.length; i++) {
axios.get(http_url + '/geoserver/wfs?service=wfs&version=2.0.0&request=DescribeFeatureType&typeNames=' + user_workspace + ':' + encodeURIComponent(layer[i].name) + '&outputFormat=application%2Fjson').then(response => {
//用字典存,就不存在同异步的问题
types[layer[i].name] = response.data.featureTypes[0].properties[0].localType
}).catch(err => {
console.log(err)
})
}
setTimeout(() => {
for (let i = 0; i < layer.length; i++) {
let WMS_url = '' + http_url + '/geoserver/' + user_workspace + '/wms?&layer=' + user_workspace + ':' + layer[i].name + '&format=image/png&transparent=true'
let WMS_url2 = '' + http_url + '/geoserver/' + user_workspace + '/wms?&layer=' + user_workspace + ':' + layer[i].name + ''
let WMTS_url = '' + http_url + '/geoserver/gwc/service/wmts?service=WMTS&request=GetTile&version=1.0.0&layer=' + user_workspace + ':' + layer[i].name + '&style=&tilematrixset=WebMercatorQuad&format=image%2Fpng&width=256&height=256&tilematrix={z}&tilerow={y}&tilecol={x}'
let WFS_url = '' + http_url + '/geoserver/wfs?request=GetFeature&version=1.1.0&typeName=' + user_workspace + ':' + layer[i].name + '&maxFeatures=50&outputFormat=application/json'
let TMS_url = '' + http_url + '/geoserver/gwc/service/tms/1.0.0/' + user_workspace + ':' + layer[i].name + '@WebMercatorQuad@png/{z}/{x}/{-y}.png'
let TMS_url2 = '' + http_url + '/geoserver/gwc/service/tms/1.0.0/' + user_workspace + ':' + layer[i].name + '@EPSG:900913@png/{z}/{x}/{reverseY}.png'
let WMTS_url2 = '' + http_url + '/geoserver/gwc/service/wmts/rest/' + user_workspace + ':' + layer[i].name + '/{style}/{TileMatrixSet}/{TileMatrixSet}:{TileMatrix}/{TileRow}/{TileCol}?format=image/png'
let services = [
{
ServiceName: "WMS-二维",
URL: WMS_url
},
{
ServiceName: "WMS-三维",
URL: WMS_url2
},
{
ServiceName: "WFS",
URL: WFS_url
},
{
ServiceName: "TMS-二维",
URL: TMS_url
},
{
ServiceName: "TMS-三维",
URL: TMS_url2
},
{
ServiceName: "WMTS-二维",
URL: WMTS_url
},
{
ServiceName: "WMTS-三维",
URL: WMTS_url2
},
]
let temp = {}
temp.name = layer[i].name
temp.id = i + 1
temp.type = types[layer[i].name]
temp.service = services
Layer_big.push(temp)
}
}, 100);
setTimeout(() => {
res.send(Layer_big)
}, 200);
})
}
7. WFS获取Layer属性表
  • 核心:WFS请求,获取json格式属性数据,解析成友好的格式
  • 难点:从json数据中把属性数据提取出来
  • 思路:后端向Geoserver后台发送请求,获取Json格式的图层数据。把Json中的有用Attribute Table信息,push到数组中,将数组发送到前端。
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
//wfs服务获取指定Layer的属性表
exports.WFS_getFeatures = (req, res) => {
let attrList = []
let treeCol = new Array();
let WFSUrl = "http://localhost:8080/geoserver/wfs?"
var feature_url = WFSUrl + 'request=GetFeature&version=1.1.0&typeName=' + workspace_used + ':' + req.body.layer + '&outputFormat=application/json';
axios.get(feature_url).then((WFS_res) => {
//WFS请求,以json格式返回制定图层的属性数据
let GeoObject = WFS_res.data
console.log(GeoObject)
// res.send(typeof(GeoObject))
try {
for (atr in GeoObject.features[0].properties) {
attrList.push(atr)
}
// res.send(attrList)
let attrLen = Object.keys(GeoObject.features[0].properties).length
let featureCount = GeoObject.totalFeatures

for (var i = 0; i < attrLen; i++) {
treeCol[i] = new Array();
for (var j = 0; j < featureCount; j++) {
treeCol[i][j] = GeoObject.features[j].properties[attrList[i]];
//拼接属性表
}
}
} catch (error) {
console.log("Error:" + error)
}
res.send({ Attributes: attrList, Value: treeCol })
})
}

暑期项目总结
https://anonymouslosty.ink/2022/09/01/暑期项目总结/
作者
Ling yi
发布于
2022年9月1日
更新于
2022年9月21日
许可协议