GIS地图点汇聚及空间搜索算法Java实现样例

背景

当需要在GIS地图上显示的点数据量比较大时,会考虑将多个点汇聚成一个点展示;汇聚后图标上显示一个数字,以表示当前汇聚点包含了多少个原始数据对象。用户可以鼠标点击这些汇聚点查看单个原始数据的详细信息。

GIS数据汇聚展示可以让地图呈现更简洁,美观(如果所有点展开,地图缩小时显示得密密麻麻)。另外更重要的一点是可以提升地图渲染的效率。

方案分析

汇聚算法与地图的放大级别(zoom size),以及当前屏幕窗口边界范围有关。当地图缩小时,数据需要汇聚,地图放大时数据需要展开。经纬度落在屏幕边界范围外的数据不需要展示,这样可以减少加载的数据量,提升性能(坏处是拖动地图时需要重新加载数据, 实测几十万点的情况下延迟两到三秒,还可以接受)。

有两种实现方式:一种是采用空间搜索算法,在地图视窗变化时根据地图放大层级,搜索半径范围的点汇聚在一起;另一种是将屏幕范围内的地图区域划分多个网格,落到同一个网格类的点汇聚在一起(网格大小根据不同的放大层级指定)。

前一种方案实现复杂,地图放大和缩小时点数据汇聚和展开的效果很平滑。后一种画网格的方式实现比较简单,但效果比较差,数据量,位置分布,离群值都会影响汇聚效果。

汇聚算法只能解决点数据的汇聚,如果地图上同时有线数据,与点相连的;处理不好就会出现放大或者缩小时点与线脱节的情况,很影响呈现效果。这种情况通过画网格的方式是解决不了的。

