Java解析气象热力图NC文件:完整实现与代码解析

引言

在气象和环境科学领域,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文件的核心逻辑,它包括以下几个步骤:

  1. 读取数据:使用NetcdfFile类打开NC文件,并查找所需的数据变量。

    try (NetcdfFile open = NetcdfFiles.open(localFilePath)) {
        Variable data = open.findVariable("data");
    } catch (IOException | InvalidRangeException e) {
        throw new RuntimeException(e);
    }
    

  2. 处理缩放因子:某些NC文件会使用scale_factor来缩放存储的数据,我们在读取数据时需要应用这个缩放因子。

    Attribute scaleFactor = data.findAttribute(SCALE_FACTOR);
    float v = 1f;
    if (scaleFactor != null) {
        v = Objects.requireNonNull(scaleFactor.getNumericValue()).floatValue();
    }
    
  3. 提取经纬度数据:解析经度和纬度数据,并计算其最小值、最大值和间隔。

    Variable lon = open.findVariable(LON);
    Variable lat = open.findVariable(LAT);
    double[] lonArr = (double[]) lon.read().copyTo1DJavaArray();
    double[] latArr = (double[]) lat.read().copyTo1DJavaArray();
    

  4. 数据处理:根据经纬度坐标遍历所有数据点,提取数据并存储为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));
            }
        }
    }
    

其他实用方法

我们还定义了一些辅助方法,如calculateIntervalsfindIndexOf,它们用于计算数据间隔和处理不精确的匹配情况。

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/

  • 32
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值