定位区域判断更新,更加准确

This commit is contained in:
磷叶 2025-04-10 19:56:50 +08:00
parent 8534e08033
commit 578adc465c
11 changed files with 222 additions and 79 deletions

View File

@ -24,6 +24,7 @@ import org.locationtech.jts.io.WKTWriter;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.referencing.operation.MathTransform;
import org.locationtech.jts.operation.distance.DistanceOp;
import java.io.File;
import java.math.BigDecimal;
@ -243,51 +244,37 @@ public class GeoUtils {
double toleranceValue = tolerance == null ? 0 : tolerance.doubleValue();
GeometryFactory geometryFactory = new GeometryFactory();
Coordinate coordinate = new Coordinate(lon, lat);
Point point = geometryFactory.createPoint(coordinate);
Point point = geometryFactory.createPoint(new Coordinate(lon, lat));
if (polygon != null && polygon.contains(point)) {
return true;
} else {
// 获取多边形的外边界
Coordinate[] coordinates = polygon.getCoordinates();
for (Coordinate coord : coordinates) {
double distance = calculateDistance(lat, lon, coord.y, coord.x);
if (distance <= toleranceValue) {
return true;
}
}
return false;
// 计算点到多边形的最短距离若在误差范围内则认为在多边形内
double distance = calculateMinDistanceToPolygon(polygon, lon, lat);
return distance <= toleranceValue;
}
}
/**
* 判断一个点是否在一个缩短后的圆形区域内
* 判断一个点是否在缩短后的区域内
* */
public static boolean isInPolygonWithShorten(BigDecimal longitude, BigDecimal latitude, Geometry polygon, BigDecimal shortenDistance) {
if (longitude == null || latitude == null || polygon == null ) {
if (longitude == null || latitude == null || polygon == null) {
return false;
}
double lon = longitude.doubleValue();
double lat = latitude.doubleValue();
double shortenDistanceValue = shortenDistance == null ? 0 : shortenDistance.doubleValue();
GeometryFactory geometryFactory = new GeometryFactory();
Coordinate coordinate = new Coordinate(lon, lat);
Point point = geometryFactory.createPoint(coordinate);
Point point = GEOMETRY_FACTORY.createPoint(new Coordinate(lon, lat));
if (polygon.contains(point)) {
// 获取多边形的外边界
Coordinate[] coordinates = polygon.getCoordinates();
for (Coordinate coord : coordinates) {
double distance = calculateDistance(lat, lon, coord.y, coord.x);
if (shortenDistanceValue >= distance) {
return false;
}
}
return true;
} else {
return false;
// 计算点到多边形边界的最短距离
double minDistance = calculateMinDistanceToPolygon(polygon, lon, lat);
// 如果最短距离小于缩短距离则认为点在缩短区域外
return minDistance > shortenDistanceValue;
}
return false;
}
// 将角度转换为弧度
@ -346,34 +333,65 @@ public class GeoUtils {
return EARTH_RADIUS * c; // 距离
}
/**
* 判获取到最近一个运营区
* */
public static boolean getNearestOperatingArea (String longitude, String latitude, List<Geometry> polygon) {
double lon = Double.parseDouble(longitude);
double lat = Double.parseDouble(latitude);
* 计算给定点到多边形的最短距离单位
*/
public static double calculateMinDistanceToPolygon(Geometry polygon, double lon, double lat) {
Point point = GEOMETRY_FACTORY.createPoint(new Coordinate(lon, lat));
GeometryFactory geometryFactory = new GeometryFactory();
Coordinate coordinate = new Coordinate(lon, lat);
Point point = geometryFactory.createPoint(coordinate);
// 获取多边形的边界
Geometry boundary = polygon.getBoundary();
Coordinate[] coordinates = boundary.getCoordinates();
for (Geometry geometry : polygon) {
if (geometry.contains(point)) {
return true;
}else{
double minDistance = Double.MAX_VALUE;
}
// 遍历多边形的每条边
for (int i = 0; i < coordinates.length - 1; i++) {
Coordinate c1 = coordinates[i];
Coordinate c2 = coordinates[i + 1];
// 计算点到线段的最短距离
double distance = calculateDistanceToSegment(lat, lon, c1.y, c1.x, c2.y, c2.x);
minDistance = Math.min(minDistance, distance);
}
return false;
return minDistance;
}
/**
* 计算给定点到多边形的最短距离
* */
public double calculateMinDistanceToPolygon(Geometry polygon, double lon, double lat) {
Coordinate coord = new Coordinate(lon, lat);
Point point = new GeometryFactory().createPoint(coord);
return polygon.distance(point); // 返回给定点到多边形的最短距离
* 计算点到线段的最短距离单位
*/
private static double calculateDistanceToSegment(double lat, double lon,
double lat1, double lon1,
double lat2, double lon2) {
// 计算点到线段两端点的距离
double d1 = calculateDistance(lat, lon, lat1, lon1);
double d2 = calculateDistance(lat, lon, lat2, lon2);
double lineLength = calculateDistance(lat1, lon1, lat2, lon2);
// 如果线段长度接近0返回到端点的距离
if (lineLength < 0.000001) {
return Math.min(d1, d2);
}
// 计算点到线段的投影是否在线段上
double t = ((lat - lat1) * (lat2 - lat1) + (lon - lon1) * (lon2 - lon1)) /
((lat2 - lat1) * (lat2 - lat1) + (lon2 - lon1) * (lon2 - lon1));
if (t < 0) {
return d1; // 最近点是第一个端点
}
if (t > 1) {
return d2; // 最近点是第二个端点
}
// 计算投影点的坐标
double projLat = lat1 + t * (lat2 - lat1);
double projLon = lon1 + t * (lon2 - lon1);
// 返回点到投影点的距离
return calculateDistance(lat, lon, projLat, projLon);
}
}

View File

@ -0,0 +1,33 @@
package com.ruoyi.bst.area.domain.vo;
import com.ruoyi.bst.areaSub.domain.AreaSubVO;
import lombok.Data;
/**
* 定位所属区域信息
*/
@Data
public class LocationAreaVO {
// 是否在运营区内
private Boolean isInArea;
// 是否在运营区最小内边界
private Boolean isInAreaMin;
// 是否在运营区最大外边界
private Boolean isInAreaMax;
// 是否在禁行区内
private Boolean isInNoRidingArea;
// 位于的禁行区
private AreaSubVO inNoRidingArea;
// 是否靠近禁行区
private Boolean isNearNoRidingArea;
// 靠近的禁行区
private AreaSubVO nearNoRidingArea;
}

View File

@ -1,10 +1,12 @@
package com.ruoyi.bst.area.service;
import java.math.BigDecimal;
import java.util.List;
import com.ruoyi.bst.area.domain.Area;
import com.ruoyi.bst.area.domain.AreaQuery;
import com.ruoyi.bst.area.domain.AreaVO;
import com.ruoyi.bst.area.domain.vo.LocationAreaVO;
/**
* 运营区Service接口
@ -83,4 +85,13 @@ public interface AreaService
*/
public List<AreaVO> selectSimpleList(AreaQuery query);
/**
* 查询定位所属区域信息
* @param lon 经度
* @param lat 纬度
* @param areaId 运营区ID
* @return 定位所属区域信息
*/
public LocationAreaVO selectLocationArea(BigDecimal lon, BigDecimal lat, Long areaId);
}

View File

@ -1,5 +1,6 @@
package com.ruoyi.bst.area.service.impl;
import java.math.BigDecimal;
import java.util.List;
import org.locationtech.jts.geom.Geometry;
@ -10,8 +11,12 @@ import com.github.pagehelper.PageHelper;
import com.ruoyi.bst.area.domain.Area;
import com.ruoyi.bst.area.domain.AreaQuery;
import com.ruoyi.bst.area.domain.AreaVO;
import com.ruoyi.bst.area.domain.vo.LocationAreaVO;
import com.ruoyi.bst.area.mapper.AreaMapper;
import com.ruoyi.bst.area.service.AreaService;
import com.ruoyi.bst.area.utils.AreaUtil;
import com.ruoyi.bst.areaSub.domain.AreaSubVO;
import com.ruoyi.bst.areaSub.service.AreaSubService;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.collection.CollectionUtils;
import com.ruoyi.common.utils.map.GeoUtils;
@ -31,6 +36,9 @@ public class AreaServiceImpl implements AreaService
@Autowired
private AreaMapper areaMapper;
@Autowired
private AreaSubService areaSubService;
/**
* 查询运营区
*
@ -150,4 +158,20 @@ public class AreaServiceImpl implements AreaService
return areaMapper.logicDel(ids);
}
@Override
public LocationAreaVO selectLocationArea(BigDecimal lon, BigDecimal lat, Long areaId) {
if (lon == null || lat == null || areaId == null) {
return null;
}
// 查询运营区
AreaVO area = this.selectAreaById(areaId);
if (area == null) {
return null;
}
// 查询运营区内的禁行区
List<AreaSubVO> noRidingList = areaSubService.selectNoRidingListByAreaId(areaId);
return AreaUtil.getLocationArea(lon, lat, area, noRidingList);
}
}

View File

@ -1,23 +1,27 @@
package com.ruoyi.bst.area.utils;
import java.math.BigDecimal;
import java.util.List;
import org.locationtech.jts.geom.Geometry;
import com.ruoyi.bst.area.domain.Area;
import com.ruoyi.bst.area.domain.AreaVO;
import com.ruoyi.bst.area.domain.vo.LocationAreaVO;
import com.ruoyi.bst.areaSub.domain.AreaSubVO;
import com.ruoyi.bst.areaSub.utils.AreaSubUtil;
import com.ruoyi.common.utils.map.GeoUtils;
public class AreaUtil {
// 是否在运营区内
// 是否在运营区内0误差
public static boolean isInArea(Area area, BigDecimal longitude, BigDecimal latitude) {
Geometry geometry = GeoUtils.fromWkt(area.getBoundary());
if (geometry == null) {
return false;
}
return GeoUtils.isInPolygonWithTolerance(longitude, latitude, geometry, area.getOutageDistance());
return GeoUtils.isInPolygonWithTolerance(longitude, latitude, geometry, BigDecimal.ZERO);
}
// 是否在运营区最小内边界
@ -44,4 +48,35 @@ public class AreaUtil {
return GeoUtils.isInPolygonWithTolerance(longitude, latitude, geometry, area.getOutageDistance());
}
public static LocationAreaVO getLocationArea(BigDecimal lon, BigDecimal lat, AreaVO area, List<AreaSubVO> noRidingList) {
if (lon == null || lat == null) {
return null;
}
LocationAreaVO vo = new LocationAreaVO();
// 是否在运营区内
vo.setIsInArea(AreaUtil.isInArea(area, lon, lat));
// 是否在运营区最小内边界
vo.setIsInAreaMin(AreaUtil.isInAreaMin(area, lon, lat));
// 是否在运营区最大外边界
vo.setIsInAreaMax(AreaUtil.isInAreaMax(area, lon, lat));
// 是否在禁行区内
AreaSubVO inNoRidingArea = AreaSubUtil.getInAreaSub(noRidingList, lon, lat, area.getError());
vo.setInNoRidingArea(inNoRidingArea);
vo.setIsInNoRidingArea(inNoRidingArea != null);
// 靠近运营区边界时的播报距离
BigDecimal boundaryDistance = area.getBoundaryDistance();
// 是否在禁行区内
AreaSubVO nearAreaSub = AreaSubUtil.getNearAreaSub(noRidingList, lon, lat, area.getError(), boundaryDistance);
vo.setNearNoRidingArea(nearAreaSub);
vo.setIsNearNoRidingArea(nearAreaSub != null);
return vo;
}
}

View File

@ -47,24 +47,33 @@ public class AreaSubUtil {
}
/**
* 获取靠近的区域
* 获取靠近的区域而非是在区域内
* @param areaSubList 区域列表
* @param longitude 经度
* @param latitude 纬度
* @param error 误差
* @param distance 靠近多少
* @return 靠近的区域
*/
public static AreaSubVO getNearAreaSub(List<AreaSubVO> areaSubList, BigDecimal longitude, BigDecimal latitude, BigDecimal error) {
public static AreaSubVO getNearAreaSub(List<AreaSubVO> areaSubList, BigDecimal longitude, BigDecimal latitude, BigDecimal areaError, BigDecimal distance) {
if (CollectionUtils.isEmptyElement(areaSubList)) {
return null;
}
BigDecimal tolerance = BigDecimal.ZERO; // 误差距离
if (areaError != null) {
tolerance = areaError;
}
for (AreaSubVO area : areaSubList) {
Geometry geometry = GeoUtils.fromWkt(area.getBoundary());
if (geometry == null) {
continue;
}
boolean inCircle = GeoUtils.isInPolygonWithTolerance(longitude, latitude, geometry, error);
if (inCircle) {
if (area.getError() != null) {
tolerance = area.getError();
}
boolean inMax = GeoUtils.isInPolygonWithTolerance(longitude, latitude, geometry, distance); // 在外侧误差范围内
boolean inCircle = GeoUtils.isInPolygonWithTolerance(longitude, latitude, geometry, tolerance); // 在内部
// 不在内部但在外侧误差范围内视为靠近
if (!inCircle && inMax) {
return area;
}
}

View File

@ -230,7 +230,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</insert>
<update id="updateDevice" parameterType="Device">
update bst_device
update bst_device bd
<trim prefix="SET" suffixOverrides=",">
<include refid="updateColumns"/>
</trim>

View File

@ -163,6 +163,7 @@ public class OrderValidatorImpl implements OrderValidator{
ServiceUtil.assertion(!DeviceStatus.canUse().contains(device.getStatus()), "ID为%s的车辆当前状态不可使用", device.getId());
ServiceUtil.assertion(MathUtils.smallerThan(device.getRemainingPower(), device.getAreaUndercharge()), "ID为%s的车辆电量不足%s%%,暂时无法使用", device.getId(), device.getAreaUndercharge());
ServiceUtil.assertion(OrderStatus.PROCESSING.getCode().equals(device.getOrderStatus()), "ID为%s的车辆当前有正在进行的订单无法使用", device.getId());
ServiceUtil.assertion(device.getAreaId() == null, "当前车辆未绑定运营区,无法使用");
// 设备能否在该订单上使用换车
if (order != null) {

View File

@ -154,10 +154,10 @@ public class OrderUtil {
}
// 通过设备定位获取是否在运营区
boolean deviceInArea = AreaUtil.isInArea(area, device.getLongitude(), device.getLatitude()); // 是否在运营区
boolean deviceInArea = AreaUtil.isInAreaMax(area, device.getLongitude(), device.getLatitude()); // 是否在运营区
vo.setDeviceInArea(deviceInArea);
// 通过手机定位获取是否在运营区
boolean mobileInArea = AreaUtil.isInArea(area, lon, lat); // 是否在运营区
boolean mobileInArea = AreaUtil.isInAreaMax(area, lon, lat); // 是否在运营区
vo.setMobileInArea(mobileInArea);
// 是否在运营区
boolean inArea = deviceInArea || mobileInArea;

View File

@ -1,24 +1,23 @@
package com.ruoyi.iot.service.impl;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.TimeUnit;
import com.ruoyi.bst.device.domain.enums.DeviceUnLockType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.bst.area.domain.AreaVO;
import com.ruoyi.bst.area.domain.vo.LocationAreaVO;
import com.ruoyi.bst.area.service.AreaService;
import com.ruoyi.bst.area.utils.AreaUtil;
import com.ruoyi.bst.areaSub.domain.AreaSubVO;
import com.ruoyi.bst.areaSub.service.AreaSubService;
import com.ruoyi.bst.areaSub.utils.AreaSubUtil;
import com.ruoyi.bst.device.domain.DeviceVO;
import com.ruoyi.bst.device.domain.enums.DeviceLockStatus;
import com.ruoyi.bst.device.domain.enums.DeviceStatus;
import com.ruoyi.bst.device.domain.enums.DeviceUnLockType;
import com.ruoyi.bst.device.service.DeviceIotService;
import com.ruoyi.bst.device.service.DeviceService;
import com.ruoyi.bst.device.utils.DeviceUtil;
@ -152,14 +151,17 @@ public class IotReceiveServiceImpl implements IotReceiveService {
boolean isOpen = DeviceLockStatus.OPEN.getCode().equals(device.getLockStatus()); // 是否开锁
boolean isQLocked = DeviceStatus.Q_LOCKED.getCode().equals(device.getStatus()); // 是否强制断电
// 是否在运营区最大外边界
boolean isInAreaMax = AreaUtil.isInAreaMax(area, device.getLongitude(), device.getLatitude());
// 是否在禁行区内
AreaSubVO inAreaSub = AreaSubUtil.getInAreaSub(noRidingList, device.getLongitude(), device.getLatitude(), area.getError());
boolean isInNoRidingArea = inAreaSub != null;
// 是否有正在使用的订单
boolean hasOrder = device.getOrderDeviceId() != null && OrderDeviceStatus.USING.getCode().equals(device.getOrderDeviceStatus());
// 获取车辆位置信息
LocationAreaVO locationArea = AreaUtil.getLocationArea(device.getLongitude(), device.getLatitude(), area, noRidingList);
boolean isInArea = locationArea.getIsInArea() != null && locationArea.getIsInArea();
boolean isInAreaMin = locationArea.getIsInAreaMin() != null && locationArea.getIsInAreaMin();
boolean isInAreaMax = locationArea.getIsInAreaMax() != null && locationArea.getIsInAreaMax();
boolean isInNoRidingArea = locationArea.getIsInNoRidingArea() != null && locationArea.getIsInNoRidingArea();
boolean isNearNoRidingArea = locationArea.getIsNearNoRidingArea() != null && locationArea.getIsNearNoRidingArea();
// 在运营区内并且不在禁行区内并且车辆为强制断电状态为车辆上电
if (isInAreaMax && !isInNoRidingArea && isQLocked && !isOpen && hasOrder) {
deviceIotService.unlock(device, DeviceUnLockType.BACK_AREA, "重新返回运营区上电", false);
@ -176,9 +178,8 @@ public class IotReceiveServiceImpl implements IotReceiveService {
deviceIotService.qLock(device, "超出运营区外边界最大值,强制断电", false);
return 1;
}
boolean isInArea = AreaUtil.isInArea(area, device.getLongitude(), device.getLatitude()); // 是否在运营区内
boolean isInAreaMin = AreaUtil.isInAreaMin(area, device.getLongitude(), device.getLatitude()); // 是否在运营区最小内边界
// 一次警告
log.info("车辆MAC{},定位:{},{}。是否在运营区内:{},是否在运营区最小内边界:{},是否在运营区最大外边界:{}", device.getMac(), device.getLongitude(), device.getLatitude(), isInArea, isInAreaMin, isInAreaMax);
if (isInArea && !isInAreaMin) {
deviceIotService.play(device, IotConstants.PLAY_BOUNDARY_NEAR, "靠近运营区内边界");
return 1;
@ -199,10 +200,7 @@ public class IotReceiveServiceImpl implements IotReceiveService {
}
// 靠近禁行区播报语音警告
BigDecimal boundaryDistance = area.getBoundaryDistance();// 靠近运营区边界时的播报距离
AreaSubVO nearAreaSub = AreaSubUtil.getNearAreaSub(noRidingList, device.getLongitude(), device.getLatitude(), boundaryDistance);
boolean isNearyNoRidingArea = nearAreaSub != null;
if (isNearyNoRidingArea) {
if (isNearNoRidingArea) {
deviceIotService.play(device, IotConstants.PLAY_BOUNDARY_NEAR, "靠近禁行区");
return 1;
}

View File

@ -1,5 +1,6 @@
package com.ruoyi.web.bst;
import java.math.BigDecimal;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
@ -156,4 +157,17 @@ public class AreaController extends BaseController
}
return toAjax(areaService.logicDel(ids));
}
/**
* 获取定位所属区域信息Debug
* @param lon 经度
* @param lat 纬度
* @param areaId 运营区ID
* @return 定位所属区域信息
*/
@PreAuthorize("@ss.hasPermi('bst:area:locationArea')")
@GetMapping("/locationArea")
public AjaxResult locationArea(BigDecimal lon, BigDecimal lat, Long areaId) {
return success(areaService.selectLocationArea(lon, lat, areaId));
}
}