基于空间搜索的方案,有成熟的开源组件SuperCluster(https://github.com/mapbox/supercluster)可用,实测效果也非常好。在使用的过程中发现有性能瓶颈(不是组件本身的算法瓶颈),由于SuperCluster组件用JS开发的,在前端应用。这就要求后端一次把所有的点数据传到前端,数据量大时传输时间很长。

为解决数据传输性能的问题,需要把汇聚算法放在后台实现,参考SuperCluster的实现逻辑,用Java代码重写了一遍(JSuperCluster)。数据在后台汇聚后再传到前台,10几万个点汇聚后通常只有只百个点。传输数据量减小后,性能得到很大提升。

样例代码

代码目录结构

在这里插入图片描述

定义算法基础模型

  1. 汇聚模型基础类定义-应用层模型从该类派生
package com.elon.jsc.model;

import lombok.Data;
import java.io.Serializable;

/**
 * 汇聚模型基础类定义。所有需要调用汇聚算法的模型从该类派生。
 *
 * @author elon
 * @version 1.0 2019-03-15
 */
@Data
public class AggregationModelBase implements Serializable {

    private static final long serialVersionUID = 4951392595928922035L;

    /**
     * 对象唯一ID(应用层定义,可以是数据库中对象的自增ID或者其它唯一标识)。
     */
    private int id = -1;

    /**
     * 经度
     */
    private double longitude = 0;

    /**
     * 维度
     */
    private double latitude = 0;
}
  1. 汇聚算法参数模型定义-最大放大层级根据地图支持的规格而定
package com.elon.jsc.model;

import lombok.Data;

import java.io.Serializable;

/**
 * 汇聚算法参数模型定义
 */
@Data
public class JClusterOption implements Serializable {

    private static final long serialVersionUID = -7916591464862026234L;

    /**
     * 生成聚合数据的最小地图放大层级
     */
    private int minZoom = 0;

    /**
     * 生成数据的最大地图放大层级
     */
    private int maxZoom = 16;

    /**
     * 聚合半径(单位:像素)
     */
    private int radius = 40;

    /**
     * 瓦片大小
     */
    private int extent = 512;

    /**
     * KD-Tree的叶子节点数量
     */
    private int nodeSize = 64;

    private Object reduce = null;

    private Object initial = null;

    private Object map = null;
}

  1. KD树单点对象模型定义
package com.elon.jsc.model;

import lombok.Data;

import java.io.Serializable;

/**
 * KD树单点对象模型定义。应用层的单个GIS点在计算前转换为该模型。
 *
 * @author elon
 */
@Data
public class JKDNode implements Serializable {

    private static final long serialVersionUID = -7134737233652288121L;

    /**
     * 对象索引ID(算法对数量重新编号后的id)
     */
    private int id = -1;

    /**
     * 父节点ID
     */
    private int parentId = 1;

    /**
     * 地图放大级别
     */
    private int zoom = Integer.MAX_VALUE;

    /**
     * 聚合点的数量
     */
    private int numPoints = 0;

    /**
     * 对象属性
     */
    private Object properties = null;

    /**
     * 对象原始ID,记录应用层汇聚模型的ID值
     */
    private int orignalId = -1;

    /**
     * X坐标
     */
    private double x = 0;

    /**
     * Y坐标
     */
    private double y = 0;

    private int index = -1;
}

  1. 聚合节点模型定义
package com.elon.jsc.model;

import lombok.Data;

import java.io.Serializable;

/**
 * 聚合节点模型定义。
 *
 * @author elon
 * @version 1.0 2019-03-15
 */
@Data
public class JClusterNode <T extends AggregationModelBase> implements Serializable {

    private static final long serialVersionUID = -5358622773451333438L;
    /**
     * 是否聚合对象
     */
    private boolean isCluster = false;

    /**
     * 聚合点的ID
     */
    private int clusterId = -1;

    /**
     * 聚合点数量
     */
    private int pointCount = 0;

    /**
     * 聚合点的X坐标
     */
    private double x = 0;

    /**
     * Y坐标
     */
    private double y = 0;

    /**
     * 聚合点为单点时存储应用层的对象模型。
     */
    private T data = null;
}


定义算法底层使用的K-D树

  1. K-D树模型定义

将GIS对象放到K-D Tree, 以支持后续的快速搜索。

package com.elon.jsc.kdbush;

import com.elon.jsc.model.JKDNode;

import java.util.Arrays;
import java.util.List;

/**
 * K-D树模型定义,用于GIS点汇聚时空间搜索。根据开源软件KDBush的JS代码翻译而来。
 *
 * @author elon
 * @version 1.0 2019-03-15
 */
public class JKDBush {

    /**
     * KD树节点数量
     */
    private int nodeSize = 64;

    /**
     * 节点列表
     */
    private List<JKDNode> points = null;

    /**
     * 节点ID列表(从0开始新分配的ID)
     */
    private List<Integer> ids = null;

    /**
     * 节点坐标列表(同一个点的X和Y存储在相邻的位置)
     */
    private List<Double> coords = null;

    /**
     * 构造函数。根据传入的KDNode模型初始化数据。
     *
     * @param points KDNode对象模型
     */
    public JKDBush(List<JKDNode> points){
        this.points = points;

        // 分配ID和坐标的存储空间(坐标存储X和Y, 需要两倍的空间)
        ids = Arrays.asList(new Integer[points.size()]);
        coords = Arrays.asList(new Double[points.size() * 2]);

        // 初始化数据
        for (int i = 0; i < points.size(); ++i){
            ids.set(i, i);

            // 偶数位存储X坐标, 奇数位存储Y坐标
            coords.set(2 * i, points.get(i).getX());
            coords.set(2 * i + 1, points.get(i).getY());
        }

        // 排序以支持快速搜索
        new JKDSort(nodeSize, ids, coords).sort(0, ids.size() - 1, 0);
    }

    public List<Integer> range(double minX, double minY, double maxX, double maxY) {
        return new JKDRange(nodeSize, ids, coords).range(minX, minY, maxX, maxY);
    }

    public List<Integer> within(double x, double y, double r) {
        return new JKDWithin(nodeSize, ids, coords).within(x, y, r);
    }

    public int getNodeSize() {
        return nodeSize;
    }

    public void setNodeSize(int nodeSize) {
        this.nodeSize = nodeSize;
    }

    public List<JKDNode> getPoints() {
        return points;
    }

    public void setPoints(List<JKDNode> points) {
        this.points = points;
    }

    public List<Integer> getIds() {
        return ids;
    }

    public void setIds(List<Integer> ids) {
        this.ids = ids;
    }

    public List<Double> getCoords() {
        return coords;
    }

    public void setCoords(List<Double> coords) {
        this.coords = coords;
    }
}

  1. KD树排序
package com.elon.jsc.kdbush;

import java.util.List;

/**
 * KD树排序
 */
public class JKDSort {

    /**
     * 节点数量
     */
    private int nodeSize;

    /**
     * 节点ID列表
     */
    private List<Integer> ids = null;

    /**
     * 节点坐标列表
     */
    private List<Double> coords = null;

    /**
     * 构造方法
     */
    public JKDSort(int nodeSize, List<Integer> ids, List<Double> coords) {
        this.nodeSize = nodeSize;
        this.ids = ids;
        this.coords = coords;
    }

    /**
     * 数据排序
     *
     * @param left
     * @param right
     * @param depth
     */
    public void sort(int left, int right, int depth) {
        if (right - left <= nodeSize) {
            return;
        }

        // 计算中间节点的位置
        int m = (left + right) / 2;

        // 以中间节点排序
        select(m, left, right, depth % 2);

        // 递归处理左右子树
        sort(left, m - 1, depth + 1);
        sort(m + 1, right, depth + 1);
    }

    /**
     * 排序使左子树的点小于右子树的点。
     *
     * @param k
     * @param left
     * @param right
     * @param depth
     */
    private void select(int k, int left, int right, int depth) {

        while (right > left) {
            if (right - left > 600) {
                int n = right - left + 1;
                int m = k - left + 1;
                double z = Math.log(n);
                double s = 0.5 * Math.exp(2 * z / 3);
                double sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);
                int newLeft = (int) Math.max(left, Math.floor(k - m * s / n + sd));
                int newRight = (int) Math.min(right, Math.floor(k + (n - m) * s / n + sd));
                select(k, newLeft, newRight, depth);
            }

            double t = coords.get(2 * k + depth);
            int i = left;
            int j = right;

            swapItem(left, k);
            if (coords.get(2 * right + depth) > t){
                swapItem(left, right);
            }

            while (i < j) {
                swapItem(i, j);
                i++;
                j--;

                while (coords.get(2 * i + depth) < t) {
                    i++;
                }
                while (coords.get(2 * j + depth) > t) {
                    j--;
                }
            }

            if (Double.compare(coords.get(2 * left + depth), t) == 0) {
                swapItem(left, j);
            } else {
                j++;
                swapItem(j, right);
            }

            if (j <= k) {
                left = j + 1;
            }
            if (k <= j) {
                right = j - 1;
            }

        }
    }

    private void swapItem(int i, int j){
        swapInt(ids, i, j);
        swapDouble(coords, 2 * i, 2 * j);
        swapDouble(coords, 2 * i + 1, 2 * j +1);
    }

    private void swapInt(List<Integer> arr, int i, int j) {
        int tmp = arr.get(i);
        arr.set(i, arr.get(j));
        arr.set(j, tmp);
    }

    private void swapDouble(List<Double> arr, int i, int j) {
        double tmp = arr.get(i);
        arr.set(i, arr.get(j));
        arr.set(j, tmp);
    }

}
  1. KD-Tree空间范围查询
