前言
热力图(Heatmap)是一种数据可视化技术,用于表示数据中不同区域的相对密集程度或者权重分布。它一般通过使用不同颜色来展示数据的热度,从而提供关于数据分布和趋势的直观理解。
热力图常用于以下几个方面:
- 数据密度可视化:热力图可以显示数据集中的密集区域和稀疏区域,帮助人们更直观地了解数据的分布情况。例如,在地图上展示城市中不同区域的人口密度或犯罪率。
- 热度趋势分析:热力图可以显示数据在时间或空间上的变化趋势。通过比较不同时间段或地点的热力图,可以发现数据的演变和变化模式。例如,用于分析股票市场中不同股票的交易活跃度随时间的变化。
- 点击热度分析:在网页设计和用户体验中,热力图可以用于展示用户在页面上点击、触摸或浏览的热度分布。这有助于优化页面布局和内容,提升用户体验和用户行为分析。
- 事件和行为分析:热力图可以用于分析人群的行为模式和交互热度。例如,在市场调研中,可以通过观察消费者在商店中的走动和停留热度来了解他们的兴趣和偏好。
geoServer热力图
使用
geoServer提供了热力图的WPS服务,在geoServer的WPS请求构建器中,选择“gs:Heatmap”进程,输入参数即可执行。
参数含义
相关参数含义:
- Input features:输入的要素集合。
- radiusPixels:用于定义热力图中每个数据点的影响范围的半径,单位为像素。较大的radiusPixel会导致更广泛的像素受到影响,热力图会显示更平滑的过渡,但也可能丧失一些细节信息。
- weightAttr:权重字段,如果没有,则会根据数据的位置生成热力图。
- pixelsPerCell:定义热力图的分辨率,每一个单元格长宽多少像素,较小的pixelsPerCell会导致更多的细节信息被保留,但也会增加热图的复杂性和计算量。
- outputBBOX:输出的热力图的地理范围,如果BBOX的坐标系与数据源坐标系不一致,则会进行坐标转换。
- outputWidth:输出的图像宽度。
- outputHeight: 输出的图像高度。
代码实现
目前有一个需求是前端传入geoJSON数据,后端根据这个数据生成geotiff影像,返回给前端。经过调查知道geoServer的HeatMap的WPS服务实际是由org.geotools.process.vector.HeatMapProcess执行的。
所以实现这个接口主要有三步:
1、解析geoJSON数据构造请求参数。
2、执行HeatMapProcess,生成栅格影像。
3、将影像以tiff格式返回给前端。
从请求流中拿到geoJSON数据
前端会将geoJSON放到request body里,只需要读取HttpServletRequest的body里的数据即可
public static String getPostData(HttpServletRequest request) {
StringBuilder data = new StringBuilder();
String line;
BufferedReader reader;
try {
reader = request.getReader();
while (null != (line = reader.readLine())) {
data.append(line);
}
} catch (IOException e) {
return null;
}
return data.toString();
}
解析geoJSON为SimpleFeatureCollection
FeatureJSON featureJSON = new FeatureJSON();
JSONParser jsonParser = new JSONParser();
JSONObject json = (JSONObject)
//这里的inputHeatMapFeatures就是解析request body获取到的字符串
jsonParser.parse(inputHeatMapFeatures);
SimpleFeatureCollection featureCollection = null;
featureCollection = (SimpleFeatureCollection) featureJSON.readFeatureCollection(json.toJSONString());
SimpleFeatureType featureType = featureCollection.getSchema();
// 获取SRID
String srs = CRS.lookupIdentifier(featureType.getCoordinateReferenceSystem(),true);
//FeatureJSON解析的坐标顺序可能会出现颠倒,如EPSG:4326会经纬度倒过来,导致生成的图像错误,所以需要再进行一次坐标转换。
featureCollection = new ForceCoordinateSystemFeatureResults(featureCollection, CRS.decode(srs, true));
这里有一个坑,就是FeatureJSON解析的坐标顺序是经度在前,纬度在后,而geotools读取顺序是纬度在前,经度在后,这会导致生成的图像经纬度颠倒,所以需要再做一次坐标变换。
构造请求范围
我这里的请求范围就是geoJSON的范围,并且全部转为WGS 84坐标系。
/**
* @Description 获取SimpleFeatureCollection的边界范围
* @param featureCollection:要素集合
* @return org.geotools.geometry.jts.ReferencedEnvelope
**/
public static ReferencedEnvelope getReferencedEnvelopeOfFeatureCollection(SimpleFeatureCollection featureCollection) throws FactoryException, TransformException {
// 获取FeatureCollection的坐标参考系
CoordinateReferenceSystem crs = featureCollection.getSchema().getCoordinateReferenceSystem();
// 初始化一个初始范围
Envelope envelope = new Envelope();
// 遍历FeatureCollection中的每个Feature
try (SimpleFeatureIterator it = featureCollection.features()) {
while (it.hasNext()) {
SimpleFeature feature = it.next();
Geometry geometry = (Geometry) feature.getDefaultGeometry();
// 合并每个Feature的几何图形的边界范围
envelope.expandToInclude(geometry.getEnvelopeInternal());
}
}
//输出范围的坐标系转为WGS 84
CoordinateReferenceSystem targetCRS;
Envelope targetEnvelope;
if(("EPSG:WGS 84").equals(crs.getName().toString())){
return new ReferencedEnvelope(envelope, crs);
}else{
targetCRS = CRS.decode("EPSG:4326");
targetEnvelope = convertEnvelope(envelope, crs, targetCRS);
}
// 将边界范围转换为被引用的范围
return new ReferencedEnvelope(targetEnvelope, targetCRS);
}
/**
* @Description 将Envelope转换到目标坐标系
* @param sourceEnvelope: 待转换的Envelope
* @param sourceCRS: 源坐标系
* @param targetCRS:目标坐标系
* @return org.locationtech.jts.geom.Envelope
**/
public static Envelope convertEnvelope(Envelope sourceEnvelope, CoordinateReferenceSystem sourceCRS, CoordinateReferenceSystem targetCRS) throws FactoryException, TransformException {
Envelope targetEnvelope = null;
return JTS.transform(sourceEnvelope, targetEnvelope, CRS.findMathTransform(sourceCRS, targetCRS), 0);
}
构造HeatMapProcess,执行execute方法
HeatmapProcess heatmapProcess = new HeatmapProcess();
GridCoverage2D coverage = heatmapProcess.execute(featureCollection, radiusPixels, weightAttr, pixelPerCell, referencedEnvelope, outputWidth, outputHeight, null);
将结果输出为geotiff文件
/**
* @param radiusPixels Radius of the density kernel in pixels 核密度半径(单位:像素)
* @param pixelPerCell Resolution at which to compute the heatmap (in pixels) 热力图分辨率,每个像元多少像素
* @param outputHeight 输出的tiff图像的高度
* @param outputWidth 输出的tiff图像的宽度
* @param servletRequest body里传geoJSON参数
* @param weightAttr 权重字段
* @param response
* @throws IOException
* @Description 传入geojson要素,生成热力图,返回tiff图片
*/
@PostMapping(value = "/heatMap")
public void heatMap(@RequestParam(defaultValue = "5", required = false) int radiusPixels,
@RequestParam(defaultValue = "1", required = false) int pixelPerCell,
@RequestParam(defaultValue = "100", required = false) int outputHeight,
@RequestParam(defaultValue = "100", required = false) int outputWidth,
@RequestParam(defaultValue = "value", required = false) String weightAttr,
HttpServletRequest servletRequest,
HttpServletResponse response) throws Exception {
String inputHeatMapFeatures = HttpUtil.getPostData(servletRequest);
GridCoverage2D coverage2D = HeatMap.localHeatMapExecute(radiusPixels, pixelPerCell, outputHeight, outputWidth, inputHeatMapFeatures, weightAttr);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
GeoTiffWriter writer = new GeoTiffWriter(byteArrayOutputStream);
writer.write(coverage2D, null);
writer.dispose();
//输出geotiff
response.setContentType("image/tiff");
ServletOutputStream outputStream = response.getOutputStream();
byteArrayOutputStream.writeTo(outputStream);
outputStream.flush();
outputStream.close();
byteArrayOutputStream.close();
}
该接口最终生成tiff图片,HeatMapProcess是geotools里的一个类,来源gt-process包:
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-process-raster</artifactId>
<version>${gt.version}</version>
</dependency>