三木猿 发表于 2021-6-24 17:10

利用道格拉斯-普克算法优化压缩经纬度图形点位数(进阶版)

本帖最后由 三木猿 于 2021-7-2 16:55 编辑

代码已更新成进阶版,原版代码太过简陋,让我们的算法工程师看的很不顺眼,所以又更新了一版,这次的10000个点能简化成200多个点,并且不丢失轮廓
分得清吗,前一个图的有1103个点,后一个图有185个点。
道格拉斯-普克算法:懒得解释,请参考:https://zhuanlan.zhihu.com/p/355323735
管你看不看得懂,拿过去用就完了!
优化前:
优化后:

代码:import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import net.sf.geographiclib.Geodesic;
import org.apache.commons.lang3.StringUtils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
* @ClassName: DPUtil
* @Package: tos-product
* @Description: 点位数优化
* @Author: sen.yang@youhualin.com
* @Date: 2021/6/25 9:25
* @Copyright: 悠桦林信息科技(上海)有限公司
*/

public class DpUtil {

    /**
   * 默认的点到轨迹线最大距离误差阈值(单位:米)
   */
    private static final double DEFAULTD_MAX = 30.0;

    /**
   * @param dis   经纬度字符串
   * @param range 控制数据经度压缩的极差
   */
    public static String smooth(String dis, double range) {
      String[] points = dis.split(";");
      List<String> strings = Arrays.asList(points);
      //拆分list,这里的意思是每10条算一条曲线,利用道格拉斯-普克算法对这条曲线进行优化
      List<List<String>> lists = CollectionUtils.splitList(strings, strings.size() / 2);
      List<TyPoint> tyPointList = new ArrayList<>();
      int id = 0;
      for (List<String> stringList : lists) {
            List<TyPoint> list = new ArrayList<>();
            for (int i = 0; i < stringList.size(); i++) {
                if (StringUtils.isNotBlank(stringList.get(i))) {
                  TyPoint data = new TyPoint();
                  data.setId(id);
                  String[] split = stringList.get(i).split(",");
                  data.setX(Double.parseDouble(split));
                  data.setY(Double.parseDouble(split));
                  list.add(data);
                  id++;
                }

            }
            List<TyPoint> list1 = dpAlgorithm(list, range);
            tyPointList.addAll(list1);
      }
      StringBuilder res = new StringBuilder();
      Map<Integer, TyPoint> collect = tyPointList.stream().distinct().collect(Collectors.toMap(TyPoint::getId, Function.identity(), (k1, k2) -> k2));
      for (int i = 0; i < strings.size(); i++) {
            TyPoint tyPoint = collect.get(i);
            if (tyPoint != null) {
                res.append(tyPoint.getX()).append(",").append(tyPoint.getY()).append(";");
            }
      }
      res.deleteCharAt(res.lastIndexOf(";"));
      return res.toString();
    }

    /**
   * DP算法入口
   * 传入压缩前的轨迹点集合
   * 输出压缩后的结果轨迹点集合
   *
   * @param originPoints 压缩前的轨迹点集合
   * @param dMax         点到轨迹线最大距离误差阈值
   * @return 优化结果
   */
    public static List<TyPoint> dpAlgorithm(List<TyPoint> originPoints, Double dMax) {
      List<TyPoint> resultPoints = new ArrayList<>();
      resultPoints.add(originPoints.get(0));//获取第一个原始经纬度点坐标并添加到过滤后的数组中
      resultPoints.add(originPoints.get(originPoints.size() - 1));//获取最后一个原始经纬度点坐标并添加到过滤后的数组中
      //最大距离误差阈值
      if (dMax == null) {
            dMax = DEFAULTD_MAX;
      }
      int start = 0;
      int end = originPoints.size() - 1;
      compression(originPoints, resultPoints, start, end, dMax);

      return resultPoints;
    }

    /**
   * 函数功能:根据最大距离限制,采用DP方法递归的对原始轨迹进行采样,得到压缩后的轨迹
   *
   * @param originPoints:原始经纬度坐标点数组
   * @param resultPoints:保持过滤后的点坐标数组
   * @param start:起始下标
   * @param end:终点下标
   * @param dMax:预先指定好的最大距离误差      计算
   */
    public static void compression(List<TyPoint> originPoints, List<TyPoint> resultPoints,
                                 int start, int end, double dMax) {
      if (start < end) {//递归进行的条件
            double maxDist = 0;//最大距离
            int curPt = 0;//当前下标
            for (int i = start + 1; i < end; i++) {
                //当前点到对应线段的距离
                double curDist = distToSegment(originPoints.get(start), originPoints.get(end), originPoints.get(i));
                if (curDist > maxDist) {
                  maxDist = curDist;
                  curPt = i;
                }//求出最大距离及最大距离对应点的下标
            }
            //若当前最大距离大于最大距离误差
            if (maxDist >= dMax) {
                resultPoints.add(originPoints.get(curPt));//将当前点加入到过滤数组中
                //将原来的线段以当前点为中心拆成两段,分别进行递归处理
                compression(originPoints, resultPoints, start, curPt, dMax);
                compression(originPoints, resultPoints, curPt, end, dMax);
            }
      }
    }