package com.elon.jsc.kdbush;

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

/**
 * KD-Tree空间范围查询
 */
public class JKDRange {

    /**
     * KD树节点数量
     */
    private int nodeSize;

    /**
     * 节点ID列表
     */
    private List<Integer> ids = null;

    /**
     * 节点坐标列表
     */
    private List<Double> coords = null;

    public JKDRange(int nodeSize, List<Integer> ids, List<Double> coords) {
        this.nodeSize = nodeSize;
        this.ids = ids;
        this.coords = coords;
    }

    /**
     * 查询矩形范围内的点,过滤不在屏幕范围内的对象。
     *
     * @param minX
     * @param minY
     * @param maxX
     * @param maxY
     * @return
     */
    public List<Integer> range(double minX, double minY, double maxX, double maxY) {
        Stack<Integer> stack = new Stack<>();
        stack.push(0);
        stack.push(ids.size() - 1);
        stack.push(0);

        List<Integer> result = new ArrayList<>();

        // 递归搜索KD-Tree上指定范围内的元素。
        while (!stack.isEmpty()) {
            int axis = stack.pop();
            int right = stack.pop();
            int left = stack.pop();

            if (right - left <= nodeSize) {
                for (int i = left; i <= right; i++) {
                    double x = coords.get(2 * i);
                    double y = coords.get(2 * i + 1);
                    if (x >= minX && x <= maxX && y >= minY && y <= maxY) {
                        result.add(ids.get(i));
                    }
                }

                continue;
            }

            int m = (left + right) >> 1;

            // 如果中间节点在范围内,加入结果集。
            double x = coords.get(2 * m);
            double y = coords.get(2 * m + 1);
            if (x >= minX && x <= maxX && y >= minY && y <= maxY){
                result.add(ids.get(m));
            }

            int nextAxis = (axis + 1) % 2;

            if (axis == 0? minX <= x : minY <= y) {
                stack.push(left);
                stack.push(m - 1);
                stack.push(nextAxis);
            }

            if (axis == 0 ? maxX >= x : maxY >= y) {
                stack.push(m + 1);
                stack.push(right);
                stack.push(nextAxis);
            }
        }

        return result;
    }
}

  1. 按搜索半径查询KD-Tree的数据
