当需要在GIS地图上显示的点数据量比较大时,会考虑将多个点汇聚成一个点展示;汇聚后图标上显示一个数字,以表示当前汇聚点包含了多少个原始数据对象。用户可以鼠标点击这些汇聚点查看单个原始数据的详细信息。
GIS数据汇聚展示可以让地图呈现更简洁,美观(如果所有点展开,地图缩小时显示得密密麻麻)。另外更重要的一点是可以提升地图渲染的效率。
汇聚算法与地图的放大级别(zoom size),以及当前屏幕窗口边界范围有关。当地图缩小时,数据需要汇聚,地图放大时数据需要展开。经纬度落在屏幕边界范围外的数据不需要展示,这样可以减少加载的数据量,提升性能(坏处是拖动地图时需要重新加载数据, 实测几十万点的情况下延迟两到三秒,还可以接受)。
有两种实现方式:一种是采用空间搜索算法,在地图视窗变化时根据地图放大层级,搜索半径范围的点汇聚在一起;另一种是将屏幕范围内的地图区域划分多个网格,落到同一个网格类的点汇聚在一起(网格大小根据不同的放大层级指定)。
前一种方案实现复杂,地图放大和缩小时点数据汇聚和展开的效果很平滑。后一种画网格的方式实现比较简单,但效果比较差,数据量,位置分布,离群值都会影响汇聚效果。
汇聚算法只能解决点数据的汇聚,如果地图上同时有线数据,与点相连的;处理不好就会出现放大或者缩小时点与线脱节的情况,很影响呈现效果。这种情况通过画网格的方式是解决不了的。
基于空间搜索的方案,有成熟的开源组件SuperCluster(https://github.com/mapbox/supercluster)可用,实测效果也非常好。在使用的过程中发现有性能瓶颈(不是组件本身的算法瓶颈),由于SuperCluster组件用JS开发的,在前端应用。这就要求后端一次把所有的点数据传到前端,数据量大时传输时间很长。
为解决数据传输性能的问题,需要把汇聚算法放在后台实现,参考SuperCluster的实现逻辑,用Java代码重写了一遍(JSuperCluster)。数据在后台汇聚后再传到前台,10几万个点汇聚后通常只有只百个点。传输数据量减小后,性能得到很大提升。
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;
}
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;
}
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;
}
package com.elon.jsc.model;
import lombok.Data;
import java.io.Serializable;
/**
* 聚合节点模型定义。
*
* @author elon
* @version 1.0 2019-03-15
*/
@Data
public class JClusterNode 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;
}
将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 points = null;
/**
* 节点ID列表(从0开始新分配的ID)
*/
private List ids = null;
/**
* 节点坐标列表(同一个点的X和Y存储在相邻的位置)
*/
private List coords = null;
/**
* 构造函数。根据传入的KDNode模型初始化数据。
*
* @param points KDNode对象模型
*/
public JKDBush(List 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 range(double minX, double minY, double maxX, double maxY) {
return new JKDRange(nodeSize, ids, coords).range(minX, minY, maxX, maxY);
}
public List 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 getPoints() {
return points;
}
public void setPoints(List points) {
this.points = points;
}
public List getIds() {
return ids;
}
public void setIds(List ids) {
this.ids = ids;
}
public List getCoords() {
return coords;
}
public void setCoords(List coords) {
this.coords = coords;
}
}
package com.elon.jsc.kdbush;
import java.util.List;
/**
* KD树排序
*/
public class JKDSort {
/**
* 节点数量
*/
private int nodeSize;
/**
* 节点ID列表
*/
private List ids = null;
/**
* 节点坐标列表
*/
private List coords = null;
/**
* 构造方法
*/
public JKDSort(int nodeSize, List ids, List 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 arr, int i, int j) {
int tmp = arr.get(i);
arr.set(i, arr.get(j));
arr.set(j, tmp);
}
private void swapDouble(List arr, int i, int j) {
double tmp = arr.get(i);
arr.set(i, arr.get(j));
arr.set(j, tmp);
}
}
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 ids = null;
/**
* 节点坐标列表
*/
private List coords = null;
public JKDRange(int nodeSize, List ids, List coords) {
this.nodeSize = nodeSize;
this.ids = ids;
this.coords = coords;
}
/**
* 查询矩形范围内的点,过滤不在屏幕范围内的对象。
*
* @param minX
* @param minY
* @param maxX
* @param maxY
* @return
*/
public List range(double minX, double minY, double maxX, double maxY) {
Stack stack = new Stack<>();
stack.push(0);
stack.push(ids.size() - 1);
stack.push(0);
List 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;
}
}
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 ids = null;
/**
* 节点坐标列表
*/
private List coords = null;
public JKDWithin(int nodeSize, List ids, List coords) {
this.nodeSize = nodeSize;
this.ids = ids;
this.coords = coords;
}
/**
* 搜索半径范围内的点。
*
* @param qx
* @param qy
* @param r
* @return
*/
public List within(double qx, double qy, double r) {
Stack stack = new Stack<>();
stack.push(0);
stack.push(ids.size() - 1);
stack.push(0);
List 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;
}
}
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 {
/**
* 汇聚算法参数
*/
private JClusterOption option = null;
/**
* K-D树
*/
private List trees = null;
/**
* 参数汇聚的点数据列表
*/
private List 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 points) {
this.points = points;
// 构建聚合点数据
List 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 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> getClusters(List 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 easternBBox = new ArrayList<>();
easternBBox.add(minLng);
easternBBox.add(minLat);
easternBBox.add(180.0);
easternBBox.add(maxLat);
List> easternHem = getClusters(easternBBox, zoom);
List westernBBox = new ArrayList<>();
westernBBox.add(-180.0);
westernBBox.add(minLat);
westernBBox.add(maxLng);
westernBBox.add(maxLat);
List> westernHem = getClusters(westernBBox, zoom);
easternHem.addAll(westernHem);
return easternHem;
}
JKDBush tree = trees.get(limitZoom(zoom));
List ids = tree.range(lngX(minLng), latY(maxLat), lngX(maxLng), latY(minLat));
List> clusters = new ArrayList<>();
for (int id : ids) {
JKDNode c = tree.getPoints().get(id);
if (c.getNumPoints() > 0) {
JClusterNode 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 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 getLeaves(int clusterId) {
int limit = Integer.MAX_VALUE;
int offset = 0;
List leaves = new ArrayList<>();
appendLeaves(leaves, clusterId, limit, offset, 0);
return leaves;
}
/**
* 构建聚簇对象。
* @param points
* @param zoom
* @return
*/
private List buildCluster(List points, int zoom) {
List 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 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> getChildren(int clusterId) {
int originId = clusterId >> 5;
int originZoom = clusterId % 32;
List> 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 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 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 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 result, int clusterId, int limit, int offset, int skipped) {
List> children = getChildren(clusterId);
for (JClusterNode 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 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(option);
jsc.load(testDataList);
List bbox = new ArrayList<>();
bbox.add(112.6675);
bbox.add(27.76451);
bbox.add(113.13648);
bbox.add(27.95462);
List> resultList = jsc.getClusters(bbox, 3);
System.out.println("汇聚结果:" + JSONObject.toJSONString(resultList));
System.out.println("显示聚簇点下的叶子节点:");
for (JClusterNode 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