前言
因为,笔者最近在做一个大数据量地图在线展示的项目,因此需要利用一些手段来提升效率,例如地图缩放的时候,需要展示浏览器窗口下的实时点位,轨迹等信息。在大比例尺下(也就是放大级别较大的时候),因此只需要展示目前这个窗口大小内的数据。那么怎么知道目前窗口大小内的有哪些点位信息呢,这个时候,R树的索引就出现了,可以很好地解决这个问题。利用一个矩形框(浏览器窗口大小)来进行R树索引就可以找出矩形框内的点位了。
R树索引是一种空间数据索引,具有高效的特点,在现实生活中,R树可以用来存储地图上的空间信息,例如餐馆地址,或者地图上用来构造街道,建筑,湖泊边缘和海岸线的多边形。然后可以用它来回答“查找距离我2千米以内的博物馆”,“检索距离我2千米以内的所有路段”(然后显示在导航系统中)或者“查找(直线距离)最近的加油站”这类问题。R树还可以用来加速使用包括大圆距离在内的各种距离度量方式的最邻近搜索。
具体原理和用途可以参考下面两个网页:
而目前有很多成熟的第三方库提供了这个功能,例如本文用到的JTS。
JTS的开发文档和相应的jar包的下载地址:JTS下载
JTS提供了如下的空间数据类型,还提供了读取各种空间描述文件(WTK等),线简化,空间操作(求交,计算距离,计算外包矩形等),建立空间索引等多种算法。
Point
MultiPoint
LineString
LinearRing 封闭的线条
MultiLineString 多条线
Polygon
MultiPolygon
GeometryCollection 包括点,线,面
里面最主要的几个类,GeometryFactory,Geometry,Envelope以及上面提到的几种常用数据类型。
因为本文中主要用到空间索引和空间拓扑关系的计算,因此下面重点讲解一下相关的内容。
1,几个概念
Geometry类:所有的空间数据类型,点,线,面,多点,环,多边形等等都是继承自Geometry类的。
Envelope类:该类就是描述一个空间几何对象的外包矩形,由max_x,max_y,min_x,min_y组成。
2,空间关系
空间关系主要是由九交模型来描述的,九交模型的讲解可以参考:九交模型的讲解
至于在JTS中的对应的关系,就是以下几种:
3,空间索引
空间索引有很多种,下面就介绍网格索引和R树索引,这部分很成熟,网格索引是最原始的空间索引了,R树稍微“时髦”一点,效率更高。
这两个可以参看:
地理信息系统原理在线教程——空间索引
深入浅出空间索引:为什么需要空间索引
深入浅出空间索引:2
相应的,如何利用JTS实现R树索引呢?
以下是相应的代码
SpatialUtil.java
package util;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.CoordinateSequences;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.geom.impl.CoordinateArraySequence;
import com.vividsolutions.jts.index.strtree.STRtree;
import config.GeoConfig;
import dao.QueryTrail;
import entity.CompareValue;
import entity.GPSPoint;
import entity.Grid;
import entity.Line;
import entity.Point;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.client.HttpResponse;
import io.vertx.ext.web.client.WebClient;
public class SpatialUtil {
static String result=new String();
//下面是我用高德地图API和JTS求解武汉市的边界多边形之后,求解出的武汉的外包矩形范围(里面用了vertx中的client)
public static void getPolygonofWuhan(String[] args) {
Vertx vertx = Vertx.vertx();
WebClient client = WebClient.create(vertx);
client.get(80, "restapi.amap.com",
"/v3/config/district?keywords=%E6%AD%A6%E6%B1%89&subdistrict=0&key=高德地图API申请的Token&extensions=all")
.send(ar -> {
if (ar.succeeded()) {
// Obtain response
HttpResponse<Buffer> response = ar.result();
result = response.bodyAsString();
JsonObject jo = new JsonObject(result);
result = jo.getJsonArray("districts").getJsonObject(0).getString("polyline");
GeometryFactory factory=new GeometryFactory();
String[] xyStrings=result.replace("|", ";").split(";");
List<Coordinate> list=new ArrayList<>();
for(String xy:xyStrings){
String[] s_arr=xy.split(",");
double[] d_xy=new double[2];
d_xy[0]=Double.parseDouble(s_arr[0]);
d_xy[1]=Double.parseDouble(s_arr[1]);
Coordinate coor=new Coordinate(d_xy[0], d_xy[1]);
list.add(coor);
}
Coordinate[] coor_arr=list.toArray(new Coordinate[0]);
MultiPoint multiPoint=factory.createMultiPoint(coor_arr);
Geometry env=multiPoint.getEnvelope();
Coordinate[] MBR=env.getCoordinates();
for(int i=0;i<MBR.length;i++){
System.out.println(MBR[i].x+","+MBR[i].y);
}
client.close();
vertx.close();
} else {
System.out.println("Something went wrong " + ar.cause().getMessage());
}
});
}
/**
* 计算两点的距离差在哪个阈值范围内
* @param pt1
* @param pt2
* @param config 阈值的设置
* @return
*/
public static CompareValue inTolerance(Point pt1,Point pt2,GeoConfig config){
double delta=Math.sqrt(Math.pow(pt1.getX()-pt2.getX(),2)+Math.pow(pt1.getY()-pt2.getY(), 2));
double max=config.getMaxGeoRange();
double min=config.getMinGeoRange();
if(delta<min){
return CompareValue.LT;
}else if(delta<=max&&delta>=min){
return CompareValue.IN;
}else{
return CompareValue.GT;
}
}
/**
* 建立网格
* @return
*/
public static HashMap<String,Grid> createGrids(){
HashMap<String,Grid> gridMap=new HashMap<>();
double left_top_x=Double.parseDouble(PropertiesUtil.getProperties("common", "left-top").split(",")[0]);
double left_top_y=Double.parseDouble(PropertiesUtil.getProperties("common", "left-top").split(",")[1]);
double right_bottom_x=Double.parseDouble(PropertiesUtil.getProperties("common", "right-bottom").split(",")[0]);
double right_bottom_y=Double.parseDouble(PropertiesUtil.getProperties("common", "right-bottom").split(",")[1]);
int rows=Integer.parseInt(PropertiesUtil.getProperties("common", "rows"));
int cols=Integer.parseInt(PropertiesUtil.getProperties("common", "cols"));
double interval_x=Double.parseDouble(ParseDataType.parseD2s((right_bottom_x-left_top_x)/(cols*1.0),6));
double interval_y=Double.parseDouble(ParseDataType.parseD2s((left_top_y-right_bottom_y)/(rows*1.0),6));
for(int i=0;i<rows;i++){
for(int j=0;j<cols;j++){
Grid grid=new Grid();
grid.setCol(cols);
grid.setRow(rows);
grid.setIndex(i*cols+j+1);
Point lefttop=new Point();
Point rightbottom=new Point();
lefttop.setX(left_top_x+j*interval_x);
lefttop.setY(left_top_y-i*interval_y);
if(j==cols-1){
rightbottom.setX(right_bottom_x);
}else{
rightbottom.setX(left_top_x+(j+1)*interval_x);
}
if(i==rows-1){
rightbottom.setY(right_bottom_y);
}else{
rightbottom.setY(left_top_y-(i+1)*interval_y);
}
grid.setLefttop(lefttop);
grid.setRightbottom(rightbottom);
gridMap.put(String.valueOf(grid.getRow())+"_"+String.valueOf(grid.getCol())+"_"+String.valueOf(grid.getIndex()),grid);
}
}
return gridMap;
}
/**
* 建立网格索引
*/
public static HashMap<String, Grid> createGridIndex(){
HashMap<String, Grid> gridmap=createGrids();
int cols=Integer.parseInt(PropertiesUtil.getProperties("common", "cols"));
int rows=Integer.parseInt(PropertiesUtil.getProperties("common", "rows"));
String rbPt_s=PropertiesUtil.getProperties("common", "right-bottom");
Point rbPt=new Point();
rbPt.setX(Double.parseDouble(rbPt_s.split(",")[0]));
rbPt.setY(Double.parseDouble(rbPt_s.split(",")[1]));
String ltPt_s=PropertiesUtil.getProperties("common", "left-top");
Point ltPt=new Point();
ltPt.setX(Double.parseDouble(ltPt_s.split(",")[0]));
ltPt.setY(Double.parseDouble(ltPt_s.split(",")[1]));
double range_x=rbPt.getX()-ltPt.getX();
double range_y=ltPt.getY()-rbPt.getY();
QueryTrail query=new QueryTrail();
HashMap<String, Line> map=query.getLine();
GeoConfig config=new GeoConfig();
config.setMaxGeoRange(Double.parseDouble(PropertiesUtil.getProperties("common", "maxGeoRange")));
config.setMinGeoRange(Double.parseDouble(PropertiesUtil.getProperties("common", "minGeoRange")));
GeometryFactory factory=new GeometryFactory();
for(Entry<String, Line> entry:map.entrySet()){
Line templine=entry.getValue().sort(true);
List<Line> list=templine.filter(config);
for(Line line:list){
List<GPSPoint> gpslist=line.getCoors();
List<Coordinate> coors=new ArrayList<>();
for(GPSPoint xy:gpslist){
double x=xy.getX();
double y=xy.getY();
Coordinate coor=new Coordinate(x, y);
coors.add(coor);
}
Coordinate[] coor_arr=coors.toArray(new Coordinate[0]);
if(coor_arr.length>1){
LineString l_s=factory.createLineString(coor_arr);
Envelope env=l_s.getEnvelopeInternal();
double max_x=env.getMaxX();
double min_x=env.getMinX();
double max_y=env.getMaxY();
double min_y=env.getMinY();
int max_j=(int)((max_x-ltPt.getX())/range_x*cols);
int max_i=(int)((ltPt.getY()-max_y)/range_y*rows);
int min_j=(int)((min_x-ltPt.getX())/range_x*cols);
int min_i=(int)((ltPt.getY()-min_y)/range_y*rows);
a:for(int i=max_i;i<=min_i;i++){
for(int j=min_j;j<=max_j;j++){
Grid grid=gridmap.get(String.valueOf(rows)+"_"+String.valueOf(cols)+"_"+String.valueOf(i*cols+j+1));
Coordinate[] coor_arr1=new Coordinate[5];
coor_arr1[0]=new Coordinate(grid.getLefttop().getX(), grid.getLefttop().getY());
coor_arr1[1]=new Coordinate(grid.getLefttop().getX(), grid.getRightbottom().getY());
coor_arr1[2]=new Coordinate(grid.getRightbottom().getX(), grid.getRightbottom().getY());
coor_arr1[3]=new Coordinate(grid.getRightbottom().getX(), grid.getLefttop().getY());
coor_arr1[4]=new Coordinate(grid.getLefttop().getX(), grid.getLefttop().getY());
CoordinateArraySequence seq=new CoordinateArraySequence(coor_arr1);
LinearRing ring = new LinearRing(seq, new GeometryFactory());
Polygon poly=new Polygon(ring, null, new GeometryFactory());
if(l_s.crosses(poly)||poly.covers(l_s)){
grid.addLine(line);
break a;
}
}
}
}else{
GPSPoint point=gpslist.get(0);
int j=(int)((point.getX()-ltPt.getX())/range_x*cols);
int i=(int)((ltPt.getY()-point.getY())/range_y*rows);
Grid grid=gridmap.get(String.valueOf(rows)+"_"+String.valueOf(cols)+"_"+String.valueOf(i*cols+j+1));
grid.addLine(line);
}
}
}
System.out.println("网格索引创建成功!");
return gridmap;
}
/**
* 建立R树索引
* @return
*/
public static STRtree createRtree(){
QueryTrail query=new QueryTrail();
HashMap<String, Line> map=query.getLine();
GeoConfig config=new GeoConfig();
config.setMaxGeoRange(Double.parseDouble(PropertiesUtil.getProperties("common", "maxGeoRange")));
config.setMinGeoRange(Double.parseDouble(PropertiesUtil.getProperties("common", "minGeoRange")));
STRtree tree=new STRtree();
for(Entry<String, Line> entry:map.entrySet()){
Line templine=entry.getValue().sort(true);
List<Line> list=templine.filter(config);
for(Line line:list){
GeometryFactory factory=new GeometryFactory();
List<Coordinate> coors=new ArrayList<>();
List<GPSPoint> gpslist=line.getCoors();
for(GPSPoint xy:gpslist){
double x=xy.getX();
double y=xy.getY();
Coordinate coor=new Coordinate(x, y);
coors.add(coor);
}
Coordinate[] coor_arr=coors.toArray(new Coordinate[0]);
if(coor_arr.length>1){
LineString lineStr=factory.createLineString(coor_arr);
Envelope env=lineStr.getEnvelopeInternal();
tree.insert(env, lineStr);
}else{
com.vividsolutions.jts.geom.Point point=factory.createPoint(coor_arr[0]);
Envelope env=point.getEnvelopeInternal();
tree.insert(env, point);
}
}
}
tree.build();
System.out.println("R树索引创建成功!");
return tree;
}
/**
* R树查询
* @param tree
* @param searchGeo
* @return
*/
public static List<Geometry> query(STRtree tree,Geometry searchGeo){
List <Geometry> result=new ArrayList<>();
@SuppressWarnings("rawtypes")
List list=tree.query(searchGeo.getEnvelopeInternal());
for(int i=0;i<list.size();i++){
Geometry lineStr=(Geometry)list.get(i);
if(lineStr.intersects(searchGeo)){
result.add(lineStr);
}
}
return result;
}
//根据两点生成矩形搜索框
public static Geometry generateSearchGeo(double left_top_x,double left_top_y,double right_bottom_x,double right_bottom_y){
Coordinate[] coors=new Coordinate[4];
coors[0]=new Coordinate(left_top_x, left_top_y);
coors[1]=new Coordinate(right_bottom_x, left_top_y);
coors[2]=new Coordinate(left_top_x, right_bottom_y);
coors[3]=new Coordinate(right_bottom_x, right_bottom_y);
LinearRing ring=new LinearRing(new CoordinateArraySequence(coors),new GeometryFactory());
return ring;
}
}
common.properties:
#两点间最大间隔(经纬度差值,目前按照江的宽度)
maxGeoRange=0.002
#maxGeoRange=0.012
#两点间最小间隔(经纬度差值,目前按照地图上一栋楼的宽度)
minGeoRange=0.0005
#minGeoRange=0.0017
#武汉市外包矩形的范围
#左上角
left-top=113.702282,31.36127
#左下角
left-bottom=113.702282,29.969079
#右上角
right-top=115.082574,31.36127
#右下角
right-bottom=115.082574,29.969079
#设定的网格的行数
rows=10
#设定的网格的列数
cols=10
#坐标保留小数点后几位
CoorAbs=10000
GPSPoint.java:
package entity;
import java.util.Date;
/**
* 点,描述点的位置,所属网格和所属线条
* @author KingWang
*
*/
public class GPSPoint extends Point{
private Date date=new Date();
private Grid grid=new Grid();
private Line line=new Line();
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public Grid getGrid() {
return grid;
}
public void setGrid(Grid grid) {
this.grid = grid;
}
public Line getLine() {
return line;
}
public void setLine(Line line) {
this.line = line;
}
}
Grid.java:
package entity;
import java.util.HashSet;
public class Grid {
private int index=0;
private int col=0;
private int row=0;
private Point lefttop=new Point();
private Point rightbottom=new Point();
private HashSet<Line> set=new HashSet<>();
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
public int getCol() {
return col;
}
public void setCol(int col) {
this.col = col;
}
public int getRow() {
return row;
}
public void setRow(int row) {
this.row = row;
}
public Point getLefttop() {
return lefttop;
}
public void setLefttop(Point lefttop) {
this.lefttop = lefttop;
}
public Point getRightbottom() {
return rightbottom;
}
public void setRightbottom(Point rightbottom) {
this.rightbottom = rightbottom;
}
public HashSet<Line> getSet() {
return set;
}
public void setSet(HashSet<Line> set) {
this.set = set;
}
public void addLine(Line line){
this.set.add(line);
}
}
Line.java:
package entity;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CopyOnWriteArrayList;
import config.GeoConfig;
import util.SpatialUtil;
/**
* 线,点串,描述整条轨迹
* @author KingWang
*
*/
public class Line {
/**
* 轨迹的id
*/
private String id="";
/**
* 按照顺序存储点轨迹
*/
private List<GPSPoint> coors=new ArrayList<>();
/**
* 经过的网格,按照轨迹顺序存储
*/
private List<Grid> grids=new ArrayList<>();
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public List<GPSPoint> getCoors() {
return coors;
}
public void setCoors(List<GPSPoint> coors) {
this.coors = coors;
}
public List<Grid> getGrids() {
return grids;
}
public void setGrids(List<Grid> grids) {
this.grids = grids;
}
public void addPoint(GPSPoint p){
this.coors.add(p);
}
public void removePoint(int index){
this.coors.remove(index);
}
public Line sort(boolean isTimeAsc){
List<GPSPoint> list=this.getCoors();
Collections.sort(list, (point1,point2)->{
if(point1.getDate().after(point2.getDate())){
if(isTimeAsc){
return 1;
}else{
return -1;
}
}else{
if(isTimeAsc){
return -1;
}else{
return 1;
}
}
});
return this;
}
/**
* 对线坐标串进行粗处理,太密的点删掉,太远的点打断成两段
* @param config
* @return
*/
public List<Line> filter(GeoConfig config){
List<Line> resultList=new ArrayList<>();
List<GPSPoint> list=new CopyOnWriteArrayList<>(this.getCoors());
Point lastPt=new Point();
int i=0;
int lastCutIndex=0;
for(GPSPoint point:list){
if(i>0&&SpatialUtil.inTolerance(lastPt,point,config)==CompareValue.GT){
List<GPSPoint> list_temp=new ArrayList<>();
list_temp.addAll(list.subList(lastCutIndex, i));
Line line_temp=new Line();
line_temp.setCoors(list_temp);
line_temp.setId(String.valueOf(System.currentTimeMillis()+new Random().nextInt(10)));
resultList.add(line_temp);
lastCutIndex=i;
}else if(i>0&&SpatialUtil.inTolerance(lastPt, point, config)==CompareValue.LT){
list.remove(i);
i--;
}
lastPt=point;
i++;
}
if(lastCutIndex==i){
Line line_temp=new Line();
line_temp.setCoors(this.getCoors());
line_temp.setId(String.valueOf(System.currentTimeMillis()+new Random().nextInt(10)));
resultList.add(line_temp);
}else{
List<GPSPoint> list_temp=new ArrayList<>();
list_temp.addAll(list.subList(lastCutIndex, i));
Line line_temp=new Line();
line_temp.setCoors(list_temp);
line_temp.setId(String.valueOf(System.currentTimeMillis()+new Random().nextInt(10)));
resultList.add(line_temp);
}
return resultList;
}
}
Point.java:
package entity;
import config.GeoConfig;
/**
* 点,描述点的位置
* @author KingWang
*
*/
public class Point {
private double x=0.0;
private double y=0.0;
public double getX() {
return x;
}
public void setX(double x) {
this.x = x;
}
public double getY() {
return y;
}
public void setY(double y) {
this.y = y;
}
}
注:
这里需要用到求解空间拓扑关系的函数。
具体参考博主另外一篇文章关于拓扑关系的测试。