基於實時向量切片的要素繪製Demo
阿新 • • 發佈:2019-01-03
文章基於我拆自Geoserver的向量切片外掛中的程式碼做的一個封裝:https://github.com/polixiaohai/mvn-repository。其中有兩個比較成熟的封裝,有需要的朋友可以自行使用。
maven中倉庫配置:
<repository>
<id>maven-repo-master</id>
<url>https://raw.github.com/polixiaohai/mvn-repository/master/</url>
</repository>
包POM:
<!--一個實體轉geojson的包--> <dependency> <groupId>cn.com.enersun.dgpmicro</groupId> <artifactId>common-geojson</artifactId> <version>2.0.2-RELEASE</version> </dependency> <!--實體轉向量切片的包--> <dependency> <groupId>cn.com.enersun.dgpmicro</groupId> <artifactId>common-gs-vectortile</artifactId> <version>2.0.7-RELEASE</version> </dependency>
工程使用的是SpringBoot,ORM框架使用的是國產ibeetlSQL,如果不喜歡可以自行使用其他的,其他的廢話不多說,上程式碼:
POM檔案:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.1.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>cn.com.walkgis.microdraw</groupId> <artifactId>walkgis-draw</artifactId> <version>0.0.1-SNAPSHOT</version> <name>walkgis-draw</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>com.ibeetl</groupId> <artifactId>beetlsql</artifactId> <version>2.10.40</version> </dependency> <dependency> <groupId>cn.com.enersun.dgpmicro</groupId> <artifactId>common-gs-vectortile</artifactId> <version>2.0.7-RELEASE</version> </dependency> <dependency> <groupId>cn.com.enersun.dgpmicro</groupId> <artifactId>common-geojson</artifactId> <version>2.0.2-RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.2.4</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
表結構(使用的PG資料庫)如下:
create table if not exists public.t_poi ( id serial not null constraint t_area_pkey primary key, name varchar(100) not null, type integer not null ); select addGeometryColumn('public','t_poi','shape',4326,'Point',2); create table if not exists public.t_river ( id serial not null constraint t_river_pkey primary key, name varchar(100) not null, type integer not null ); select addGeometryColumn('public','t_river','shape',4326,'LineString',2); create table if not exists public.t_build ( id serial not null constraint t_build_pkey primary key, name varchar(100) not null, type integer not null ); select addGeometryColumn('public','t_build','shape',4326,'Polygon',2);
其中資料來源配置和本地測試的時候,SpringBoot跨域設定就不貼出程式碼來了,可以自行百度。
實體貼出一個來:
public class TBuild implements GeoEntity<Integer> {
private Integer id;
private Integer type;
private String name;
private Object shape;
public TBuild() {
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getType() {
return type;
}
public void setType(Integer type) {
this.type = type;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Object getShape() {
return shape;
}
public void setShape(Object shape) {
this.shape = shape;
}
}
實體儲存的Controller,其他實體儲存的Controller可以對照進行實現:
@Controller
@RequestMapping(value = "build")
public class BuildController extends GeoJsonServicesImpl<TBuild, Integer> {
@Autowired
@Qualifier("sqlManagerFactoryBeanGIS")
private SQLManager sqlManagerGIS;
@RequestMapping(value = "save")
@ResponseBody
public Integer save(@RequestParam("feature") String feature) {
ObjectMapper mapper = new ObjectMapper();
try {
Feature fea = mapper.readValue(feature, Feature.class);
TBuild build = new TBuild();
BeanUtils.populate(build, fea.getProperties());
Geometry geometry = geometryConvert.geometryDeserialize(fea.getGeometry());
if (geometry != null) {
PGobject pGobject = new PGobject();
pGobject.setType("Geometry");
geometry.setSRID(4326);
pGobject.setValue(WKBWriter.toHex(new WKBWriter(2, true).write(geometry)));
build.setShape(pGobject);
}
return sqlManagerGIS.insert(TBuild.class, build);
} catch (IOException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return -1;
}
}
生成向量切片的Controller,其中有部分對檔案做的防止多執行緒同時操作而作的鎖處理,以及利用Java中fork/join來並行下載切片的:
@Controller
@RequestMapping(value = "vectortile")
public class VectorTileControllerImpl extends VectorTileController {
@Value("${cache.vector-tile-geoserver-path}")
public String cachePath;
@Value("${region.split}")
public String regionSplit;
@Value("${cache.maxz}")
private Integer tmaxz;
@Value("${cache.minz}")
private Integer tminz;
public static Map<Integer, Long> info = new ConcurrentHashMap<>();// 總數 原子操作
public static AtomicInteger successCount = new AtomicInteger();// 總數 原子操作
public static AtomicInteger zoom = new AtomicInteger();// 總數 原子操作
private static final ForkJoinPool pool = new ForkJoinPool();
@Autowired
@Qualifier("sqlManagerFactoryBeanGIS")
private SQLManager sqlManagerGIS;
/**
* 進來的是XYZ scheme
*
* @param layerName
* @param x
* @param y
* @param z
* @return
*/
@RequestMapping(value = "vt/{z}/{x}/{y}.mvt", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity<InputStreamResource> generateVectorTiles(
@RequestParam(value = "layerName", defaultValue = "vtdemo") String layerName,
@RequestParam(value = "CRS") String crs,
@PathVariable("x") Integer x,
@PathVariable("y") Integer y,
@PathVariable("z") Integer z
) throws Exception {
ReadWriteLock lock = new ReentrantReadWriteLock();
final Lock readLock = lock.readLock();
final Lock writeLock = lock.writeLock();
File file = new File(cachePath + File.separator + layerName + File.separator + z + File.separator + x + File.separator + String.format("%d.%s", y, "mvt"));
if (!file.exists()) {
//#region 下載內容
double[] bboxs = new double[]{0, 0, 0, 0};
if (crs.equalsIgnoreCase("EPSG:4326"))
bboxs = new GlobalGeodetic("", 256).tileLatLonBounds(x, y, z);
else if (crs.equalsIgnoreCase("EPSG:3857"))
bboxs = new GlobalMercator(256).tileLatLonBounds(x, y, z);
else throw new Exception("不支援的地理座標系");
Map<String, List> entityMap = new ConcurrentHashMap<>();
String sql = "SELECT t.* FROM t_build t WHERE ST_Intersects (st_setsrid(t.shape,4326),ST_MakeEnvelope(" + bboxs[1] + "," + bboxs[0] + "," + bboxs[3] + "," + bboxs[2] + ",4326))";
List<TBuild> entityList = sqlManagerGIS.execute(new SQLReady(sql), TBuild.class);
if (entityList.size() > 0) entityMap.put("t_build", entityList);
sql = "SELECT t.* FROM t_river t WHERE ST_Intersects (st_setsrid(t.shape,4326),ST_MakeEnvelope(" + bboxs[1] + "," + bboxs[0] + "," + bboxs[3] + "," + bboxs[2] + ",4326))";
List<TRiver> tRiverList = sqlManagerGIS.execute(new SQLReady(sql), TRiver.class);
if (tRiverList.size() > 0) entityMap.put("t_river", tRiverList);
sql = "SELECT t.* FROM t_poi t WHERE ST_Intersects (st_setsrid(t.shape,4326),ST_MakeEnvelope(" + bboxs[1] + "," + bboxs[0] + "," + bboxs[3] + "," + bboxs[2] + ",4326))";
List<TPoi> tPois = sqlManagerGIS.execute(new SQLReady(sql), TPoi.class);
if (tPois.size() > 0) entityMap.put("t_poi", tPois);
try {
if (entityMap.size() <= 0)
return downloadFile(readLock, file);
byte[] res = produceMap(entityMap, bboxs);
if (res == null || res.length <= 0)
return downloadFile(readLock, file);
try {
writeLock.lock();
if (!file.getParentFile().exists()) file.getParentFile().mkdirs();
FileOutputStream fos = new FileOutputStream(file);
fos.write(res, 0, res.length);
fos.flush();
fos.close();
System.out.println("增加:" + file.getAbsolutePath());
} finally {
writeLock.unlock();
}
} catch (Exception ex) {
ex.printStackTrace();
}
return downloadFile(readLock, file);
//endregion
} else {
return downloadFile(readLock, file);
}
}
@RequestMapping(value = "buildCache/{layerName}")
@ResponseBody
public void buildCach(@PathVariable("layerName") String layerName,
@RequestParam(value = "CRS") String crs,
@RequestParam(value = "extent", required = false) String extent) {
String[] str = extent.split(",");
if (str.length == 4) {
Double xmin = Double.parseDouble(str[0]);
Double ymin = Double.parseDouble(str[1]);
Double xmax = Double.parseDouble(str[2]);
Double ymax = Double.parseDouble(str[3]);
Envelope envelope = new Envelope(xmin, xmax, ymin, ymax);
GlobalMercator mercator = new GlobalMercator(256);
double[] min = mercator.latLonToMeters(envelope.getMinY(), envelope.getMinX());
double[] max = mercator.latLonToMeters(envelope.getMaxY(), envelope.getMaxX());
//#region 計算
for (int tz = tmaxz; tz > tminz - 1; tz--) {
int[] tminxy = mercator.metersToTile(min[0], min[1], tz);
int[] tmaxxy = mercator.metersToTile(max[0], max[1], tz);
tminxy = new int[]{Math.max(0, tminxy[0]), Math.max(0, tminxy[1])};
tmaxxy = new int[]{(int) Math.min(Math.pow(2, tz) - 1, tmaxxy[0]), (int) Math.min(Math.pow(2, tz) - 1, tmaxxy[1])};
info.put(tz, (long) ((tmaxxy[1] - (tminxy[1] - 1)) * (tmaxxy[0] + 1 - tminxy[0])));
for (int tx = tminxy[0]; tx < tmaxxy[0] + 1; tx++) {
pool.execute(new DownloadTask(layerName, crs, tz, tminxy[1], tmaxxy[1], tx));
}
}
//endregion
}
}
@RequestMapping(value = "clearCache/{layerName}", method = RequestMethod.GET)
@ResponseBody
public Integer clearCache(@PathVariable("layerName") String layerName,
@RequestParam(value = "CRS") String crs,
@RequestParam(value = "extent", required = false) String extent) throws Exception {
if (null == extent) {
String filePath = cachePath + File.separator + layerName;
if (new File(filePath).exists()) {
new Thread(() -> FileUtils.delFolder(filePath)).start();
}
} else {
String[] str = extent.split(",");
if (str.length == 4) {
Envelope envelope = new Envelope(Double.parseDouble(str[0]), Double.parseDouble(str[2]), Double.parseDouble(str[1]), Double.parseDouble(str[3]));
double[] min = new double[0], max = new double[0];
GlobalMercator mercator = null;
GlobalGeodetic geodetic = null;
if (crs.equalsIgnoreCase("EPSG:4326")) {
geodetic = new GlobalGeodetic("", 256);
} else if (crs.equalsIgnoreCase("EPSG:3857")) {
mercator = new GlobalMercator(256);
min = mercator.latLonToMeters(envelope.getMinY(), envelope.getMinX());
max = mercator.latLonToMeters(envelope.getMaxY(), envelope.getMaxX());
} else throw new Exception("不支援的地理座標系");
//#region 計算
for (int tz = tmaxz; tz > tminz - 1; tz--) {
int[] tminxy = new int[0], tmaxxy = new int[0];
if ((crs.equalsIgnoreCase("EPSG:3857"))) {
tminxy = mercator.metersToTile(min[0], min[1], tz);
tmaxxy = mercator.metersToTile(max[0], max[1], tz);
} else if (crs.equalsIgnoreCase("EPSG:4326")) {
tminxy = geodetic.lonlatToTile(envelope.getMinX(), envelope.getMinY(), tz);
tmaxxy = geodetic.lonlatToTile(envelope.getMaxX(), envelope.getMaxY(), tz);
}
tminxy = new int[]{Math.max(0, tminxy[0]), Math.max(0, tminxy[1])};
tmaxxy = new int[]{(int) Math.min(Math.pow(2, tz) - 1, tmaxxy[0]), (int) Math.min(Math.pow(2, tz) - 1, tmaxxy[1])};
for (int tx = tminxy[0]; tx < tmaxxy[0] + 1; tx++) {
for (int ty = tmaxxy[1]; ty > tminxy[1] - 1; ty--) {
File file = new File(cachePath + File.separator + layerName + File.separator + tz + File.separator + tx + File.separator + ty + ".mvt");
if (file.exists())
file.delete();
System.out.println("刪除:" + file.getAbsolutePath());
}
}
}
return 1;
//endregion
}
}
return -1;
}
public ResponseEntity<InputStreamResource> downloadFile(Lock readLock, File filePath) {
if (filePath.exists()) {
try {
readLock.lock();
FileSystemResource file = new FileSystemResource(filePath);
return ResponseEntity.ok().contentLength(file.contentLength())
.contentType(MediaType.parseMediaType(MediaType.APPLICATION_OCTET_STREAM_VALUE))
.body(new InputStreamResource(file.getInputStream()));
} catch (IOException e) {
return ResponseEntity.noContent().build();
} finally {
readLock.unlock();
}
} else {
return ResponseEntity.noContent().build();
}
}
private class DownloadTask extends RecursiveTask<Void> {
private static final long serialVersionUID = 1L;
private static final int THRESHOLD = 1000;
private String layerName;
private String crs;
private int tz;
private int start;
private int end;
private int tx;
public DownloadTask(String layerName, String crs, int tz, int start, int end, int tx) {
this.layerName = layerName;
this.crs = crs;
this.tz = tz;
this.start = start;
this.end = end;
this.tx = tx;
}
@Override
protected Void compute() {
if (end - (start - 1) <= THRESHOLD) {
for (int ty = end; ty > start - 1; ty--) {
try {
generateVectorTiles(layerName, crs, tx, ty, tz);
} catch (Exception e) {
e.printStackTrace();
}
successCount.getAndIncrement();
zoom = new AtomicInteger(tz);
}
}
int m = (start - 1) + (end - (start - 1)) / 2;
DownloadTask task1 = new DownloadTask(layerName, crs, tz, start, m, tx);
DownloadTask task2 = new DownloadTask(layerName, crs, tz, m, end, tx);
invokeAll(task1, task2);
return null;
}
}
}
SpringBoot(application-local.yml,這裡還需要配置一個application.yml指定那個profile為active的)配置檔案部分 :
server:
port: 8084
tomcat:
uri-encoding: UTF-8
servlet:
context-path: /${spring.application.name}
spring:
profiles: local
datasource:
gis:
url: jdbc:postgresql://localhost:5432/gis
username: postgres
password: postgres
driver-class-name: org.postgresql.Driver
sql-script-encoding: utf-8
type: com.zaxxer.hikari.HikariDataSource
hikari:
connection-timeout: 30000
idle-timeout: 60000
max-lifetime: 1800000
maximum-pool-size: 60
minimum-idle: 0
application:
name: walkgis-draw
beetlsql:
ds:
gis:
sqlPath: /sql
basePackage: cn.com.walkgis.microdraw.walkgisdraw.dao
nameConversion: org.beetl.sql.core.UnderlinedNameConversion
daoSuffix: Dao
dbStyle: org.beetl.sql.core.db.PostgresStyle
mutiple:
datasource: gis
beetl-beetlsql: dev=false
# 存放快取檔案的地址
cache:
vector-tile-geoserver-path: E:\Data\tiles\vt-geoserver
maxz: 18
minz: 1
前端呼叫頁面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!--bootstrap-->
<link href="https://cdn.bootcss.com/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<!--jqueryUI-->
<link href="https://cdn.bootcss.com/jqueryui/1.12.1/jquery-ui.min.css" rel="stylesheet">
<link href="https://cdn.bootcss.com/jqueryui/1.12.1/jquery-ui.theme.min.css" rel="stylesheet">
<link href="https://cdn.bootcss.com/jqueryui/1.12.1/jquery-ui.theme.min.css" rel="stylesheet">
<!--openlayers-->
<link href="https://cdn.bootcss.com/openlayers/4.6.5/ol-debug.css" rel="stylesheet">
<script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="https://cdn.bootcss.com/jqueryui/1.12.1/jquery-ui.min.js"></script>
<script src="https://cdn.bootcss.com/openlayers/4.6.5/ol-debug.js"></script>
<style>
html, body {
width: 100%;
height: 100%;
}
#map {
width: 100%;
height: 100%;
border: 1px solid red;
}
#tools {
position: fixed;
left: 20px;
top: 40px;
z-index: 99999;
background: #fff;
}
#tools select {
width: 150px;
}
</style>
</head>
<body>
<div id="tools">
<select name="typeSelect" id="typeSelect">
<option value="Point" data="poi" selected="selected">t_poi</option>
<option value="LineString" data="river">t_river</option>
<option value="Polygon" data="build">t_build</option>
</select>
</div>
<div id="map"></div>
<div id="dialog" title="儲存對話方塊">
<form>
<div class="form-group">
<label for="inputName">名稱:</label>
<input type="email" class="form-control" name="name" id="inputName" placeholder="名稱">
</div>
<div class="form-group">
<label for="selectType">型別:</label>
<select class="form-control" name="type" id="selectType" placeholder="型別">
<option value="1" selected="selected">點</option>
<option value="2">線</option>
<option value="3">面</option>
</select>
</div>
</form>
</div>
<script type="text/javascript">
$(function () {
var map, draw, snap, baseLayer, baseSource, tempLayer;
baseSource = new ol.source.VectorTile({
format: new ol.format.MVT(),
url: 'http://localhost:8084/walkgis-draw/vectortile/vt/{z}/{x}/{-y}.mvt?CRS=EPSG:4326',
projection: "EPSG:4326",
extent: ol.proj.get("EPSG:4326").getExtent(),
tileSize: 256,
maxZoom: 21,
minZoom: 0,
wrapX: true
});
baseLayer = new ol.layer.VectorTile({
renderMode: "image",
preload: 12,
source: baseSource,
style: new ol.style.Style({
fill: new ol.style.Fill({
color: 'rgba(255, 0, 0, 0.2)'
}),
stroke: new ol.style.Stroke({
color: '#ff0000',
width: 2
}),
image: new ol.style.Circle({
radius: 7,
fill: new ol.style.Fill({
color: '#ff0000'
})
})
})
})
tempLayer = new ol.layer.Vector({
source: new ol.source.Vector()
})
function addInteractions() {
draw = new ol.interaction.Draw({
source: tempLayer.getSource(),
type: typeSelect.value,
style: new ol.style.Style({
fill: new ol.style.Fill({
color: 'rgba(255, 255, 255, 0.2)'
}),
stroke: new ol.style.Stroke({
color: '#ffcc33',
width: 2
}),
image: new ol.style.Circle({
radius: 7,
fill: new ol.style.Fill({
color: '#ffcc33'
})
})
})
});
draw.on('drawend', function (target) {
$("#dialog").dialog({
title: '儲存對話方塊',
dialogClass: "no-close",
width: 600,
height: 300,
modal: true,
buttons: {
'確 定': function () {
tempLayer.getSource().clear();
var attrs = $(this).find('form').serializeArray();
$(this).find('form')[0].reset();
attrs.forEach(function (item) {
target.feature.set(item.name, item.value);
})
var route = $("#typeSelect option:selected").attr("data")
$.ajax({
url: 'http://localhost:8084/walkgis-draw/' + route + "/save",
type: "GET",
data: {
feature: new ol.format.GeoJSON().writeFeature(target.feature)
}
}).done(function (re) {
if (re > 0) {
var extent = target.feature.getGeometry().getExtent().join(',')
$.ajax({
url: 'http://localhost:8084/walkgis-draw/vectortile/clearCache/vtdemo?CRS=EPSG:4326',
type: "GET",
data: {
extent: extent
}
}).done(function (re) {
baseLayer.getSource().clear();
baseLayer.getSource().dispatchEvent("change");
})
}
})
$(this).dialog('destroy');
},
'取 消': function () {
tempLayer.getSource().clear();
$(this).dialog('destroy');
}
}
});
})
snap = new ol.interaction.Snap({
source: tempLayer.getSource()
});
map.addInteraction(draw);
map.addInteraction(snap);
}
$("#typeSelect").change(function () {
map.removeInteraction(draw);
map.removeInteraction(snap);
addInteractions();
});
map = new ol.Map({
target: "map",
layers: [
new ol.layer.Tile({
source: new ol.source.OSM(),
projection: "EPSG:4326"
}),
baseLayer,
new ol.layer.Tile({
source: new ol.source.TileDebug({
projection: "EPSG:4326",
tileGrid: ol.tilegrid.createXYZ({
tileSize: [256, 256],
minZoom: 0,
maxZoom: 18,
extent: ol.proj.get("EPSG:4326").getExtent()
}),
wrapX: true
}),
projection: 'EPSG:4326'
}),
tempLayer
],
view: new ol.View({
center: [100, 25],
projection: "EPSG:4326",
zoom: 10
}),
projection: "EPSG:4326",
})
addInteractions();
})
</script>
</body>
</html>
效果如下:
工程下載地址:https://download.csdn.net/download/polixiaohai/10873787