Java项目实战记录:雷达数据渲染
业务背景
我之前已经成功使用Java语言解析了C++处理的雷达数据文件,并提取了其中的经纬度点数据。这些文件中的点数据量相当庞大,每个雷达产品文件通常包含超过20万个数据点,有时甚至更多。
目前面临着如何在前端展示这20万个点数据的挑战。有两种可行的解决方案:一种是直接将所有点数据传输到前端进行渲染;另一种则是在后端将点数据渲染成PNG图片,然后再将图片传输到前端展示。经过考虑,决定采用第二种方案,即在后端进行数据的渲染处理。这样不仅可以有效减轻前端的负担,提高数据处理和展示的效率,而且还可以通过优化渲染过程来提高整体的展示质量。
代码逻辑
这里,选用了GeoTools这个强大的开源Java库来执行雷达数据的渲染任务。GeoTools是专为地理空间数据处理设计的工具集,它提供了丰富的API来支持地理数据的读取、查询、分析和展示。利用GeoTools,能够高效地将雷达扫描得到的点数据转换为图像,并以PNG格式保存。
数据结构
雷达数据结构如下所示:
import lombok.Data;
/**
* 雷达点数据信息
*/
@Data
public class LonLatData {
private float lon; // 经度
private float lat; // 纬度
private double val; // 回拨强度 (等于value/100)
}
颜色渲染
根据回拨强度需要渲染不同颜色,定义一个Style样式,具体分类如下所示:
/* * 根据数值创建基于值的点样式。
*
* @return 样式对象,根据不同的值范围应用不同的颜色。
*/
public static Style createValueBasedPointStyle() {
// 创建一个黑色描边,宽度为1。
Stroke stroke = styleFactory.createStroke(filterFactory.literal(Color.BLACK), filterFactory.literal(1));
FeatureTypeStyle fts = styleFactory.createFeatureTypeStyle();
// 定义值的范围及其对应的颜色(十六进制表示)。
Object[][] valueRanges = {
{-5, Double.NEGATIVE_INFINITY, "#00acb8"},
{0, -5, "#c1c0ac"},
{10, 0, "#7a71fe"},
{15, 10, "#1e26e3"},
{20, 15, "#a6fcbb"},
{25, 20, "#00ea15"},
{30, 25, "#10932d"},
{35, 30, "#fcf477"},
{40, 35, "#c9c815"},
{45, 40, "#8c8c12"},
{50, 45, "#feadc0"},
{55, 50, "#ff6370"},
{60, 55, "#ee0244"},
{65, 60, "#d48eff"},
{Double.POSITIVE_INFINITY, 65, "#ab23ff"}
};
// 遍历每个值范围,为每个范围创建一个规则并添加到FeatureTypeStyle。
for (Object[] range : valueRanges) {
fts.rules().add(createRuleForRange(Double.parseDouble(range[1].toString()), Double.parseDouble(range[0].toString()), range[2].toString()));
}
// 创建样式并添加FeatureTypeStyle。
Style style = styleFactory.createStyle();
style.featureTypeStyles().add(fts);
return style;
}
MapContent加载数据并输出截图
首先创建了一个MapContent
对象,用它作为整个地图的内容载体。接着,创建了一个包含所有雷达数据点的图层,并为这些点指定了样式,这样做旨在通过视觉上的区分,提高数据的可读性。然后,将这个图层添加到MapContent
中,最后使用GeoTools的功能,将这些内容渲染成一张清晰展示雷达数据的PNG图片。
/**
* 根据坐标和值创建地图图像。
*
* @param coordinates 坐标对象列表,每个对象包含坐标点和与之相关联的值。
* @param outputPath 图像输出路径,指定生成的地图图像保存的位置。
* @throws Exception 抛出异常,处理文件操作或渲染过程中可能出现的错误。
*/
public static void createMapImageVal(ArrayList<CoordinatePojo> coordinates, String outputPath) throws Exception {
// 步骤1: 定义SimpleFeatureType
SimpleFeatureTypeBuilder typeBuilder = new SimpleFeatureTypeBuilder();
typeBuilder.setName("Point");
typeBuilder.setCRS(DefaultGeographicCRS.WGS84); // 坐标参考系统
typeBuilder.add("location", Point.class); // 添加位置字段
typeBuilder.add("value", Double.class); // 添加值字段
SimpleFeatureType TYPE = typeBuilder.buildFeatureType(); // 构建特征类型
// 步骤2: 创建SimpleFeatureCollection
DefaultFeatureCollection featureCollection = new DefaultFeatureCollection("internal", TYPE);
GeometryFactory geometryFactory = JTSFactoryFinder.getGeometryFactory();
SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(TYPE);
// 为每个坐标点创建特征并添加到集合中
for (CoordinatePojo data : coordinates) {
Point point = geometryFactory.createPoint(data.getCoordinate()); // 根据坐标创建点
// 根据点的值创建SimpleFeature
SimpleFeature feature = SimpleFeatureBuilder.build(TYPE, new Object[]{point, data.getValue()}, null);
featureCollection.add(feature); // 将特征添加到集合
}
// 创建基于值的点样式
Style style = createValueBasedPointStyle(); // 假设这是一个自定义方法创建基于值的样式
// 步骤3: 定义样式
// 这里我们使用了自定义的创建样式方法,而不是SLD.createPointStyle
// 步骤4: 渲染图像
MapContent mapContent = new MapContent();
mapContent.setTitle("Sample Map"); // 设置地图标题
Layer layer = new FeatureLayer(featureCollection, style); // 创建图层
mapContent.addLayer(layer); // 将图层添加到地图内容
GTRenderer renderer = new StreamingRenderer();
renderer.setMapContent(mapContent); // 设置地图内容
Rectangle imageBounds = null;
ReferencedEnvelope mapBounds = null;
try {
mapBounds = mapContent.getMaxBounds(); // 获取最大边界
double heightToWidth = mapBounds.getSpan(1) / mapBounds.getSpan(0); // 计算高宽比
imageBounds = new Rectangle(0, 0, 800, (int) Math.round(800 * heightToWidth)); // 设置图像边界
} catch (Exception e) {
// 处理可能的异常
}
BufferedImage image = new BufferedImage(imageBounds.width, imageBounds.height, BufferedImage.TYPE_INT_ARGB);
Graphics2D gr = image.createGraphics();
gr.setPaint(Color.WHITE);
gr.fill(imageBounds); // 填充背景色
renderer.paint(gr, imageBounds, mapBounds); // 渲染图像
File file = new File(outputPath); // 创建文件
ImageIO.write(image, "png", file); // 写入图像文件
gr.dispose(); // 释放资源
mapContent.dispose(); // 释放地图内容
}
完整代码
GenerateMapImage地图渲染工具
import com.zykj.radar_server.datahandle.RadarDataParser;
import com.zykj.radar_server.datahandle.pojo.LonLatData;
import com.zykj.radar_server.entity.pojo.CoordinatePojo;
import com.zykj.radar_server.entity.pojo.RadarDataPojo;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.feature.DefaultFeatureCollection;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.geometry.jts.JTSFactoryFinder;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.map.FeatureLayer;
import org.geotools.map.Layer;
import org.geotools.map.MapContent;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.renderer.GTRenderer;
import org.geotools.renderer.lite.StreamingRenderer;
import org.geotools.styling.SLD;
import org.geotools.styling.Stroke;
import org.geotools.styling.Style;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Point;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import org.geotools.styling.*;
import org.opengis.filter.FilterFactory2;
public class GenerateMapImage {
/**
* 根据坐标和值创建地图图像。
*
* @param coordinates 坐标对象列表,每个对象包含坐标点和与之相关联的值。
* @param outputPath 图像输出路径,指定生成的地图图像保存的位置。
* @throws Exception 抛出异常,处理文件操作或渲染过程中可能出现的错误。
*/
public static void createMapImageVal(ArrayList<CoordinatePojo> coordinates, String outputPath) throws Exception {
// 步骤1: 定义SimpleFeatureType
SimpleFeatureTypeBuilder typeBuilder = new SimpleFeatureTypeBuilder();
typeBuilder.setName("Point");
typeBuilder.setCRS(DefaultGeographicCRS.WGS84); // 坐标参考系统
typeBuilder.add("location", Point.class); // 添加位置字段
typeBuilder.add("value", Double.class); // 添加值字段
SimpleFeatureType TYPE = typeBuilder.buildFeatureType(); // 构建特征类型
// 步骤2: 创建SimpleFeatureCollection
DefaultFeatureCollection featureCollection = new DefaultFeatureCollection("internal", TYPE);
GeometryFactory geometryFactory = JTSFactoryFinder.getGeometryFactory();
SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(TYPE);
// 为每个坐标点创建特征并添加到集合中
for (CoordinatePojo data : coordinates) {
Point point = geometryFactory.createPoint(data.getCoordinate()); // 根据坐标创建点
// 根据点的值创建SimpleFeature
SimpleFeature feature = SimpleFeatureBuilder.build(TYPE, new Object[]{point, data.getValue()}, null);
featureCollection.add(feature); // 将特征添加到集合
}
// 创建基于值的点样式
Style style = createValueBasedPointStyle(); // 假设这是一个自定义方法创建基于值的样式
// 步骤3: 定义样式
// 这里我们使用了自定义的创建样式方法,而不是SLD.createPointStyle
// 步骤4: 渲染图像
MapContent mapContent = new MapContent();
mapContent.setTitle("Sample Map"); // 设置地图标题
Layer layer = new FeatureLayer(featureCollection, style); // 创建图层
mapContent.addLayer(layer); // 将图层添加到地图内容
GTRenderer renderer = new StreamingRenderer();
renderer.setMapContent(mapContent); // 设置地图内容
Rectangle imageBounds = null;
ReferencedEnvelope mapBounds = null;
try {
mapBounds = mapContent.getMaxBounds(); // 获取最大边界
double heightToWidth = mapBounds.getSpan(1) / mapBounds.getSpan(0); // 计算高宽比
imageBounds = new Rectangle(0, 0, 800, (int) Math.round(800 * heightToWidth)); // 设置图像边界
} catch (Exception e) {
// 处理可能的异常
}
BufferedImage image = new BufferedImage(imageBounds.width, imageBounds.height, BufferedImage.TYPE_INT_ARGB);
Graphics2D gr = image.createGraphics();
gr.setPaint(Color.WHITE);
gr.fill(imageBounds); // 填充背景色
renderer.paint(gr, imageBounds, mapBounds); // 渲染图像
File file = new File(outputPath); // 创建文件
ImageIO.write(image, "png", file); // 写入图像文件
gr.dispose(); // 释放资源
mapContent.dispose(); // 释放地图内容
}
// StyleFactory 用于创建地图样式元素,例如图层的边框、填充等。
private static final StyleFactory styleFactory = CommonFactoryFinder.getStyleFactory();
// FilterFactory2 用于创建过滤条件,例如,在样式规则中选择符合特定条件的要素。
private static final FilterFactory2 filterFactory = CommonFactoryFinder.getFilterFactory2();
/**
* 根据数值创建基于值的点样式。
*
* @return 样式对象,根据不同的值范围应用不同的颜色。
*/
public static Style createValueBasedPointStyle() {
// 创建一个黑色描边,宽度为1。
Stroke stroke = styleFactory.createStroke(filterFactory.literal(Color.BLACK), filterFactory.literal(1));
FeatureTypeStyle fts = styleFactory.createFeatureTypeStyle();
// 定义值的范围及其对应的颜色(十六进制表示)。
Object[][] valueRanges = {
{-5, Double.NEGATIVE_INFINITY, "#00acb8"},
{0, -5, "#c1c0ac"},
{10, 0, "#7a71fe"},
{15, 10, "#1e26e3"},
{20, 15, "#a6fcbb"},
{25, 20, "#00ea15"},
{30, 25, "#10932d"},
{35, 30, "#fcf477"},
{40, 35, "#c9c815"},
{45, 40, "#8c8c12"},
{50, 45, "#feadc0"},
{55, 50, "#ff6370"},
{60, 55, "#ee0244"},
{65, 60, "#d48eff"},
{Double.POSITIVE_INFINITY, 65, "#ab23ff"}
};
// 遍历每个值范围,为每个范围创建一个规则并添加到FeatureTypeStyle。
for (Object[] range : valueRanges) {
fts.rules().add(createRuleForRange(Double.parseDouble(range[1].toString()), Double.parseDouble(range[0].toString()), range[2].toString()));
}
// 创建样式并添加FeatureTypeStyle。
Style style = styleFactory.createStyle();
style.featureTypeStyles().add(fts);
return style;
}
/**
* 根据给定的值范围和颜色创建一个规则。
*
* @param min 范围的最小值。
* @param max 范围的最大值。
* @param hexColor 颜色的十六进制表示。
* @return 样式规则,包含一个基于值范围的符号化表示。
*/
public static Rule createRuleForRange(double min, Object max, String hexColor) {
// 根据十六进制颜色创建填充,并设置不透明度为1.0。
Fill fill = styleFactory.createFill(filterFactory.literal(hexToColor(hexColor)), filterFactory.literal(1.0));
// 获取默认的圆形标记,并设置填充和描边。
Mark mark = styleFactory.getCircleMark();
mark.setFill(fill);
mark.setStroke(null); // 不使用描边。
// 创建图形对象,清除默认的图形符号,添加自定义的标记。
Graphic graphic = styleFactory.createDefaultGraphic();
graphic.graphicalSymbols().clear();
graphic.graphicalSymbols().add(mark);
graphic.setSize(filterFactory.literal(4)); // 设置图形大小。
// 创建点符号器,应用上面的图形。
PointSymbolizer symbolizer = styleFactory.createPointSymbolizer(graphic, null);
// 创建规则,设置过滤条件为值在指定范围内。
Rule rule = styleFactory.createRule();
rule.symbolizers().add(symbolizer);
rule.setFilter(
filterFactory.between(
filterFactory.property("value"),
filterFactory.literal(min),
filterFactory.literal(max)
)
);
return rule;
}
/**
* 将十六进制颜色字符串转换为Color对象。
*
* @param hex 十六进制颜色字符串(例如,"#ff0000"表示红色)。
* @return 转换后的Color对象。
*/
public static Color hexToColor(String hex) {
// 将十六进制字符串转换为整数,并创建颜色对象。
// 这里没有添加前缀"FF",因为Color类的构造函数默认处理不透明颜色。
return new Color(
Integer.parseInt(hex.substring(1), 16)
);
}
}
测试代码
测试数据自行准备,如何解析雷达数据请看我上一篇博客。
package com.zykj.radar_server.data;
import com.zykj.radar_server.datahandle.GenerateMapImage;
import com.zykj.radar_server.datahandle.RadarDataParser;
import com.zykj.radar_server.datahandle.pojo.LonLatData;
import com.zykj.radar_server.entity.pojo.CoordinatePojo;
import com.zykj.radar_server.entity.pojo.RadarDataPojo;
import org.junit.jupiter.api.Test;
import org.locationtech.jts.geom.Coordinate;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.ArrayList;
import java.util.List;
@SpringBootTest
public class GenerateMapImageTest {
@Test
public void test() throws Exception {
String filePath = "D:\\雷达测试数据\\数据\\20190305_110801_ZHBJ_Z_VOL_2.50.dat";
RadarDataPojo radarDataPojo = RadarDataParser.handlePolarProductForPojo(filePath);
ArrayList<LonLatData> lonLatDataList = radarDataPojo.getLonLatDataList();
System.out.println(lonLatDataList.size());
ArrayList<CoordinatePojo> coordinates = new ArrayList<>();
for (int i = 0; i < lonLatDataList.size(); i++) {
CoordinatePojo coordinatePojo = new CoordinatePojo();
coordinatePojo.setCoordinate(new Coordinate(lonLatDataList.get(i).getLon(), lonLatDataList.get(i).getLat()));
double val = lonLatDataList.get(i).getVal();
coordinatePojo.setValue(val);
coordinates.add(coordinatePojo);
}
// 指定输出图片路径
String outputPath = "D:\\temp\\layers\\jiangsu_map_val4.png";
GenerateMapImage.createMapImageVal(coordinates, outputPath);
System.out.println("地图图片已生成: " + outputPath);
}
}
渲染效果
雷达数据渲染效果如下所示,颜色越深回波强度越大:
渲染点的大小可自行调节。