package com.elon.jsc.kdbush;

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

/**
 * 按搜索半径查询KD-Tree的数据
 */
public class JKDWithin {
    /**
     * KD树节点数量
     */
    private int nodeSize;

    /**
     * 节点ID列表
     */
    private List<Integer> ids = null;

    /**
     * 节点坐标列表
     */
    private List<Double> coords = null;

    public JKDWithin(int nodeSize, List<Integer> ids, List<Double> coords) {
        this.nodeSize = nodeSize;
        this.ids = ids;
        this.coords = coords;
    }

    /**
     * 搜索半径范围内的点。
     *
     * @param qx
     * @param qy
     * @param r
     * @return
     */
    public List<Integer> within(double qx, double qy, double r) {
        Stack<Integer> stack = new Stack<>();
        stack.push(0);
        stack.push(ids.size() - 1);
        stack.push(0);

        List<Integer> result = new ArrayList<>();
        double r2 = r * r;

        // 在KD-Tree上搜索半径范围内的数据
        while (!stack.isEmpty()) {
            int axis = stack.pop();
            int right = stack.pop();
            int left = stack.pop();

            if (right - left <= nodeSize) {
                for (int i = left; i <= right; i++) {
                    if (sqDist(coords.get(2 * i), coords.get(2 * i + 1), qx, qy) <= r2) {
                        result.add(ids.get(i));
                    }
                }

                continue;
            }

            int m = (left + right) >> 1;

            double x = coords.get(2 * m);
            double y = coords.get(2 * m + 1);
            if (sqDist(x, y, qx, qy) <= r2) {
                result.add(ids.get(m));
            }

            int nextAxis = (axis + 1) % 2;

            if (axis == 0 ? qx - r <= x : qy - r <= y) {
                stack.push(left);
                stack.push(m - 1);
                stack.push(nextAxis);
            }

            if (axis == 0 ? qx + r >= x : qy + r >= y) {
                stack.push(m + 1);
                stack.push(right);
                stack.push(nextAxis);
            }
        }

        return result;
    }

    private double sqDist(double ax, double ay, double bx, double by) {
        double dx = ax -bx;
        double dy = ay -by;

        return dx * dx + dy * dy;
    }
}

SuperCluster汇聚主类-提供给应用层调用的接口

package com.elon.jsc.supercluster;

import com.elon.jsc.kdbush.JKDBush;
import com.elon.jsc.model.AggregationModelBase;
import com.elon.jsc.model.JClusterNode;
import com.elon.jsc.model.JClusterOption;
import com.elon.jsc.model.JKDNode;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * SuperCluster汇聚类。
 */
public class JSuperCluster<T extends AggregationModelBase> {
    /**
     * 汇聚算法参数
     */
    private JClusterOption option = null;

