引言
在气象和环境科学领域,NC(NetCDF)文件是一种广泛使用的数据格式,通常用于存储多维科学数据。在这篇博客中,我们将介绍如何使用Java来解析NC文件,并将提取的数据转换为JSON格式。本文将逐步解析代码,帮助你理解每一个关键步骤。
效果图
准备工作
依赖库
首先,我们需要导入以下依赖库:
- Hutool:一个功能强大的Java工具类库,用于文件读写和JSON处理。
- NetCDF for Java:用于处理NetCDF文件的核心库。
- Lombok:消除Get、Set、全参/无参构造等等冗余代码。
确保在你的pom.xml
中引入以下依赖:
<!--https://artifacts.unidata.ucar.edu/service/rest/repository/browse/unidata-all/edu/ucar/netcdfAll/5.5.2/-->
<!-- 导入不进来的 看文章结尾-->
<dependency>
<groupId>edu.ucar</groupId>
<artifactId>netcdfAll</artifactId>
<version>5.5.2</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.27</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
代码实现
解析NC文件的主流程
定义实体类
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @ClassName: HeatMap
* @Description: 热力图
* @Author: liu
* @Date: 2024-03-13
* @Version: 1.0
**/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class HeatMap {
private double longitude;
private double latitude;
private double value;
private String dataType;
private String areaCode;
public HeatMap(double longitude, double latitude, double value) {
this.longitude = roundToTwoDecimals(longitude);
this.latitude = roundToTwoDecimals(latitude);
this.value = roundToTwoDecimals(value);
}
private double roundToTwoDecimals(double value) {
//性能最好,适用于对精度要求不高的场景
return Math.round(value * 100.0) / 100.0;
}
/* private double roundToTwoDecimals(double value) {
//性能较差,但精度高,适用于金融计算等高精度要求的场景。
return new BigDecimal(value)
.setScale(2, RoundingMode.HALF_UP)
.doubleValue();
}*/
}
j接着,定义了一个NcFileParse类,其中包含了用于解析NC文件的核心方法ParseNcFile。
import cn.hutool.core.io.FileUtil;
import cn.hutool.json.JSONUtil;
import com.google.common.collect.ImmutableList;
import com.lps.domain.NC.HeatMap;
import lombok.extern.slf4j.Slf4j;
import ucar.ma2.DataType;
import ucar.ma2.InvalidRangeException;
import ucar.nc2.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
/**
* @ClassName: NcFileParse
* @Description: Nc文件解析
* @Author: liu
* @Date: 2024-08-08
* @Version: 1.0
**/
@Slf4j
public class NcFileParse {
private static final String[] LON_LAT_STR = new String[]{"lon", "lat", "longitude", "latitude"};
//缩放因子
private static final String SCALE_FACTOR = "scale_factor";
private static final String LON = "lon";
private static final String LAT = "lat";
public static void main(String[] args) {
String filename = "D:\\data\\气象NC文件\\温风辐照降雨\\TMP-H24-H1\\2024-01-19\\xxxxxxxxxxxxx.NC";
List<HeatMap> ParseNcFile = ParseNcFile(filename, "温度");
//结果写入json文件
String jsonPrettyStr = JSONUtil.toJsonPrettyStr(ParseNcFile);
//写到temp.json文件中
FileUtil.writeUtf8String(jsonPrettyStr, "temp.json");
log.info("解析完成");
}
/**
* 解析nc文件
* @param filePath
* @return
*/
public static List<HeatMap> ParseNcFile(String filePath) {
List<HeatMap> heatMapList = new ArrayList<>();
try (NetcdfFile open = NetcdfFiles.open(filePath)) {
// 读取nc中的第一个平面数据
Variable data = open.findVariable("data");
ImmutableList<Dimension> dimensions = Objects.requireNonNull(data).getDimensions();
// 获取变量的平面数据形状
int[] shape = new int[dimensions.size()];
for (int i = 0; i < dimensions.size(); i++) {
Dimension dimension = dimensions.get(i);
String name = dimension.getName();
if (Arrays.asList(LON_LAT_STR).contains(name)) {
shape[i] = dimension.getLength();
} else {
shape[i] = 1;
}
}
double[] dataD;
// 根据平面形状读取数据
if (DataType.FLOAT.equals(data.getDataType()) || DataType.DOUBLE.equals(data.getDataType())) {
dataD = (double[]) data.read(new int[dimensions.size()], shape).get1DJavaArray(DataType.DOUBLE);
} else {
int[] dataArr = (int[]) data.read(new int[dimensions.size()], shape).get1DJavaArray(DataType.INT);
float v = 1f;
Attribute scaleFactor = data.findAttribute(SCALE_FACTOR);
if (scaleFactor != null) {
v = Objects.requireNonNull(scaleFactor.getNumericValue()).floatValue();
}
dataD = new double[dataArr.length];
for (int i = 0; i < dataArr.length; i++) {
dataD[i] = dataArr[i] * v;
}
}
// 获取经纬度信息
Variable lon = open.findVariable(LON);
Variable lat = open.findVariable(LAT);
assert lon != null;
double[] lonArr = (double[]) lon.read().copyTo1DJavaArray();
// 计算经度的每个数据点的间隔
double[] lonIntervals = calculateIntervals(lonArr);
double longitudeInterval = lonIntervals[1];
assert lat != null;
double[] latArr = (double[]) lat.read().copyTo1DJavaArray();
double[] latIntervals = calculateIntervals(latArr);
double latitudeInterval = latIntervals[1];
// 计算经度的最小和最大值
double minLon = lonArr[0];
double maxLon = lonArr[lonArr.length - 1];
// 计算纬度的最小和最大值
double minLat = latArr[0];
double maxLat = latArr[latArr.length - 1];
// 遍历所有经纬度点{}
for (double lonValue : lonArr) {
for (double latValue : latArr) {
// 这里使用线性搜索替代binarySearch,因为binarySearch需要精确匹配
int lonIndex = findIndexOf(lonArr, lonValue);
int latIndex = findIndexOf(latArr, latValue);
if (lonIndex != -1 && latIndex != -1) {
int dataIndex = lonIndex + latIndex * lonArr.length;
double targetValue = dataD[dataIndex];
//log.info(" 经度={}, 纬度={} , 目标值为:{}", String.format("%.2f", lonValue), String.format("%.2f", latValue), String.format("%.2f", targetValue));
HeatMap heatMap = new HeatMap(lonValue, latValue, targetValue);
heatMapList.add(heatMap);
} else {
log.warn("无效的lon或lat值");
}
}
}
log.info("经度的最小值:{}", String.format("%.2f", minLon));
log.info("经度的最大值:{}", String.format("%.2f", maxLon));
log.info("纬度的最小值:{}", String.format("%.2f", minLat));
log.info("纬度的最大值:{}", String.format("%.2f", maxLat));
log.info("经度的间隔:{}", Double.parseDouble(String.format("%.10f", longitudeInterval)));
log.info("纬度的间隔:{}", Double.parseDouble(String.format("%.10f", latitudeInterval)));
} catch (IOException | InvalidRangeException e) {
throw new RuntimeException(e);
}
return heatMapList;
}
public static List<HeatMap> ParseNcFile(String localFilePath, String type) {
List<HeatMap> ParseNcFile = ParseNcFile(localFilePath);
ParseNcFile.forEach(heatMap -> {
heatMap.setDataType(type);
});
return ParseNcFile;
}
// 线性搜索以便处理不精确匹配的情况
private static int findIndexOf(double[] arr, double value) {
int index = Arrays.binarySearch(arr, value);
// 处理binarySearch返回的负值,表示未找到,转换为插入点索引
return (index >= 0) ? index : -index - 1;
}
/**
* 计算间隔
*
* @param values
* @return
*/
private static double[] calculateIntervals(double[] values) {
double[] intervals = new double[values.length - 1];
for (int i = 0; i < intervals.length; i++) {
intervals[i] = values[i + 1] - values[i];
}
return intervals;
}
}
核心解析逻辑
ParseNcFile
方法是解析NC文件的核心逻辑,它包括以下几个步骤:
-
读取数据:使用
NetcdfFile
类打开NC文件,并查找所需的数据变量。try (NetcdfFile open = NetcdfFiles.open(localFilePath)) { Variable data = open.findVariable("data"); } catch (IOException | InvalidRangeException e) { throw new RuntimeException(e); }
-
处理缩放因子:某些NC文件会使用
scale_factor
来缩放存储的数据,我们在读取数据时需要应用这个缩放因子。Attribute scaleFactor = data.findAttribute(SCALE_FACTOR); float v = 1f; if (scaleFactor != null) { v = Objects.requireNonNull(scaleFactor.getNumericValue()).floatValue(); }
-
提取经纬度数据:解析经度和纬度数据,并计算其最小值、最大值和间隔。
Variable lon = open.findVariable(LON); Variable lat = open.findVariable(LAT); double[] lonArr = (double[]) lon.read().copyTo1DJavaArray(); double[] latArr = (double[]) lat.read().copyTo1DJavaArray();
-
数据处理:根据经纬度坐标遍历所有数据点,提取数据并存储为
HeatMap
对象。for (double lonValue : lonArr) { for (double latValue : latArr) { int lonIndex = findIndexOf(lonArr, lonValue); int latIndex = findIndexOf(latArr, latValue); if (lonIndex != -1 && latIndex != -1) { int dataIndex = lonIndex + latIndex * lonArr.length; double targetValue = dataD[dataIndex]; heatMapList.add(new HeatMap(lonValue, latValue, targetValue)); } } }
其他实用方法
我们还定义了一些辅助方法,如calculateIntervals
和findIndexOf
,它们用于计算数据间隔和处理不精确的匹配情况。
private static double[] calculateIntervals(double[] values) {
double[] intervals = new double[values.length - 1];
for (int i = 0; i < intervals.length; i++) {
intervals[i] = values[i + 1] - values[i];
}
return intervals;
}
private static int findIndexOf(double[] arr, double value) {
int index = Arrays.binarySearch(arr, value);
return (index >= 0) ? index : -index - 1;
}
输出结果
在解析完成后,我们将结果转换为JSON格式并保存到文件中。
String jsonPrettyStr = JSONUtil.toJsonPrettyStr(parsedData);
FileUtil.writeUtf8String(jsonPrettyStr, "temp.json");
log.info("解析完成");
常见问题
1. 如何处理缺失的缩放因子?
如果NC文件没有定义scale_factor
,默认情况下使用缩放因子1
,即数据不进行缩放。
示例:
假设 NetCDF 文件中实际的温度值是浮点数 25.6,但为了节省空间,文件中存储的是一个整数 256,同时文件中包含一个 scale_factor
为 0.1。在读取数据时,你需要用这个 scale_factor
去乘以存储的整数值 256,从而得到实际的温度 25.6。代码中如果找到了 scale_factor
,就会将读取的整数值乘以这个因子,得到实际的浮点数值
2. 为什么要计算经纬度的间隔?
计算间隔有助于判断经纬度数据的分布,尤其是在处理非均匀分布的数据时,这个步骤非常重要。
3. 如何处理异常情况?
在代码中使用了try-with-resources
语法,确保在发生异常时正确关闭文件资源,并在catch
块中抛出明确的异常信息。
结语
这篇博客详细介绍了如何使用Java解析NC文件,并将其数据转化为JSON格式。在实际项目中,这种方法可以用于处理大量的气象数据。希望这篇博客能帮助你更好地理解和应用NC文件的解析技术。
扩展
如果netcdfAll的pom依赖下载不来可以去网址手动下载
Index of /edu/ucar/netcdfAll/5.5.2
或者设置maven仓库的镜像仓库地址 也能让maven载入进来该依赖
https://artifacts.unidata.ucar.edu/repository/unidata-all/