    /**
   * 函数功能:使用三角形面积(使用海伦公式求得)相等方法计算点pX到点pA和pB所确定的直线的距离
   *
   * @param pA:起始点
   * @param pB:结束点
   * @param pX:第三个点
   * @return distance:点pX到pA和pB所在直线的距离
   */
    public static double distToSegment(TyPoint pA, TyPoint pB, TyPoint pX) {
      double a = Math.abs(Geodesic.WGS84.Inverse(pA.y, pA.x, pB.y, pB.x).s12);
      double b = Math.abs(Geodesic.WGS84.Inverse(pA.y, pA.x, pX.y, pX.x).s12);
      double c = Math.abs(Geodesic.WGS84.Inverse(pB.y, pB.x, pX.y, pX.x).s12);
      double p = (a + b + c) / 2.0;
      double s = Math.sqrt(Math.abs(p * (p - a) * (p - b) * (p - c)));
      return s * 2.0 / a;
    }
    /**
   * 返回一个点是否在一个多边形边界上
   *
   * @param polygon 多边形坐标点列表
   * @param point1   待判断点
   * @return true 点在多边形边上,false 点不在多边形边上。
   */
    public static boolean isPointInPolygonBoundary(String point1, String polygon) {
      String[] split = point1.split(",");
      TyPoint point = new TyPoint(0, Double.parseDouble(split), Double.parseDouble(split));
      List<TyPoint> mPoints = new ArrayList<>();
      String[] split1 = polygon.split(";");
      for (int i = 0; i < split1.length; i++) {
            String[] split2 = split1.split(",");
            mPoints.add(new TyPoint(i, Double.parseDouble(split2), Double.parseDouble(split2)));
      }
      for (int i = 0; i < mPoints.size(); i++) {
            TyPoint p1 = mPoints.get(i);
            TyPoint p2 = mPoints.get((i + 1) % mPoints.size());
            // 取多边形任意一个边,做点point的水平延长线,求解与当前边的交点个数

            // point 在p1p2 底部 --> 无交点
            if (point.y < Math.min(p1.y, p2.y))
                continue;
            // point 在p1p2 顶部 --> 无交点
            if (point.y > Math.max(p1.y, p2.y))
                continue;

            // p1p2是水平线段,要么没有交点,要么有无限个交点
            if (p1.y == p2.y) {
                double minX = Math.min(p1.x, p2.x);
                double maxX = Math.max(p1.x, p2.x);
                // point在水平线段p1p2上,直接return true
                if ((point.y == p1.y) && (point.x >= minX && point.x <= maxX)) {
                  return true;
                }
            } else { // 求解交点
                double x = (point.y - p1.y) * (p2.x - p1.x) / (p2.y - p1.y) + p1.x;
                if (x == point.x) // 当x=point.x时,说明point在p1p2线段上
                  return true;
            }
      }
      return false;
    }

}
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
class TyPoint {
    public int id;//点ID
    public double x;//经度
    public double y;//纬度
}

三木猿 发表于 2021-7-2 16:04

niusan521 发表于 2021-7-2 12:56
上来直接撸代码看的人一脸懵逼,优化的突出点和算法精髓讲解下。。

但是大概原理你可以搜一下道格拉斯普客算法,原理比较简单

三木猿 发表于 2021-7-2 16:03

niusan521 发表于 2021-7-2 12:56
上来直接撸代码看的人一脸懵逼,优化的突出点和算法精髓讲解下。。

都说了不是算法工程师,咋解说,我自己都看不懂{:301_1001:}

三木猿 发表于 2021-6-24 17:11

大部分内容靠百度,具体哪个帖子忘了,谁找到了可以发出来,我置顶

三木猿 发表于 2021-6-24 17:12

本人非算法工程师,java开发一枚,别找我写啥算法,头给你卸了

52jcool 发表于 2021-6-24 17:32

高端,牛逼大了,收藏学习

sam喵喵 发表于 2021-6-24 18:06

边缘变平滑了。
大佬请教下,这么整的作用是什么

三木猿 发表于 2021-6-24 18:53

sam喵喵 发表于 2021-6-24 18:06
边缘变平滑了。
大佬请教下,这么整的作用是什么

点位过多,前端处理时就会卡死,需要在后端压缩下点位数量

sam喵喵 发表于 2021-6-24 19:28

三木猿 发表于 2021-6-24 18:53
点位过多,前端处理时就会卡死,需要在后端压缩下点位数量

这算法好野蛮,丢失细节颇多

恰巧 发表于 2021-6-24 21:05

看的好蒙啊{:301_985:}

列明 发表于 2021-6-25 00:12

是粉紅色區域的邊啊。

niusan521 发表于 2021-7-2 12:56

上来直接撸代码看的人一脸懵逼,优化的突出点和算法精髓讲解下。。
页: [1] 2
查看完整版本: 利用道格拉斯-普克算法优化压缩经纬度图形点位数(进阶版)