    /**
     * K-D树
     */
    private List<JKDBush> trees = null;

    /**
     * 参数汇聚的点数据列表
     */
    private List<T> points = null;

    public JSuperCluster(JClusterOption option) {
        this.option = option;
        this.trees = Arrays.asList(new JKDBush[option.getMaxZoom() + 2]);
    }

    /**
     * 加载数据,生成KD树。
     * @param points
     */
    public void load(List<T> points) {
        this.points = points;

        // 构建聚合点数据
        List<JKDNode> clusters = new ArrayList<>();
        for (int i = 0; i < this.points.size(); i++) {
            if (points.get(i) == null) {
                continue;
            }

            clusters.add(createPointCluster(points.get(i), i));
        }
        trees.set(option.getMaxZoom() + 1, new JKDBush(clusters));

        // 逐层构建每一级的汇聚节点
        for (int z = option.getMaxZoom(); z >= option.getMinZoom(); z--) {
            List<JKDNode> clustersList = buildCluster(clusters, z);

            trees.set(z, new JKDBush(clustersList));
            clusters = clustersList;
        }
    }

    private JKDNode createPointCluster(T p, int id) {
        JKDNode node = new JKDNode();
        node.setId(id);

        node.setX(lngX(p.getLongitude()));
        node.setY(latY(p.getLatitude()));
        node.setParentId(-1);
        node.setZoom(Integer.MAX_VALUE);
        node.setOrignalId(p.getId());
        node.setIndex(id);

        return node;
    }

    /**
     * 获取聚簇对象。
     * @param bbox
     * @param zoom
     * @return
     */
    public List<JClusterNode<T>> getClusters(List<Double> bbox, int zoom) {
        double minLng = ((bbox.get(0) + 180) % 360 + 360) % 360 - 180;
        double minLat = Math.max(-90, Math.min(90, bbox.get(1)));
        double maxLng = bbox.get(2) == 180 ? 180 : ((bbox.get(2) + 180) % 360 + 360) % 360 -180;
        double maxLat = Math.max(-90, Math.min(90, bbox.get(3)));

        if (bbox.get(2) - bbox.get(0) >= 360) {
            minLng = -180;
            maxLng = 180;
        } else if (minLng > maxLng) {
            List<Double> easternBBox = new ArrayList<>();
            easternBBox.add(minLng);
            easternBBox.add(minLat);
            easternBBox.add(180.0);
            easternBBox.add(maxLat);
            List<JClusterNode<T>> easternHem = getClusters(easternBBox, zoom);

            List<Double> westernBBox = new ArrayList<>();
            westernBBox.add(-180.0);
            westernBBox.add(minLat);
            westernBBox.add(maxLng);
            westernBBox.add(maxLat);
            List<JClusterNode<T>> westernHem = getClusters(westernBBox, zoom);

            easternHem.addAll(westernHem);

            return easternHem;
        }

        JKDBush tree = trees.get(limitZoom(zoom));
        List<Integer> ids = tree.range(lngX(minLng), latY(maxLat), lngX(maxLng), latY(minLat));
        List<JClusterNode<T>> clusters = new ArrayList<>();

        for (int id : ids) {
            JKDNode c = tree.getPoints().get(id);
            if (c.getNumPoints() > 0) {
                JClusterNode<T> cn = new JClusterNode<>();
                cn.setCluster(true);
                cn.setClusterId(c.getId());
                cn.setPointCount(c.getNumPoints());
                cn.setX(xLng(c.getX()));
                cn.setY(yLat(c.getY()));
                clusters.add(cn);
            } else {
                T vo = points.get(c.getIndex());
                JClusterNode<T> cn = new JClusterNode<>();
                cn.setClusterId(vo.getId());
                cn.setX(xLng(c.getX()));
                cn.setY(yLat(c.getY()));
                cn.setData(vo);
                clusters.add(cn);
            }
        }

        return clusters;
    }

    /**
     * 获取聚簇节点下所有叶子节点。
     *
     * @param clusterId
     * @return
     */
    public List<T> getLeaves(int clusterId) {
        int limit = Integer.MAX_VALUE;
        int offset = 0;

        List<T> leaves = new ArrayList<>();
        appendLeaves(leaves, clusterId, limit, offset, 0);

        return leaves;
    }

