|
|
@@ -1,71 +1,90 @@
|
|
|
package com.ylx.common.utils;
|
|
|
|
|
|
import cn.hutool.core.util.ObjectUtil;
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.math.RoundingMode;
|
|
|
|
|
|
/**
|
|
|
- * 计算经纬度之间的直线距离
|
|
|
+ * Haversine公式计算两点经纬度球面直线距离工具类
|
|
|
+ * 所有坐标入参统一使用 BigDecimal,保证数据库高精度存储兼容
|
|
|
*/
|
|
|
-public class DistanceUtil {
|
|
|
+public final class DistanceUtil {
|
|
|
|
|
|
- private static final double EARTH_RADIUS_M = 6_371_000; // 地球半径,单位米
|
|
|
- private static final double TO_RADIANS = Math.PI / 180.0;
|
|
|
+ // 地球平均半径,单位:米
|
|
|
+ private static final double EARTH_RADIUS_M = 6_371_000D;
|
|
|
+ // 角度转弧度系数
|
|
|
+ private static final double TO_RADIANS = Math.PI / 180.0D;
|
|
|
+ // 米转公里换算值
|
|
|
+ private static final BigDecimal METER_TO_KM = new BigDecimal("1000");
|
|
|
+ // 经纬度极值常量
|
|
|
+ private static final BigDecimal MAX_LAT = new BigDecimal("90");
|
|
|
+ private static final BigDecimal MAX_LON = new BigDecimal("180");
|
|
|
+ private static final BigDecimal ZERO = BigDecimal.ZERO;
|
|
|
|
|
|
+ // 私有构造,禁止实例化工具类
|
|
|
+ private DistanceUtil() {
|
|
|
+ throw new AssertionError("工具类不可实例化");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===================== 对外业务方法 =====================
|
|
|
/**
|
|
|
- * 格式化两点间距离
|
|
|
- *
|
|
|
+ * 获取距离(单位:米,四舍五入整数字符串)
|
|
|
* @param userLat 用户纬度
|
|
|
* @param userLon 用户经度
|
|
|
* @param shopLat 门店纬度
|
|
|
* @param shopLon 门店经度
|
|
|
- * @return 距离(米),若坐标无效则返回 "未知"
|
|
|
+ * @return 有效距离返回米数字,坐标为空/0/超出范围 返回 "未知"
|
|
|
*/
|
|
|
- public static String formatDistance(Double userLat, Double userLon, Double shopLat, Double shopLon) {
|
|
|
- // 任意坐标空/0 → 未知
|
|
|
- if (ObjectUtil.hasNull(userLat, userLon, shopLat, shopLon)
|
|
|
- || userLat == 0 || userLon == 0 || shopLat == 0 || shopLon == 0) {
|
|
|
+ public static String formatDistance(BigDecimal userLat, BigDecimal userLon,
|
|
|
+ BigDecimal shopLat, BigDecimal shopLon) {
|
|
|
+ if (!isAllCoordinateValid(userLat, userLon, shopLat, shopLon)) {
|
|
|
return "未知";
|
|
|
}
|
|
|
-
|
|
|
- // 验证坐标范围(可选,增加健壮性)
|
|
|
- if (!isValidCoordinate(userLat, userLon) || !isValidCoordinate(shopLat, shopLon)) {
|
|
|
- return "未知"; // 或抛出异常
|
|
|
- }
|
|
|
-
|
|
|
- double meter = getDistance(userLat, userLon, shopLat, shopLon);
|
|
|
+ double meter = getDistance(userLat.doubleValue(), userLon.doubleValue(),
|
|
|
+ shopLat.doubleValue(), shopLon.doubleValue());
|
|
|
return String.valueOf(Math.round(meter));
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 格式化两点间距离(单位:公里)
|
|
|
- *
|
|
|
+ * 获取距离(单位:公里,保留2位小数)
|
|
|
* @param userLat 用户纬度
|
|
|
* @param userLon 用户经度
|
|
|
* @param shopLat 门店纬度
|
|
|
* @param shopLon 门店经度
|
|
|
- * @return 距离(公里),若坐标无效则返回 "未知"
|
|
|
+ * @return 公里字符串,坐标非法返回 "未知"
|
|
|
*/
|
|
|
- public static String formatDistanceInKilometers(Double userLat, Double userLon, Double shopLat, Double shopLon) {
|
|
|
- if (ObjectUtil.hasNull(userLat, userLon, shopLat, shopLon)
|
|
|
- || userLat == 0 || userLon == 0 || shopLat == 0 || shopLon == 0) {
|
|
|
- return "未知";
|
|
|
+ public static BigDecimal formatDistanceInKilometers(BigDecimal userLat, BigDecimal userLon,
|
|
|
+ BigDecimal shopLat, BigDecimal shopLon) {
|
|
|
+ if (!isAllCoordinateValid(userLat, userLon, shopLat, shopLon)) {
|
|
|
+ return null;
|
|
|
}
|
|
|
- if (!isValidCoordinate(userLat, userLon) || !isValidCoordinate(shopLat, shopLon)) {
|
|
|
+ double meterVal = getDistance(userLat.doubleValue(), userLon.doubleValue(),
|
|
|
+ shopLat.doubleValue(), shopLon.doubleValue());
|
|
|
+ BigDecimal meter = new BigDecimal(meterVal);
|
|
|
+ return meter.divide(METER_TO_KM, 2, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 前端友好展示距离
|
|
|
+ * 小于1000米:xx米;大于等于1000米:xx.xx公里;坐标异常:未知
|
|
|
+ */
|
|
|
+ public static String formatDisplay(BigDecimal userLat, BigDecimal userLon,
|
|
|
+ BigDecimal shopLat, BigDecimal shopLon) {
|
|
|
+ String meterStr = formatDistance(userLat, userLon, shopLat, shopLon);
|
|
|
+ if ("未知".equals(meterStr)) {
|
|
|
return "未知";
|
|
|
}
|
|
|
- double meter = getDistance(userLat, userLon, shopLat, shopLon);
|
|
|
- double kilometer = meter / 1000.0;
|
|
|
- // 四舍五入到两位小数
|
|
|
- return String.format("%.2f", kilometer);
|
|
|
+ long meter = Long.parseLong(meterStr);
|
|
|
+ if (meter < METER_TO_KM.longValue()) {
|
|
|
+ return meter + "米";
|
|
|
+ }
|
|
|
+ BigDecimal meterNum = new BigDecimal(meter);
|
|
|
+ BigDecimal km = meterNum.divide(METER_TO_KM, 2, RoundingMode.HALF_UP);
|
|
|
+ return km + "公里";
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 获取两点间球面距离(米)
|
|
|
- *
|
|
|
- * @param lat1 纬度1
|
|
|
- * @param lon1 经度1
|
|
|
- * @param lat2 纬度2
|
|
|
- * @param lon2 经度2
|
|
|
- * @return 距离(米)
|
|
|
+ * 底层计算:返回两点球面距离(单位米)
|
|
|
*/
|
|
|
public static double getDistance(double lat1, double lon1, double lat2, double lon2) {
|
|
|
double lat1Rad = lat1 * TO_RADIANS;
|
|
|
@@ -73,27 +92,43 @@ public class DistanceUtil {
|
|
|
double lat2Rad = lat2 * TO_RADIANS;
|
|
|
double lon2Rad = lon2 * TO_RADIANS;
|
|
|
|
|
|
- // 使用 Haversine 公式(更精确,避免 acos 在极近距离下的精度问题)
|
|
|
double deltaLat = lat2Rad - lat1Rad;
|
|
|
double deltaLon = lon2Rad - lon1Rad;
|
|
|
|
|
|
- double a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
|
|
|
- Math.cos(lat1Rad) * Math.cos(lat2Rad) *
|
|
|
- Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2);
|
|
|
+ double a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2)
|
|
|
+ + Math.cos(lat1Rad) * Math.cos(lat2Rad)
|
|
|
+ * Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2);
|
|
|
|
|
|
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
|
-
|
|
|
return EARTH_RADIUS_M * c;
|
|
|
}
|
|
|
|
|
|
+ // ===================== 坐标校验私有方法 =====================
|
|
|
+ /**
|
|
|
+ * 批量校验四个坐标:非空、不等于0、经纬度范围合法
|
|
|
+ */
|
|
|
+ private static boolean isAllCoordinateValid(BigDecimal lat1, BigDecimal lon1,
|
|
|
+ BigDecimal lat2, BigDecimal lon2) {
|
|
|
+ // 任意坐标为null
|
|
|
+ if (ObjectUtil.hasNull(lat1, lon1, lat2, lon2)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ // 任意坐标数值等于0
|
|
|
+ if (lat1.compareTo(ZERO) == 0
|
|
|
+ || lon1.compareTo(ZERO) == 0
|
|
|
+ || lat2.compareTo(ZERO) == 0
|
|
|
+ || lon2.compareTo(ZERO) == 0) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ // 两组坐标分别校验范围
|
|
|
+ return isSingleCoordinateValid(lat1, lon1) && isSingleCoordinateValid(lat2, lon2);
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
- * 验证坐标是否在有效范围内
|
|
|
- *
|
|
|
- * @param lat 纬度
|
|
|
- * @param lon 经度
|
|
|
- * @return 是否有效
|
|
|
+ * 校验单组经纬度范围:纬度[-90,90] 经度[-180,180]
|
|
|
*/
|
|
|
- private static boolean isValidCoordinate(double lat, double lon) {
|
|
|
- return Math.abs(lat) <= 90.0 && Math.abs(lon) <= 180.0;
|
|
|
+ private static boolean isSingleCoordinateValid(BigDecimal lat, BigDecimal lon) {
|
|
|
+ return lat.abs().compareTo(MAX_LAT) <= 0
|
|
|
+ && lon.abs().compareTo(MAX_LON) <= 0;
|
|
|
}
|
|
|
}
|