最近一個項(xiàng)目遇到了需要對接geoserver的需求,要定時同步固定目錄下的tif到geoserver中,并發(fā)布成圖層組給前端使用
首先需要將固定目錄mount 到服務(wù)器目錄上,這里參考了這些博客
Linux創(chuàng)建并掛載NAS:win10 https://blog.csdn.net/cyf12345678/article/details/126592249
win7 https://www.haozhuangji.com/xtjc/155916801.htmlhttps://www.xitongzhijia.net/xtjc/20111207/2349.html
https://blog.csdn.net/cyf12345678/article/details/126592249
核心的命令其實(shí)就下面這些
//liunx安裝掛載工具
yum install -y cifs-utils
//掛載共享目錄
mount -t cifs -o username=username,password=password //192.168.1.139/layer/win_share/ /data/share_data/win_share
打開文件 CIFS和SMB類似
smb://user_name:password@server_name/.... \server_name...
webdav http://ip:5005/.....
ftp://ip/....
geoserver
api文檔(英文):https://docs.geoserver.org/stable/en/user/rest/index.html#rest
docker部署:https://www.bilibili.com/read/cv17617766/
docker pull kartoza/geoserver
啟動腳本:docker run --restart=always -d -v /data/share_data:/etc/letsencrypt -p 31880:8080 --name geoserver kartoza/geoserver
注意mount之后要重新啟動一下geoserver才會出現(xiàn)新掛載的目錄:docker restart geoserver
admin的初始密碼在日志里

添加用戶:admin的初始密碼在日志里,需要重新添加用戶(別忘了給新增的用戶加上角色)并設(shè)置密碼


新建工作空間

新建存儲倉庫:tif文件需要先上傳到部署geoserver的服務(wù)器的/data/share_data目錄下

點(diǎn)擊發(fā)布
圖層可以使用
以上為基礎(chǔ)設(shè)施的建設(shè),下面是代碼方面