    /**
     * 构建聚簇对象。
     * @param points
     * @param zoom
     * @return
     */
    private List<JKDNode> buildCluster(List<JKDNode> points, int zoom) {
        List<JKDNode> clusters = new ArrayList<>();
        double r = option.getRadius() / (option.getExtent() * Math.pow(2, zoom));

        for (int i = 0; i < points.size(); i++) {
            JKDNode p = points.get(i);
            if (p.getZoom() <= zoom) {
                continue;
            }

            p.setZoom(zoom);

            // 找到所有临近的节点做汇聚
            JKDBush tree = trees.get(zoom + 1);
            List<Integer> neighborIds = tree.within(p.getX(), p.getY(), r);

            int numPoints = (p.getNumPoints() != 0) ? p.getNumPoints() : 1;
            double wx = p.getX() * numPoints;
            double wy = p.getY() * numPoints;

            // cluster id中包含的zoom和原始对象ID的信息
            int id = (i << 5) + (zoom + 1);

            for (int neighborId : neighborIds) {
                JKDNode b = tree.getPoints().get(neighborId);

                // 过滤掉已处理过的邻居节点
                if (b.getZoom() <= zoom) {
                    continue;
                }

                b.setZoom(zoom);
                int numPoints2 = (b.getNumPoints() != 0) ? b.getNumPoints() : 1;
                wx += b.getX() * numPoints2;
                wy += b.getY() * numPoints2;

                numPoints += numPoints2;
                b.setParentId(id);
            }

            if (numPoints == 1) {
                clusters.add(p);
            } else {
                p.setParentId(id);
                clusters.add(createCluster(wx / numPoints, wy / numPoints, id, numPoints, null));
            }
        }

        return clusters;
    }

    /**
     * 获取聚簇节点下的子节点。
     *
     * @param clusterId
     * @return
     */
    private List<JClusterNode<T>> getChildren(int clusterId) {
        int originId = clusterId >> 5;
        int originZoom = clusterId % 32;

        List<JClusterNode<T>> children = new ArrayList<>();

        JKDBush index = this.trees.get(originZoom);
        if (index == null) {
            return children;
        }

        JKDNode origin = index.getPoints().get(originId);
        if (origin == null) {
            return children;
        }

        double r = option.getRadius() / (option.getExtent() * Math.pow(2, originZoom - 1));
        List<Integer> ids = index.within(origin.getX(), origin.getY(), r);

        for (int id : ids) {
            JKDNode c = index.getPoints().get(id);
            if (c.getParentId() == clusterId) {
                if (c.getNumPoints() > 0) {
                    JClusterNode<T> cn = new JClusterNode<>();
                    cn.setCluster(true);
                    cn.setClusterId(c.getId());
                    cn.setPointCount(c.getNumPoints());
                    cn.setX(c.getX());
                    cn.setY(c.getY());
                    children.add(cn);
                } else {
                    T vo = points.get(c.getIndex());
                    JClusterNode<T> cn = new JClusterNode<>();
                    cn.setClusterId(vo.getId());
                    cn.setX(vo.getLongitude());
                    cn.setY(vo.getLatitude());
                    cn.setData(vo);
                    children.add(cn);
                }
            }
        }

        return children;
    }

    /**
     * 添加叶子节点。
     *
     * @param result
     * @param clusterId
     * @param limit
     * @param offset
     * @param skipped
     * @return
     */
    private int appendLeaves(List<T> result, int clusterId, int limit, int offset, int skipped) {
       List<JClusterNode<T>> children = getChildren(clusterId);

       for (JClusterNode<T> child : children) {
           if (child.isCluster()) {
               if (skipped + child.getPointCount() <= offset) {
                   // 跳过整个聚簇节点
                   skipped += child.getPointCount();
               } else {
                   skipped = appendLeaves(result, child.getClusterId(), limit, offset, skipped);
               }
           } else if (skipped < offset) {
               skipped++;
           } else {
               result.add(child.getData());
           }

           if (result.size() == limit) {
               break;
           }
       }

       return skipped;
    }

