码峰博客 – 码而思

分享积累从此时此刻开始

java 通过灰度图获取经纬度坐标对应的高程数值

总体思路:

1.统一输入坐标数据,如果是 4326 转为 3857,不管是单个 还是一组,统一转为 一组

2.计算瓦片坐标XY,基于3857 坐标(Web墨卡托或Web Mercator),获取 22 级的瓦片 X、Y,(为什么用22级,因为我们只生成了 22级的瓦片数据)

3.获取坐标相对瓦片的位置,计算坐标在单张瓦片图上的的相对位置 RelativeX RelativeY,左上为0,0

4.获取灰度值,根据瓦片XY 以及 相对位置 RelativeX RelativeY,获取坐标对应瓦片图的像素位置上的像素值

5.获取单张瓦片基准值

6.获取高程,利用像素值 和 瓦片基准值 进行高程的计算

背景说明

我们只提供 22 级的瓦片图

package com.sxkc.modules.util;

import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.geotools.referencing.CRS;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.GeometryFactory;
import org.opengis.referencing.crs.CRSAuthorityFactory;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform;

import javax.imageio.ImageIO;
import java.awt.image.*;
import java.io.File;
import java.io.FileReader;
import java.io.Reader;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.stream.Collectors;

import com.google.gson.Gson;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class HeightUtil {

    @Value("${file.resourcePath}")
    private String resourcePath;

//    private static final String  JSON_FILEs =  "%s/elv/1/22/%s/%s.json";
//    private static final String  TILE_FILEs =  "%s/elv/1/22/%s/%s.png";
        private static final String  JSON_FILE1 =  "%s/elv/";
        private static final String  TILE_FILE1 =  "%s/elv/";


    private static final String  JSON_FILE2 =  "/22/%s/%s.json";
    private static final String  TILE_FILE2 =  "/22/%s/%s.png";

    private static GeometryFactory geometryFactory = new GeometryFactory();// jts GeometryFactory


    private static CoordinateReferenceSystem crs4326; // WGS84 经纬度坐标系
    private static CoordinateReferenceSystem crs3857;   // 定义 Web Mercator 投影坐标系

    private String taskId = "";
    private int zoomLevel = 22;
    private String inputEpsg = "4326"; // 默认为4326 可传3857
    private String JSON_FILE="";
    private String TILE_FILE="";

    public HeightUtil() {
    }

    public void init(String taskId) {
        this.taskId = taskId;
    }
    public void init(String taskId, String inputEpsg) {
        this.taskId = taskId;
        this.inputEpsg  = inputEpsg;
    }

    public void init(String taskId, String inputEpsg,String zlevel) {
        this.taskId = taskId;
        this.inputEpsg  = inputEpsg;
        if(StringUtils.isEmpty(zlevel)){
            zlevel="0";
        }
        this.JSON_FILE=JSON_FILE1+zlevel+JSON_FILE2;
        this.TILE_FILE=TILE_FILE1+zlevel+TILE_FILE2;
    }
    // 坐标转换
    private double[] lonlat2xy(double[] lonlat){
        double[] xy = new double[2];
        try {
            // 创建坐标系对象
            if(ObjectUtils.isEmpty(crs4326) || ObjectUtils.isEmpty(crs3857)){
                CRSAuthorityFactory factory = CRS.getAuthorityFactory(true);
                crs4326 = factory.createCoordinateReferenceSystem("EPSG:4326");
                // 定义 Web Mercator 投影坐标系
                crs3857 = CRS.decode("EPSG:3857");
            }

            // 创建转换器,从 WGS 84 到 Web Mercator
            MathTransform transform = CRS.findMathTransform(crs4326, crs3857);

            // 将经纬度坐标转换为投影坐标
            double[] projectedCoords = new double[2];
            transform.transform(new double[]{lonlat[0], lonlat[1]}, 0, projectedCoords, 0, 1);
            xy = projectedCoords;
            // System.out.println("Web Mercator XY: " + xy[0] + ", " + xy[1]);
        }catch (Exception e){
            e.printStackTrace();
        }
        return xy;
    }


    private Integer[] getTileXY(double[] xy){
        Integer[] tileXY = new Integer[2];
        try {

            // 计算瓦片坐标
            int tileX = (int) Math.floor((xy[0] + 20037508.34) / (2 * 20037508.34) * (1 << zoomLevel));
            int tileY = (int) Math.floor((20037508.34 - xy[1]) / (2 * 20037508.34) * (1 << zoomLevel));
            // System.out.println("Tile Coordinates: " + tileX + ", " + tileY);

            tileXY[0] = tileX;
            tileXY[1] = tileY;
        }catch (Exception e){
            e.printStackTrace();
        }

        return tileXY;
    }

    // 获取经纬度相对于单个瓦片的相对坐标
    private Integer[] getRelativeXY(double[] xy, Integer[] tileXY){
        Integer[] relativeXY = new Integer[2];

        BigDecimal mercatorX = BigDecimal.valueOf(xy[0]);
        BigDecimal mercatorY = BigDecimal.valueOf(xy[1]);

        // 计算瓦片的空间范围  参考 mercantile.xy_bounds 的公式 , 使用 BigDecimal 计算更精确
        // double CE = 2 * Math.PI * 6378137.0;
        // double tileSize = CE / Math.pow(2, zoomLevel);
        // double CE_2 = CE/2;
        BigDecimal CE = BigDecimal.valueOf(2 * Math.PI * 6378137.0);
        BigDecimal tileSize = CE.divide(BigDecimal.valueOf(Math.pow(2, zoomLevel)), 13 , BigDecimal.ROUND_UP);
        BigDecimal CE_2 = CE.divide(BigDecimal.valueOf(2), 13 , BigDecimal.ROUND_UP);

        BigDecimal minX = tileSize.multiply(BigDecimal.valueOf(tileXY[0])).subtract(CE_2);
        BigDecimal maxX = minX.add(tileSize);
        BigDecimal maxY = CE_2.subtract(tileSize.multiply(BigDecimal.valueOf(tileXY[1])));
        BigDecimal minY = maxY.subtract(tileSize);
        // System.out.println("Tile Spatial Range: " + minX + ", " + minY + ", " + maxX + ", " + maxY);

        // 利用 (目标点X - 最小X)/ (最大X - 最小X)计算出x比例,乘以 256 就得到了基于图片的坐标
        BigDecimal ratioX = (mercatorX.subtract(minX)).divide(maxX.subtract(minX), 10, RoundingMode.HALF_UP );
        BigDecimal ratioY = (BigDecimal.valueOf(1).subtract((mercatorY.subtract(minY)).divide(maxY.subtract(minY), 10, RoundingMode.HALF_UP )));
        BigDecimal relativeX = ratioX.multiply(BigDecimal.valueOf(256));
        BigDecimal relativeY = ratioY.multiply(BigDecimal.valueOf(256));
        // System.out.println("Tile relative ratio: " + ratioX + ", " + ratioY );
        // System.out.println("Tile relative XY: " + relativeX + ", " + relativeY );

        relativeXY[0] = (int) relativeX.floatValue();
        relativeXY[1] = (int) relativeY.floatValue();
        return relativeXY;
    }

    // 获取灰度值
    private Integer getGray(Integer[] tileXY, Integer relativeXY[]){
        String tileImagePath = String.format(resourcePath + TILE_FILE, this.taskId, tileXY[0], tileXY[1]);
        Integer gray = 0;
        try {
            // 读取图像文件
            File imageFile = new File(tileImagePath);
            BufferedImage image = ImageIO.read(imageFile);
            // 获取单通道图像的指定像素位置的灰度值
            Raster raster = image.getRaster();
            gray = raster.getSample(relativeXY[0], relativeXY[1], 0);
            // 输出像素值
            // System.out.println("灰度值:" + gray);
            return gray;
        } catch (Exception e) {
//            e.printStackTrace();
        }
        return gray;
    }

    // 获取elv
    private Float getElv(Integer[] tileXY){
        Float elv = 0F;
        String tileJsonPath = String.format(resourcePath + JSON_FILE, this.taskId, tileXY[0], tileXY[1]);
        try {
            Gson gson = new Gson();
            Reader reader = new FileReader(tileJsonPath);
            JSONObject object = gson.fromJson(reader, JSONObject.class);
            if (object.containsKey("elv")){
                elv = Float.valueOf(object.get("elv").toString());
            }
            // 输出elv
            // System.out.println("elv:" + elv);
        } catch (Exception e) {
            // e.printStackTrace();
        }
        return elv;
    }

    public Float getHeight(double longitude, double latitude){
        Float height = 0f;
        try {
            double[] lonlat = null;
            double[] xy = null;
            if(this.inputEpsg.equals("4326")) {
                lonlat = new double[]{longitude, latitude};
                // 墨卡托坐标
                xy = lonlat2xy(lonlat);
            }else{
                xy = new double[]{longitude, latitude};
            }
            // 通过 墨卡托坐标 过去瓦片 xy
            Integer[] tileXY = getTileXY(xy);

            // 获取经纬度 相对于瓦片的相对坐标 , 从而获取灰度值
            Integer[] relativeXY = getRelativeXY(xy, tileXY);
            Integer gray = getGray(tileXY, relativeXY);

            // 获取elv
            Float elv = getElv(tileXY);

            height = (gray/(255/60)-30)/100 + elv;
        }catch (Exception e) {
            // e.printStackTrace();
        }
        return height;
    }

    public Coordinate[] getHeight(Coordinate[] coordinates){
        List<double[]> coordinateList = Arrays.stream(coordinates)
                .map(coordinate -> new double[]{coordinate.getX(), coordinate.getY()})
                .collect(Collectors.toList());
        List<BigDecimal[]> coordinateListZ = this.getHeight(coordinateList);
        Coordinate[] coordinatesReturn = coordinateListZ.stream()
                .map(bigDecimalArray -> new Coordinate(bigDecimalArray[0].doubleValue(), bigDecimalArray[1].doubleValue(), bigDecimalArray[2].doubleValue()))
                .toArray(Coordinate[]::new);
        return coordinatesReturn;
    }

    public List<BigDecimal[]> getHeight(List<double[]> inputCoords){
        List<BigDecimal[]> coordinates = new ArrayList<>();

//        for (double[] item : inputCoords) {
//            float z = getHeight(item[0], item[1]);
//            coordinates.add(new double[]{item[0], item[1], z});
//        }
//        if(coordinates.size() > 0){
//            return coordinates;
//        }

        // 每组数量,最多分10组
        Integer groupNum = (int)(inputCoords.size()/10) + 1;
        List<List<double[]>>  inputCoordsParttion = Lists.partition(inputCoords, groupNum);

        try {
            // 创建线程池
            ExecutorService executorService = Executors.newFixedThreadPool(inputCoordsParttion.size());

            // 创建任务列表
            List<Callable<List<BigDecimal[]>>> tasks = new ArrayList<>();
            Integer i = 0;
            for (List<double[]> item : inputCoordsParttion) {
                tasks.add(new DataProcessingTask(this, item));
                i++;
            }
            // 提交任务并获取Future对象列表
            List<Future<List<BigDecimal[]>>> futures = executorService.invokeAll(tasks);

            // 处理任务结果 这里是阻塞式的 有先后顺序的返回
            for (Future<List<BigDecimal[]>> future : futures) {
                // 获取任务结果,注意此处会阻塞等待任务完成
                List<BigDecimal[]> result = future.get();
                coordinates.addAll(result);
            }
            // 关闭线程池
            executorService.shutdown();

        }catch (Exception e){
            e.printStackTrace();
        }

        return coordinates;
    }
}

class DataProcessingTask implements Callable<List<BigDecimal[]>> {
    private List<double[]> coords;
    private HeightUtil heightUtil;
    public DataProcessingTask(HeightUtil heightUtil, List<double[]> coords) {
        this.coords = coords;
        this.heightUtil = heightUtil;
    }

    @Override
    public List<BigDecimal[]> call() throws Exception {
        List<BigDecimal[]> coordinates = new ArrayList<>();
        for (double[] item : coords) {
            float z = heightUtil.getHeight(item[0], item[1]);
            coordinates.add(new BigDecimal[]{BigDecimal.valueOf(item[0]), BigDecimal.valueOf(item[1]), BigDecimal.valueOf(z)});
        }
        return coordinates;
    }
}

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注