架構(gòu)很簡單,就是通過mount掛載電腦或者nas上的文件夾到服務(wù)器的data_share目錄下,Java程序定時掃描該目錄下的tif文件,再發(fā)布到geoserver服務(wù)上并拼裝圖層組的url給前端使用。
Maven引入geoserver-manager的sdk:
<dependency>
<groupId> nl.pdok</groupId>
<artifactId>geoserver-manager</artifactId>
<version>1.7.0-pdok2</version>
</dependency>
geoserver配置:
import it.geosolutions.geoserver.rest.GeoServerRESTManager;
import java.net.URL;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
@Configuration
public class GeoServerConfig {
@Resource
private GeoServerProperties geoServerProperties;
@Bean(name = "geoServerRESTManager")
public GeoServerRESTManager geoServerRESTManager() {
try {
return new GeoServerRESTManager(new URL(geoServerProperties.getEndpoint()),
geoServerProperties.getUsername(), geoServerProperties.getPassword());
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}
}
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "spring.geoserver")
public class GeoServerProperties {
private String endpoint;
private String username;
private String password;
private String workSpaceName;
}
spring:
geoserver:
endpoint: http://${global.geoserver.ip}:${global.geoserver.port}/geoserver
username: username
password: password
workSpaceName: test
具體代碼:其中需要注意的是我寫了一個規(guī)則來根據(jù)文件的目錄對應(yīng)掛載主機(jī)的ip(需要這個ip+目錄可以直接訪問到對應(yīng)主機(jī)上的文件)
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jingansi.suitation.config.GeoServerProperties;
import com.jingansi.suitation.dao.mapper.LayerGroupMapper;
import com.jingansi.suitation.dao.mapper.LayerMapper;
import com.jingansi.suitation.dao.model.Layer;
import com.jingansi.suitation.dao.model.LayerGroup;
import com.jingansi.suitation.dao.model.Point;
import com.jingansi.suitation.enums.LayerGroupAffiliation;
import com.jingansi.suitation.exception.BizException;
import com.jingansi.suitation.model.LayerGroupDTO;
import com.jingansi.suitation.model.RangeReq;
import com.jingansi.suitation.service.GeoServerService;
import com.jingansi.suitation.util.FileUtil;
import it.geosolutions.geoserver.rest.GeoServerRESTManager;
import it.geosolutions.geoserver.rest.decoder.RESTCoverageStore;
import it.geosolutions.geoserver.rest.decoder.RESTLayerGroup;
import it.geosolutions.geoserver.rest.encoder.GSLayerEncoder;
import it.geosolutions.geoserver.rest.encoder.GSLayerGroupEncoder;
import it.geosolutions.geoserver.rest.encoder.coverage.GSImageMosaicEncoder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.util.*;
@Slf4j
@Service
public class GeoServerServiceImpl extends ServiceImpl<LayerMapper, Layer> implements GeoServerService {
@Resource
private GeoServerProperties geoServerProperties;
@Resource
private GeoServerRESTManager geoServerRESTManager;
@Resource
private LayerGroupMapper layerGroupMapper;
@Resource
private LayerMapper layerMapper;
@Value("${mount.base.shareContainerPath:/etc/letsencrypt/}")
private String shareContainerPath;
@Value("${mount.base.localPathRule:win_share:192.168.1.139,win_share2:192.168.1.14,nas_share:192.168.1.19}")
private String localPathRule;
@Override
public List<LayerGroupDTO> layerGroupList() {
List<LayerGroupDTO> layerGroups = new ArrayList<>();
LambdaQueryWrapper<LayerGroup> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(LayerGroup::getLayerGroupAffiliation, LayerGroupAffiliation.SAR);
List<LayerGroup> sarLayerGroups = layerGroupMapper.selectList(queryWrapper);
layerGroups.add(LayerGroupDTO.builder().layerGroupAffiliation(LayerGroupAffiliation.SAR.name()).count(sarLayerGroups.size()).data(sarLayerGroups).build());
return layerGroups;
}
@Override
public List<Layer> layerList(Long layerGroupId) {
LambdaQueryWrapper<Layer> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Layer::getLayerGroupId, layerGroupId);
return layerMapper.selectList(queryWrapper);
}
/**
* 找出經(jīng)緯度在范圍里的圖層
* 需要記錄圖層的上下左右四個點(diǎn)的經(jīng)緯度
**/
@Override
public List<Layer> rangeList(RangeReq rangeReq) {
List<Layer> rangeLayers = new ArrayList<>();
LambdaQueryWrapper<Layer> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(Layer::getLayerGroupId, rangeReq.getLayerGroupIds());
//查詢圖層組的圖層
List<Layer> groupLayers = layerMapper.selectList(queryWrapper);
groupLayers.forEach(layer -> {
List<Point> ps = new ArrayList<>();
ps.add(Point.builder().longitude(layer.getTopLeftLongitude()).latitude(layer.getTopLeftLatitude()).build());
ps.add(Point.builder().longitude(layer.getTopRightLongitude()).latitude(layer.getTopRightLatitude()).build());
ps.add(Point.builder().longitude(layer.getDownRightLongitude()).latitude(layer.getDownRightLatitude()).build());
ps.add(Point.builder().longitude(layer.getDownLeftLongitude()).latitude(layer.getDownLeftLatitude()).build());
//判斷經(jīng)緯度是否在圖層內(nèi)
if (containsLngLat(ps.toArray(new Point[4]), Point.builder().longitude(rangeReq.getLongitude()).latitude(rangeReq.getLatitude()).build())) {
rangeLayers.add(layer);
}
});
return rangeLayers;
}
/**
* 判斷點(diǎn)是否在多邊形內(nèi)(該方法來自前端,不保證其正確性--部分圖像中心點(diǎn)會出現(xiàn)不在范圍內(nèi)的情況(高緯度等偏斜情形)--要順時針或者逆時針順序)
*
* @param {*} points [{lng,lat}]
* @param {*} point {lng,lat}
* @returns
*/
public static Boolean containsLngLat(Point[] points, Point point) {
try {
// 數(shù)組長度
int length = points.length;
boolean c = false;
for (int i = 0, j = length - 1; i < length; ) {
if (
points[i].getLatitude() > point.getLatitude() != points[j].getLatitude() > point.getLatitude() &&
point.getLongitude() <
((points[j].getLongitude() - points[i].getLongitude()) * (point.getLatitude() - points[i].getLatitude())) /
(points[j].getLatitude() - points[i].getLatitude()) +
points[i].getLongitude()
) {
c = !c;
}
j = i;
i += 1;
}
return c;
} catch (Exception e) {
e.printStackTrace();
log.error("containsLngLat point:{} points:{}", point, points);
return false;
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long addLayerGroup(LayerGroup layerGroup) {
try {
Date now = new Date();
List<Layer> layers = new ArrayList<>();
//處理圖層
ArrayList<String> layersList = handleLayer(layerGroup, layers);
if (!CollectionUtils.isEmpty(layers)) {
//創(chuàng)建并發(fā)布圖層組
createLayerGroup(layerGroup.getLayerGroupName(), layersList);
//默認(rèn)圖層組為SAR
if (StringUtils.isBlank(layerGroup.getLayerGroupAffiliation())) {
layerGroup.setLayerGroupAffiliation(LayerGroupAffiliation.SAR.name());
}
setLayerGroupUrlAndBbox(layerGroup);
layerGroup.setGmtCreate(now);
layerGroup.setGmtModified(now);
//保存圖層組
layerGroupMapper.insert(layerGroup);
layers.forEach(layer -> {
layer.setLayerGroupId(layerGroup.getId());
layer.setGmtCreate(now);
layer.setGmtModified(now);
layer.setGmtCreateBy(layerGroup.getGmtCreateBy());
layer.setGmtModifiedBy(layerGroup.getGmtModifiedBy());
});
//保存圖層
saveBatch(layers);
}
} catch (BizException e) {
log.error("addLayerGroup BizException error:" + JSON.toJSONString(layerGroup), e);
throw e;
} catch (Exception e) {
log.error("addLayerGroup error:" + JSON.toJSONString(layerGroup), e);
throw new BizException("新增圖層失敗");
}
return layerGroup.getId();
}
@Override
public ArrayList<String> handleLayer(LayerGroup layerGroup, List<Layer> layers) throws Exception {
//fileDirectory 處理
String fileDirectory = shareContainerPath + layerGroup.getLayerGroupDirectory();
//圖層組名稱如果為空則設(shè)置為入?yún)⑻幚砗蟮哪夸浀腗D5值---前端必填字段,走不到這里
if (StringUtils.isBlank(layerGroup.getLayerGroupName())) {
layerGroup.setLayerGroupName(DigestUtils.md5Hex(fileDirectory));
}
File layerDirectory = FileUtil.getFile(fileDirectory);
String[] layerPaths = layerDirectory.list();
ArrayList<String> layersList = handleLayer(layers, fileDirectory, layerPaths);
if (CollectionUtils.isEmpty(layersList)) {
throw new BizException("新增圖層失?。簣D層文件夾下未讀取到tif圖層");
}
return layersList;
}
@Override
public ArrayList<String> handleLayer(List<Layer> layers, String fileDirectory, String[] layerPaths) {
ArrayList<String> layersList = new ArrayList<>();
Map<String, String> pathIpMap = getPathIpMap();
if (layerPaths != null) {
for (String layerPath : layerPaths) {
if (StringUtils.isBlank(layerPath)) {
continue;
}
try {
if (layerPath.endsWith(".tif")) {
String layerName = layerPath.split(".tif")[0];
Layer layer = new Layer();
//解析配置文件中的目錄與ip關(guān)系
String ip = "";
for (Map.Entry<String, String> entry : pathIpMap.entrySet()) {
if (fileDirectory.contains(entry.getKey())) {
ip = entry.getValue();
}
}
layer.setLayerTifUrl(ip + fileDirectory.replace(shareContainerPath, "") + layerPath);
layer.setLayerName(layerName);
layersList.add(geoServerProperties.getWorkSpaceName() + ":" + layerName);
//推送圖層數(shù)據(jù)
publishTiffData(layerName, fileDirectory + "/" + layerPath);
layers.add(layer);
}
} catch (Exception e) {
log.error("圖層解析失敗", e);
}
}
}
return layersList;
}
/**
* 獲取ip路徑映射Map
*
* @return {@link Map}<{@link String}, {@link String}>
*/
@Override
public Map<String, String> getPathIpMap() {
String[] rules = localPathRule.split(";");
Map<String, String> pathIpMap = new HashMap<>(16);
for (String rule : rules) {
String[] pathIp = rule.split(",");
pathIpMap.put(pathIp[0], pathIp[1]);
}
return pathIpMap;
}
private void setLayerGroupUrlAndBbox(LayerGroup layerGroup) {
String layerGroupName = layerGroup.getLayerGroupName();
RESTLayerGroup restLayerGroup = geoServerRESTManager.getReader().getLayerGroup(geoServerProperties.getWorkSpaceName(), layerGroupName);
String nativeCrs = restLayerGroup.getCRS();
layerGroup.setLayer(geoServerProperties.getWorkSpaceName() + ":" + layerGroupName);
layerGroup.setBboxMinX(restLayerGroup.getMinX());
layerGroup.setBboxMinY(restLayerGroup.getMinY());
layerGroup.setBboxMaxX(restLayerGroup.getMaxX());
layerGroup.setBboxMaxY(restLayerGroup.getMaxY());
//返回圖層預(yù)覽wms地址
/*return geoServerProperties.getEndpoint() + "/" + geoServerProperties.getWorkSpaceName() + "/wms?service=WMS&version=1.1.0&request=GetMap&layers=" + geoServerProperties.getWorkSpaceName() + ":" + layerGroupName + "&bbox="
+ nativeMinX + "," + nativeMinY + "," + nativeMaxX + "," + nativeMaxY +
//todo 寬高寫死了
"&width=768&height=330&srs=" + nativeCrs + "&styles=&format=application/openlayers";*/
//返回圖層預(yù)覽緩存gwc地址-wmts
layerGroup.setLayerGroupUrl(geoServerProperties.getEndpoint() + "/gwc/demo/" + geoServerProperties.getWorkSpaceName() + ":" + layerGroupName + "?gridSet="
+ nativeCrs + "&format=image/png");
}
/**
* 創(chuàng)建圖層組
* @param layerGroupName 圖層組名稱
* @param layersList 圖層名稱隊列:格式為WorkSpace:圖層名稱 eg: ja-test:tiff-test
* @return 是否成功
*/
private boolean createLayerGroup(String layerGroupName, ArrayList<String> layersList) {
GSLayerGroupEncoder gsLayerGroupEncoder = new GSLayerGroupEncoder();
gsLayerGroupEncoder.setWorkspace(geoServerProperties.getWorkSpaceName());
gsLayerGroupEncoder.setName(layerGroupName);
for (String layer : layersList) {
gsLayerGroupEncoder.addLayer(layer);
}
return geoServerRESTManager.getPublisher().createLayerGroup(geoServerProperties.getWorkSpaceName(), layerGroupName, gsLayerGroupEncoder);
}
/**
* 調(diào)用geoserver為圖層組配置圖層
*/
@Override
public boolean configureLayerGroup(String layerGroupName, ArrayList<String> layersList) {
GSLayerGroupEncoder gsLayerGroupEncoder = new GSLayerGroupEncoder();
gsLayerGroupEncoder.setWorkspace(geoServerProperties.getWorkSpaceName());
gsLayerGroupEncoder.setName(layerGroupName);
for (String layer : layersList) {
gsLayerGroupEncoder.addLayer(layer);
}
return geoServerRESTManager.getPublisher().configureLayerGroup(geoServerProperties.getWorkSpaceName(), layerGroupName, gsLayerGroupEncoder);
}
/**
* 發(fā)布tif格式圖層
*
* @param storeName 目錄名稱/圖層名稱
* @param fileDirectory tif文件全路徑
*/
private void publishTiffData(String storeName, String fileDirectory) throws Exception {
GSImageMosaicEncoder gsCoverageEncoder = new GSImageMosaicEncoder();
//設(shè)置坐標(biāo)系
gsCoverageEncoder.setSRS("EPSG:4326");
gsCoverageEncoder.setName(storeName);
//todo 去斜拍照片的黑邊,但是會導(dǎo)致圖層發(fā)白,清晰度丟失,暫無解決辦法(樣式?)
gsCoverageEncoder.setInputTransparentColor("#000000");
GSLayerEncoder layerEncoder = new GSLayerEncoder();
RESTCoverageStore publish = geoServerRESTManager.getPublisher().publishExternalGeoTIFF(geoServerProperties.getWorkSpaceName(), storeName, new File(fileDirectory), gsCoverageEncoder, layerEncoder);
log.info("publishTiffData store:{} fileDirectory:{} publish (TIFF文件發(fā)布狀態(tài)) : {}", storeName, fileDirectory, publish);
}
}
上面的LayerGroup和Layer實(shí)體類:
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
@TableName(value ="layer_group")
@Data
public class LayerGroup implements Serializable {
/**
*
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 設(shè)備id
*/
private String deviceId;
/**
* 圖層組名稱
*/
private String layerGroupName;
/**
* 圖層組所屬:SAR/CCD
*/
private String layerGroupAffiliation;
/**
* 圖層組bbox
*/
private Double bboxMinX;
/**
* 圖層組bbox
*/
private Double bboxMinY;
/**
* 圖層組bbox
*/
private Double bboxMaxX;
/**
* 圖層組bbox
*/
private Double bboxMaxY;
/**
* 圖層組路徑
*/
private String layerGroupUrl;
/**
* 圖層組存儲目錄
*/
private String layerGroupDirectory;
/**
* 創(chuàng)建時間
*/
private Date gmtCreate;
/**
* 更新時間
*/
private Date gmtModified;
/**
* 創(chuàng)建者id
*/
private String gmtCreateBy;
/**
* 更新者id
*/
private String gmtModifiedBy;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
/**
* 圖層組名稱 workspace:name
*/
private String layer;
}
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
@TableName(value ="layer")
@Data
public class Layer implements Serializable {
/**
*
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 所屬圖層組ID
*/
private Long layerGroupId;
/**
* 設(shè)備id
*/
private String deviceId;
/**
* 圖層名稱
*/
private String layerName;
/**
* 圖層tif圖片文件路徑
*/
private String layerTifUrl;
/**
* 拓展字段
*/
private String layerExtend;
/**
* 創(chuàng)建時間
*/
private Date gmtCreate;
/**
* 更新時間
*/
private Date gmtModified;
/**
* 創(chuàng)建者id
*/
private String gmtCreateBy;
/**
* 更新者id
*/
private String gmtModifiedBy;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}
遇到的坑:
1.拼裝的geoserver預(yù)覽url加載很慢

這個問題困擾了我整整2天時間,中間各種百度和求助前同事,均無頭緒。
嘗試過加配置,集群,換交換機(jī),甚至修改源碼了,最后才發(fā)現(xiàn)問題出在了mount掛載這一步
原來受限于nas和服務(wù)器間的網(wǎng)線是百兆且是二手機(jī)械盤,導(dǎo)致nas往服務(wù)器copy文件的速度在1Mb/s-10Mb/s,而一張100多Mb的tif圖片光傳輸都要10多秒,所以通過top看geoserver根本的資源消耗根本不大,后來我嘗試直接把文件拷貝到服務(wù)器里請求速度直接起飛到幾毫秒,然后就是更換nas網(wǎng)線為千兆線,硬盤換位ssd后速度達(dá)到幾秒一次
2.原圖發(fā)布斜拍的會有黑邊,setInputTransparentColor后黑邊沒了,但是會導(dǎo)致圖層發(fā)白,清晰度丟失,暫無解決辦法,或許可以通過設(shè)置樣式來改善,但是時間有限就沒有試過了