    private  int limitZoom(int z) {
        return  Math.max(option.getMinZoom(), Math.min(z, option.getMaxZoom() + 1));
    }

    /**
     * 将经度转成墨卡托坐标
     * @param lng
     * @return
     */
    private double lngX(double lng) {
        return lng / 360 + 0.5;
    }

    private double latY(double lat) {
        double sin = Math.sin(lat * Math.PI / 180);
        double y = (0.5 - 0.25 * Math.log((1 + sin) / (1 - sin)) / Math.PI);
        return y < 0 ? 0 : y > 1 ? 1 : y;
    }

    /**
     * 墨卡托坐标转经度。
     * @return
     */
    private double xLng(double x) {
        return (x - 0.5) * 360;
    }

    private double yLat(double y) {
        double y2 = (180 - y * 360) * Math.PI / 180;
        return 360 * Math.atan(Math.exp(y2)) / Math.PI - 90;
    }

    /**
     * 构建聚合节点。
     *
     * @return
     */
    private JKDNode createCluster(double x, double y, int id, int numPoints, Object properties) {
       JKDNode cp = new JKDNode();
       cp.setId(id);
       cp.setX(x);
       cp.setY(y);
       cp.setParentId(-1);
       cp.setNumPoints(numPoints);
       cp.setProperties(properties);

       return cp;
    }
}

测试代码

从汇聚基类派生应用层模型

package com.elon.jsc.model;

public class AppModelTest extends AggregationModelBase {

    private String remark = "";

    public String getRemark() {
        return remark;
    }

    public void setRemark(String remark) {
        this.remark = remark;
    }
}

构造数据测试

package com.elon.jsc;
import com.alibaba.fastjson.JSONObject;
import com.elon.jsc.model.AppModelTest;
import com.elon.jsc.model.JClusterNode;
import com.elon.jsc.model.JClusterOption;
import com.elon.jsc.supercluster.JSuperCluster;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import java.util.ArrayList;
import java.util.List;

/**
 * 应用启动类
 */
@SpringBootApplication
public class StartupSuperCluster {

    private static Logger logger = LogManager.getLogger(StartupSuperCluster.class);

    public static void main(String[] args){
        SpringApplication.run(StartupSuperCluster.class, args);
        logger.info("Start up Java Super Cluster Success!");

        // 测试点的汇聚
        List<AppModelTest> testDataList = new ArrayList<>();
        AppModelTest test1 = new AppModelTest();
        test1.setId(1);
        test1.setLongitude(112.97362);
        test1.setLatitude(27.83088);
        testDataList.add(test1);

        AppModelTest test2 = new AppModelTest();
        test2.setId(2);
        test2.setLongitude(112.98363);
        test2.setLatitude(27.84087);
        testDataList.add(test2);

        JClusterOption option = new JClusterOption();

        JSuperCluster jsc = new JSuperCluster<AppModelTest>(option);
        jsc.load(testDataList);

        List<Double> bbox = new ArrayList<>();
        bbox.add(112.6675);
        bbox.add(27.76451);
        bbox.add(113.13648);
        bbox.add(27.95462);
        List<JClusterNode<AppModelTest>> resultList = jsc.getClusters(bbox, 3);

        System.out.println("汇聚结果:" + JSONObject.toJSONString(resultList));

        System.out.println("显示聚簇点下的叶子节点:");
        for (JClusterNode<AppModelTest> c : resultList) {
            if (c.isCluster()) {
                System.out.println("汇聚节点ID:" + c.getClusterId());
                System.out.println("叶子节点:" + JSONObject.toJSONString(jsc.getLeaves(c.getClusterId())));
            }
        }
    }
}

getClusters方法的第一个参数bbox是当前屏幕的边界(左上角、右下角坐标);第二个参数取当前地图的放大级别。

运行结果

设置地图放大层级是12时,两个点展开显示:
在这里插入图片描述
设置地图放大级别是3时,两个点合并成一个点显示:

在这里插入图片描述
GitHub源码路径:https://github.com/ylforever/elon-javasupercluster

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值