文章基于我拆自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>com.walkgis.utils</groupId>
<artifactId>common-geojson</artifactId>
<version>2.2.0-RELEASE</version>
</dependency>
<!--实体转矢量切片的包-->
<dependency>
<groupId>com.walkgis.utils</groupId>
<artifactId>common-gs-vectortile</artifactId>
<version>2.2.0-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>com.walkgis.utils</groupId>
<artifactId>common-gs-vectortile</artifactId>
<version>2.2.0-RELEASE</version>
</dependency>
<dependency>
<groupId>com.walkgis.utils</groupId>
<artifactId>common-geojson</artifactId>
<version>2.2.0-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