feat(geometry): 添加几何工具类和坐标转换支持

- 新增 GeometryUtil 工具类,提供几何对象创建和操作方法
- 添加 GpsConvertUtil 类,实现坐标系转换功能
- 在 EnterprisesUnit 模型中增加坐标字段- 更新相关参数和 VO 类,支持坐标信息
- 新增 PointTypeHandler 以支持 MyBatis-Plus 对 Point 类型的处理
- 在 FastJson2Config 中注册地理坐标相关的序列化和反序列化器
- 添加 GeometryInnerInterceptor 以支持 MyBatis-Plus 几何操作
This commit is contained in:
luozhun 2024-11-08 11:47:04 +08:00
parent 18f6bd715a
commit 201f2112e8
25 changed files with 1067 additions and 17 deletions

View File

@ -23,6 +23,7 @@
<easyexcel.version>3.3.4</easyexcel.version> <easyexcel.version>3.3.4</easyexcel.version>
<mysql.driver.version>8.0.32</mysql.driver.version> <mysql.driver.version>8.0.32</mysql.driver.version>
<mybatis.plus.version>3.5.7</mybatis.plus.version> <mybatis.plus.version>3.5.7</mybatis.plus.version>
<geotools.version>25.2</geotools.version>
<druid.version>1.2.20</druid.version> <druid.version>1.2.20</druid.version>
<minio.version>8.4.3</minio.version> <minio.version>8.4.3</minio.version>
<okhttp.version>4.8.1</okhttp.version> <okhttp.version>4.8.1</okhttp.version>
@ -190,6 +191,17 @@
<artifactId>mybatis-plus-spring-boot3-starter</artifactId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis.plus.version}</version> <version>${mybatis.plus.version}</version>
</dependency> </dependency>
<!-- 处理地理空间数据的读取、写入、转换、分析以及可视化 geotools-->
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-main</artifactId>
<version>${geotools.version}</version>
</dependency>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-geojson</artifactId>
<version>${geotools.version}</version>
</dependency>
<!-- minio对象存储 https://www.minio.org.cn/ --> <!-- minio对象存储 https://www.minio.org.cn/ -->
<dependency> <dependency>
<groupId>io.minio</groupId> <groupId>io.minio</groupId>

View File

@ -0,0 +1,428 @@
package com.changhu.common.utils;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import com.changhu.common.exception.MessageException;
import lombok.extern.slf4j.Slf4j;
import org.geotools.geojson.geom.GeometryJSON;
import org.geotools.geometry.jts.JTS;
import org.geotools.referencing.CRS;
import org.geotools.referencing.GeodeticCalculator;
import org.locationtech.jts.geom.*;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKBReader;
import org.locationtech.jts.io.WKTReader;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import java.awt.geom.Point2D;
import java.io.IOException;
import java.io.StringReader;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.List;
import java.util.Objects;
/**
* author: luozhun
* desc: GeometryUtil
* createTime: 2023/8/25 18:03
*/
@Slf4j
public class GeometryUtil {
/**
* - WGS 84srid=4326用于全球地理坐标系统的标准参考坐标系
* - 美国地理参考系统srid=4269用于美国本土的地图和地理数据
* - 欧洲地理参考系统srid=4258用于欧洲地图和地理数据
* - 中国国家大地坐标系srid=4490用于中国的地图和地理数据
*/
public static final int SRID = 4326;
public static final String ST_GeomFromText = StrUtil.format("ST_GeomFromText({},{},{})", "?", SRID, "'axis-order=long-lat'");
private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory(new PrecisionModel(), SRID);
private static final GeometryFactory LOCATION_TECH_GEOMETRY_FACTORY = new GeometryFactory(new PrecisionModel(), SRID);
/**
* 空对象因为简历空间索引不能为空 Mysql 不支持创建 Empty 的数据 这里自定义一个类型 标识为null
*/
public static Point emptyPoint() {
return createPoint("POINT(0 0)");
}
public static Polygon emptyPolygon() {
return createPolygon("POLYGON((0 0, 0 0, 0 0, 0 0))");
}
public static LineString emptyLineString() {
return createLineString("LINESTRING(0 0, 0 0)");
}
public static MultiPoint emptyMultiPoint() {
return createMultiPoint("MULTIPOINT((0 0))");
}
public static MultiPolygon emptyMultiPolygon() {
return createMultiPolygon("MULTIPOLYGON(((0 0, 0 0, 0 0, 0 0)))");
}
public static MultiLineString emptyMultiLineString() {
return createMultiLineString("MULTILINESTRING(((0 0, 0 0)))");
}
public static boolean equals(Geometry geometry1, Geometry geometry2) {
return StrUtil.equals(geometry1.toText(), geometry2.toText());
}
//** **\\
/**
* 创建点对象
*/
public static Point createPoint(List<BigDecimal> param) {
if (param == null) {
return null;
}
BigDecimal longitude = param.get(0);
BigDecimal latitude = param.get(1);
if (longitude == null || latitude == null) {
return null;
}
Coordinate coordinate = new Coordinate(longitude.doubleValue(), latitude.doubleValue());
return GEOMETRY_FACTORY.createPoint(coordinate);
}
public static Point createPoint(double longitude, double latitude) {
Coordinate coordinate = new Coordinate(longitude, latitude);
return GEOMETRY_FACTORY.createPoint(coordinate);
}
/**
* 从WKT创建点对象
*
* @param wkt 注WKT类似 POINT(109.013388 32.715519)
*/
public static Point createPoint(String wkt) {
return createGeometryFromWkt(wkt, Point.class);
}
/**
* 从WKT创建点对象
*
* @param bytes 数据库二进制数据
*/
public static Point createPoint(byte[] bytes) {
return createGeometryFromBytes(bytes, Point.class);
}
//** 多边形 **\\
/**
* 创建多边形对象
* <p>
* 后面可能考虑兼容折线类型如果是折线类型那么需要规范首尾结点要相同才能构成 polygon 否则构成 polyline
* 目前暂时没有做这方面的规划用代码兼容了首尾不相等的情况构成polygon
*/
public static Polygon createPolygon(List<List<BigDecimal>> params) {
if (params == null || params.size() < 4) {
throw new MessageException("构成多边形至少需要三个有效坐标,并且保证尾部坐标和首部坐标相同");
}
// 检测首尾是否相等不相等则补充尾坐标
List<BigDecimal> startPoint = params.get(0);
List<BigDecimal> entPoint = params.get(params.size() - 1);
if (!Objects.equals(startPoint.get(0), entPoint.get(0))
|| !Objects.equals(startPoint.get(1), entPoint.get(1))) {
params.add(params.get(0));
}
Coordinate[] coordinates = convertBigDecimalList2CoordinateArray(params);
return GEOMETRY_FACTORY.createPolygon(coordinates);
}
public static Polygon createPolygonWithDouble(List<List<Double>> params) {
if (params == null || params.size() < 4) {
throw new MessageException("构成多边形至少需要三个有效坐标,并且保证尾部坐标和首部坐标相同");
}
// 检测首尾是否相等不相等则补充尾坐标
List<Double> startPoint = params.get(0);
List<Double> entPoint = params.get(params.size() - 1);
if (!Objects.equals(startPoint.get(0), entPoint.get(0))
|| !Objects.equals(startPoint.get(1), entPoint.get(1))) {
params.add(params.get(0));
}
Coordinate[] coordinates = convertDoubleList2CoordinateArray(params);
return GEOMETRY_FACTORY.createPolygon(coordinates);
}
/**
* 从WKT创建多边形对象
*
* @param wkt 注WKT类似POLYGON((20 10, 30 0, 40 10, 30 20, 20 10))
*/
public static Polygon createPolygon(String wkt) {
return createGeometryFromWkt(wkt, Polygon.class);
}
/**
* 创建多边形对象
*
* @param bytes 数据库二进制数据
*/
public static Polygon createPolygon(byte[] bytes) {
return createGeometryFromBytes(bytes, Polygon.class);
}
//** 折线 **\\
/**
* 创建折线对象
*/
public static LineString createLineString(List<List<BigDecimal>> params) {
if (params == null || params.size() < 2) {
throw new MessageException("构成折线至少需要两个有效坐标");
}
Coordinate[] coordinates = convertBigDecimalList2CoordinateArray(params);
return GEOMETRY_FACTORY.createLineString(coordinates);
}
/**
* wkt 创建折线对象
*/
public static LineString createLineString(String wkt) {
return createGeometryFromWkt(wkt, LineString.class);
}
/**
* bytes 创建折线对象
*/
public static LineString createLineString(byte[] bytes) {
return createGeometryFromBytes(bytes, LineString.class);
}
//** 多点 **\\
/**
* 创建多点对象
*/
public static MultiPoint createMultiPoint(List<List<BigDecimal>> multiParam) {
int size = multiParam.size();
Point[] points = new Point[size];
for (int i = 0; i < size; i++) {
points[i] = createPoint(multiParam.get(i));
}
return GEOMETRY_FACTORY.createMultiPoint(points);
}
/**
* wkt 创建多点对象
*/
public static MultiPoint createMultiPoint(String wkt) {
return createGeometryFromWkt(wkt, MultiPoint.class);
}
/**
* bytes 创建多点对象
*/
public static MultiPoint createMultiPoint(byte[] bytes) {
return createGeometryFromBytes(bytes, MultiPoint.class);
}
//** 多折线 **\\
/**
* 创建多折线对象
*/
public static MultiLineString createMultiLineString(List<List<List<BigDecimal>>> multiParams) {
int size = multiParams.size();
LineString[] lineStrings = new LineString[size];
for (int i = 0; i < size; i++) {
lineStrings[i] = createLineString(multiParams.get(i));
}
return GEOMETRY_FACTORY.createMultiLineString(lineStrings);
}
/**
* wkt 创建多折线对象
*/
public static MultiLineString createMultiLineString(String wkt) {
return createGeometryFromWkt(wkt, MultiLineString.class);
}
/**
* wkt 创建多折线对象
*/
public static MultiLineString createMultiLineString(byte[] bytes) {
return createGeometryFromBytes(bytes, MultiLineString.class);
}
//** 多多边形 **\\
/**
* 创建多多边形
*/
public static MultiPolygon createMultiPolygon(List<List<List<BigDecimal>>> multiParams) {
int size = multiParams.size();
Polygon[] polygons = new Polygon[size];
for (int i = 0; i < size; i++) {
polygons[i] = createPolygon(multiParams.get(i));
}
return GEOMETRY_FACTORY.createMultiPolygon(polygons);
}
/**
* wkt 创建多多边形
*/
public static MultiPolygon createMultiPolygon(String wkt) {
return createGeometryFromWkt(wkt, MultiPolygon.class);
}
/**
* bytes 创建多多边形
*/
public static MultiPolygon createMultiPolygon(byte[] bytes) {
return createGeometryFromBytes(bytes, MultiPolygon.class);
}
//** 通用工具 **\\
/**
* bytes 创建空间对象
*
* @param bytes 数据库的二进制数据
*/
public static <T extends Geometry> T createGeometryFromBytes(byte[] bytes, Class<T> clazz) {
if (bytes == null) {
return null;
}
try {
WKBReader wkbReader = new WKBReader(GEOMETRY_FACTORY);
byte[] geomBytes = ByteBuffer.allocate(bytes.length - 4).order(ByteOrder.LITTLE_ENDIAN)
.put(bytes, 4, bytes.length - 4).array();
Geometry geometry = wkbReader.read(geomBytes);
return Convert.convert(clazz, geometry);
} catch (Exception e) {
throw new MessageException("从 Bytes 创建空间对象 {} 时出现错误:{}", clazz.getSimpleName(), e.getMessage());
}
}
/**
* wkt 创建空间对象
*
* @param wkt 类似POLYGON((20 10, 30 0, 40 10, 30 20, 20 10)) POINT(109.013388 32.715519)
* @param clazz 继承
*/
public static <T extends Geometry> T createGeometryFromWkt(String wkt, Class<T> clazz) {
if (wkt == null || StrUtil.isBlank(wkt)) {
return null;
}
try {
WKTReader reader = new WKTReader(GEOMETRY_FACTORY);
Geometry geometry = reader.read(wkt);
return Convert.convert(clazz, geometry);
} catch (ParseException e) {
throw new MessageException("从WKT创建空间对象 {} 时出现错误。wkt{}", clazz.getSimpleName(), wkt);
}
}
/**
* GeoJson 创建空间对象
*/
public static <T extends Geometry> T createGeometryFromGeoJson(String geoJson, Class<T> clazz) {
try {
GeometryJSON geometryJson = new GeometryJSON(20);
Geometry read = geometryJson.read(new StringReader(geoJson));
return createGeometryFromWkt(read.toString(), clazz);
} catch (IOException e) {
log.error("从 GeoJson 创建空间对象出错:{}", e.getMessage());
}
return null;
}
/**
* List<LngLatParam> 转化为 Coordinate[]
*/
private static Coordinate[] convertBigDecimalList2CoordinateArray(List<List<BigDecimal>> params) {
int size = params.size();
Coordinate[] coordinates = new Coordinate[size];
for (int i = 0; i < params.size(); i++) {
List<BigDecimal> param = params.get(i);
Coordinate coordinate = new Coordinate(param.get(0).doubleValue(), param.get(1).doubleValue());
coordinates[i] = coordinate;
}
return coordinates;
}
private static Coordinate[] convertDoubleList2CoordinateArray(List<List<Double>> params) {
int size = params.size();
Coordinate[] coordinates = new Coordinate[size];
for (int i = 0; i < params.size(); i++) {
List<Double> param = params.get(i);
Coordinate coordinate = new Coordinate(param.get(0), param.get(1));
coordinates[i] = coordinate;
}
return coordinates;
}
/**
* 计算面积 平方米
* 28155 .195928.042862
*/
public static BigDecimal getApproximateArea(Geometry geometry) {
try {
String wkt = geometry.toText();
WKTReader wktReader = new WKTReader(LOCATION_TECH_GEOMETRY_FACTORY);
Geometry geom = wktReader.read(wkt);
// WGS84(一般项目中常用的是CSR:84和EPSG:4326)
CoordinateReferenceSystem sourceCRS = CRS.decode("EPSG:4326");
// Pseudo-Mercator(墨卡托投影)
CoordinateReferenceSystem targetCRS = CRS.decode("EPSG:4490");
MathTransform transform = CRS.findMathTransform(sourceCRS, targetCRS, true);
Geometry geometryMercator = JTS.transform(geom, transform);
// 面积周长
return BigDecimal.valueOf(geometryMercator.getArea());
} catch (FactoryException | TransformException | ParseException e) {
log.error("计算面积出错:{}", e.getMessage());
}
return null;
}
/**
* 当前数据库的数据来源都是高德数据源 坐标系为GCJ02 如果来源是其他的地图的需要根据规则转换成GCJ02
* 同理如果需要转换到其他的数据源的坐标系也需要进行转换
*/
public static <T extends Geometry> T convertWgs84ToGcj02(T geometry) {
log.info("原始数据:{}", geometry.toText());
Coordinate[] coordinates = geometry.getCoordinates();
for (Coordinate coordinate : coordinates) {
double lon = coordinate.x;
double lat = coordinate.y;
double[] doubles = GpsConvertUtil.wgs84ToGcj02(lon, lat);
coordinate.x = doubles[0];
coordinate.y = doubles[1];
}
log.info("转后数据:{}", geometry.toText());
return geometry;
}
public static double distance(Point p0, Point p1) {
GeodeticCalculator calculator = new GeodeticCalculator();
calculator.setStartingGeographicPoint(p0.getX(), p0.getY());
calculator.setDestinationGeographicPoint(p1.getX(), p1.getY());
return calculator.getOrthodromicDistance();
}
public static Point2D calculateEndingGlobalCoordinates(double longitude, double latitude, double azi, double dis) {
GeodeticCalculator calculator = new GeodeticCalculator();
calculator.setStartingGeographicPoint(longitude, latitude);
calculator.setDirection(azi, dis);
return calculator.getDestinationGeographicPoint();
}
}

View File

@ -0,0 +1,135 @@
package com.changhu.common.utils;
/**
* author: luozhun
* desc: 坐标系转换
* createTime: 2023/8/25 18:18
*/
public class GpsConvertUtil {
public static final double pi = 3.1415926535897932384626;
public static final double xpi = 3.14159265358979324 * 3000.0 / 180.0;
public static final double a = 6378245.0;
public static final double ee = 0.00669342162296594323;
public static double transformLat(double x, double y) {
double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y
+ 0.2 * Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(y * pi) + 40.0 * Math.sin(y / 3.0 * pi)) * 2.0 / 3.0;
ret += (160.0 * Math.sin(y / 12.0 * pi) + 320 * Math.sin(y * pi / 30.0)) * 2.0 / 3.0;
return ret;
}
public static double transformLon(double x, double y) {
double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1
* Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(x * pi) + 40.0 * Math.sin(x / 3.0 * pi)) * 2.0 / 3.0;
ret += (150.0 * Math.sin(x / 12.0 * pi) + 300.0 * Math.sin(x / 30.0
* pi)) * 2.0 / 3.0;
return ret;
}
public static double[] transform(double lon, double lat) {
if (outOfChina(lon, lat)) {
return new double[]{lon, lat};
}
double dLat = transformLat(lon - 105.0, lat - 35.0);
double dLon = transformLon(lon - 105.0, lat - 35.0);
double radLat = lat / 180.0 * pi;
double magic = Math.sin(radLat);
magic = 1 - ee * magic * magic;
double sqrtMagic = Math.sqrt(magic);
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);
double mgLat = lat + dLat;
double mgLon = lon + dLon;
return new double[]{mgLon, mgLat};
}
public static boolean outOfChina(double lon, double lat) {
if (lon < 72.004 || lon > 137.8347)
return true;
return lat < 0.8293 || lat > 55.8271;
}
/**
* 84 to 火星坐标系 (GCJ-02) World Geodetic System ==> Mars Geodetic System
*/
public static double[] wgs84ToGcj02(double lon, double lat) {
if (outOfChina(lon, lat)) {
return new double[]{lon, lat};
}
double dLat = transformLat(lon - 105.0, lat - 35.0);
double dLon = transformLon(lon - 105.0, lat - 35.0);
double radLat = lat / 180.0 * pi;
double magic = Math.sin(radLat);
magic = 1 - ee * magic * magic;
double sqrtMagic = Math.sqrt(magic);
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);
double mgLat = lat + dLat;
double mgLon = lon + dLon;
return new double[]{mgLon, mgLat};
}
/**
* * 火星坐标系 (GCJ-02) to 84 * * @param lon * @param lat * @return
*/
public static double[] gcj02ToWgs84(double lon, double lat) {
double[] gps = transform(lon, lat);
double longitude = lon * 2 - gps[0];
double latitude = lat * 2 - gps[1];
return new double[]{longitude, latitude};
}
/**
* 火星坐标系 (GCJ-02) 与百度坐标系 (BD-09) 的转换算法 GCJ-02 坐标转换成 BD-09 坐标
*/
public static double[] gcj02ToBd09(double lon, double lat) {
double z = Math.sqrt(lon * lon + lat * lat) + 0.00002 * Math.sin(lat * xpi);
double theta = Math.atan2(lat, lon) + 0.000003 * Math.cos(lon * xpi);
double tempLon = z * Math.cos(theta) + 0.0065;
double tempLat = z * Math.sin(theta) + 0.006;
return new double[]{tempLon, tempLat};
}
/**
* * 火星坐标系 (GCJ-02) 与百度坐标系 (BD-09) 的转换算法 * * BD-09 坐标转换成GCJ-02 坐标 * * @param
* bdlat * @param bdlon * @return
*/
public static double[] bd09ToGcj02(double lon, double lat) {
double x = lon - 0.0065, y = lat - 0.006;
double z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * xpi);
double theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * xpi);
double tempLon = z * Math.cos(theta);
double tempLat = z * Math.sin(theta);
return new double[]{tempLon, tempLat};
}
/**
* 将wgs84转为bd09
*/
public static double[] wgs84ToBd09(double lon, double lat) {
double[] gcj02 = wgs84ToGcj02(lon, lat);
return gcj02ToBd09(gcj02[0], gcj02[1]);
}
public static double[] bd09ToWgs84(double lon, double lat) {
double[] gcj02 = bd09ToGcj02(lon, lat);
double[] wgs84 = gcj02ToWgs84(gcj02[0], gcj02[1]);
// 保留小数点后六位
wgs84[1] = retain6(wgs84[1]);
wgs84[0] = retain6(wgs84[0]);
return wgs84;
}
/**
* 保留小数点后六位
*/
private static double retain6(double num) {
String result = String.format("%.6f", num);
return Double.parseDouble(result);
}
}

View File

@ -11,6 +11,7 @@ import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import org.locationtech.jts.geom.Point;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
@ -73,6 +74,11 @@ public class EnterprisesUnit extends BaseEntity implements Serializable {
*/ */
private String address; private String address;
/**
* 坐标
*/
private Point point;
/** /**
* 联系人 * 联系人
*/ */

View File

@ -7,6 +7,7 @@ import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import org.locationtech.jts.geom.Point;
import java.util.List; import java.util.List;
@ -40,6 +41,9 @@ public class EnterprisesUnitSaveOrUpdateParams {
@Schema(description = "详细地址") @Schema(description = "详细地址")
private String address; private String address;
@Schema(description = "坐标")
private Point point;
@Schema(description = "联系人") @Schema(description = "联系人")
private ContactPersonInfo contactPersonInfo; private ContactPersonInfo contactPersonInfo;

View File

@ -4,6 +4,7 @@ import com.changhu.common.db.enums.EnterprisesUnitType;
import com.changhu.module.management.pojo.model.ContactPersonInfo; import com.changhu.module.management.pojo.model.ContactPersonInfo;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import org.locationtech.jts.geom.Point;
/** /**
* @author 20252 * @author 20252
@ -46,6 +47,9 @@ public class EnterprisesUnitPagerVo {
@Schema(description = "地址") @Schema(description = "地址")
private String address; private String address;
@Schema(description = "坐标")
private Point point;
@Schema(description = "联系方式") @Schema(description = "联系方式")
private ContactPersonInfo contactPersonInfo; private ContactPersonInfo contactPersonInfo;

View File

@ -8,9 +8,18 @@ import com.alibaba.fastjson2.support.spring6.http.converter.FastJsonHttpMessageC
import com.changhu.common.db.BaseEnum; import com.changhu.common.db.BaseEnum;
import com.changhu.common.properties.Fastjson2Properties; import com.changhu.common.properties.Fastjson2Properties;
import com.changhu.support.fastjson2.deserialze.DbEnumDeserializer; import com.changhu.support.fastjson2.deserialze.DbEnumDeserializer;
import com.changhu.support.fastjson2.deserialze.LatitudeDeserializer;
import com.changhu.support.fastjson2.deserialze.LongitudeDeserializer;
import com.changhu.support.fastjson2.deserialze.PointDeserializer;
import com.changhu.support.fastjson2.filter.DesensitizedFilter; import com.changhu.support.fastjson2.filter.DesensitizedFilter;
import com.changhu.support.fastjson2.serializer.DbEnumSerializer; import com.changhu.support.fastjson2.serializer.DbEnumSerializer;
import com.changhu.support.fastjson2.serializer.LatitudeSerializer;
import com.changhu.support.fastjson2.serializer.LongitudeSerializer;
import com.changhu.support.fastjson2.serializer.PointSerializer;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.geotools.measure.Latitude;
import org.geotools.measure.Longitude;
import org.locationtech.jts.geom.Point;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -47,9 +56,17 @@ public class FastJson2Config {
JSON.register(clazz, DbEnumDeserializer.instance); JSON.register(clazz, DbEnumDeserializer.instance);
}); });
JSON.config(JSONWriter.Feature.WriteLongAsString); JSON.register(Latitude.class, LatitudeSerializer.instance);
JSON.register(Longitude.class, LongitudeSerializer.instance);
JSON.register(Point.class, PointSerializer.instance);
//反序列化配置 //反序列化配置
JSON.register(Latitude.class, LatitudeDeserializer.instance);
JSON.register(Longitude.class, LongitudeDeserializer.instance);
JSON.register(Point.class, PointDeserializer.instance);
JSON.config(JSONWriter.Feature.WriteLongAsString);
} }
@Bean @Bean

View File

@ -0,0 +1,31 @@
package com.changhu.support.fastjson2.deserialze;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.reader.ObjectReader;
import org.geotools.measure.Latitude;
import java.lang.reflect.Type;
import java.math.BigDecimal;
/**
* author: luozhun
* desc: LatitudeDeserializer
* createTime: 2023/8/26 16:10
*/
public class LatitudeDeserializer implements ObjectReader<Latitude> {
public static final LatitudeDeserializer instance = new LatitudeDeserializer();
private LatitudeDeserializer() {
}
@Override
public Latitude readObject(JSONReader jsonReader, Type fieldType, Object fieldName, long features) {
//读取为BigDecimal来确保精度和避免浮点数的舍入误差
BigDecimal value = jsonReader.readBigDecimal();
if (value == null) {
return null;
}
return new Latitude(value.doubleValue());
}
}

View File

@ -0,0 +1,31 @@
package com.changhu.support.fastjson2.deserialze;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.reader.ObjectReader;
import org.geotools.measure.Longitude;
import java.lang.reflect.Type;
import java.math.BigDecimal;
/**
* author: luozhun
* desc: LongitudeDeserializer
* createTime: 2023/8/26 16:15
*/
public class LongitudeDeserializer implements ObjectReader<Longitude> {
public static final LongitudeDeserializer instance = new LongitudeDeserializer();
private LongitudeDeserializer() {
}
@Override
public Longitude readObject(JSONReader jsonReader, Type fieldType, Object fieldName, long features) {
//读取为BigDecimal来确保精度和避免浮点数的舍入误差
BigDecimal value = jsonReader.readBigDecimal();
if (value == null) {
return null;
}
return new Longitude(value.doubleValue());
}
}

View File

@ -0,0 +1,35 @@
package com.changhu.support.fastjson2.deserialze;
import cn.hutool.core.collection.CollUtil;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.reader.ObjectReader;
import com.changhu.common.utils.GeometryUtil;
import org.locationtech.jts.geom.Point;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* author: luozhun
* desc: PointDeserializer
* createTime: 2023/8/26 16:28
*/
public class PointDeserializer implements ObjectReader<Point> {
public static final PointDeserializer instance = new PointDeserializer();
private PointDeserializer() {
}
@Override
public Point readObject(JSONReader jsonReader, Type fieldType, Object fieldName, long features) {
List<BigDecimal> list = new ArrayList<>();
jsonReader.readArray(list, BigDecimal.class);
if (CollUtil.isEmpty(list)) {
return null;
}
return GeometryUtil.createPoint(list);
}
}

View File

@ -0,0 +1,30 @@
package com.changhu.support.fastjson2.serializer;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.writer.ObjectWriter;
import org.geotools.measure.Latitude;
import java.lang.reflect.Type;
/**
* author: luozhun
* desc: LatitudeSerializer
* createTime: 2023/8/26 15:44
*/
public class LatitudeSerializer implements ObjectWriter<Latitude> {
public static final LatitudeSerializer instance = new LatitudeSerializer();
private LatitudeSerializer() {
}
@Override
public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
if (object == null) {
jsonWriter.writeNull();
return;
}
jsonWriter.writeDouble(((Latitude) object).degrees());
}
}

View File

@ -0,0 +1,30 @@
package com.changhu.support.fastjson2.serializer;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.writer.ObjectWriter;
import org.geotools.measure.Longitude;
import java.lang.reflect.Type;
/**
* author: luozhun
* desc: LongitudeSerializer
* createTime: 2023/8/26 15:46
*/
public class LongitudeSerializer implements ObjectWriter<Longitude> {
public static final LongitudeSerializer instance = new LongitudeSerializer();
private LongitudeSerializer() {
}
@Override
public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
if (object == null) {
jsonWriter.writeNull();
return;
}
jsonWriter.writeDouble(((Longitude) object).degrees());
}
}

View File

@ -0,0 +1,45 @@
package com.changhu.support.fastjson2.serializer;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.writer.ObjectWriter;
import com.changhu.common.utils.GeometryUtil;
import org.geotools.measure.Latitude;
import org.geotools.measure.Longitude;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Point;
import java.lang.reflect.Type;
import java.util.Arrays;
/**
* author: luozhun
* desc: PointSerializer
* createTime: 2023/8/26 15:59
*/
public class PointSerializer implements ObjectWriter<Point> {
public static final PointSerializer instance = new PointSerializer();
private PointSerializer() {
}
@Override
public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
if (object == null) {
jsonWriter.writeNull();
return;
}
Point point = (Point) object;
if (GeometryUtil.equals(GeometryUtil.emptyPoint(), point)) {
jsonWriter.writeNull();
return;
}
jsonWriter.writeAny(
Arrays.asList(
new Longitude(point.getCoordinate().getOrdinate(Coordinate.X)).degrees(),
new Latitude(point.getCoordinate().getOrdinate(Coordinate.Y)).degrees()
)
);
}
}

View File

@ -9,6 +9,7 @@ import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionIntercepto
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.changhu.support.mybatisplus.interceptor.CustomDataPermissionHandler; import com.changhu.support.mybatisplus.interceptor.CustomDataPermissionHandler;
import com.changhu.support.mybatisplus.interceptor.GeometryInnerInterceptor;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -30,6 +31,8 @@ public class CustomMybatisPlusConfig {
interceptor.addInnerInterceptor(paginationInterceptor(DbType.MYSQL)); interceptor.addInnerInterceptor(paginationInterceptor(DbType.MYSQL));
// sql性能规范 // sql性能规范
// interceptor.addInnerInterceptor(new IllegalSQLInnerInterceptor()); // interceptor.addInnerInterceptor(new IllegalSQLInnerInterceptor());
// 地理位置支持 配合
interceptor.addInnerInterceptor(new GeometryInnerInterceptor());
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
//乐观锁插件 //乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());

View File

@ -0,0 +1,13 @@
package com.changhu.support.mybatisplus.handler.global.geo;
import org.apache.ibatis.type.BaseTypeHandler;
import org.locationtech.jts.geom.Geometry;
/**
* author: luozhun
* desc: GeometryTypeHandler
* createTime: 2023/8/25 17:59
*/
public abstract class AbstractGeometryTypeHandler<T extends Geometry> extends BaseTypeHandler<T> {
}

View File

@ -0,0 +1,55 @@
package com.changhu.support.mybatisplus.handler.global.geo;
import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.changhu.common.utils.GeometryUtil;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.locationtech.jts.geom.Point;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* author: luozhun
* desc: PointTypeHandler
* createTime: 2023/8/26 13:17
*/
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(Point.class)
public class PointTypeHandler extends AbstractGeometryTypeHandler<Point> {
@Override
public void setNonNullParameter(PreparedStatement preparedStatement, int i, Point point, JdbcType jdbcType) throws SQLException {
preparedStatement.setString(i, point.toText());
}
@Override
public Point getNullableResult(ResultSet resultSet, String s) throws SQLException {
return createPoint(resultSet.getString(s), resultSet.getBytes(s));
}
@Override
public Point getNullableResult(ResultSet resultSet, int i) throws SQLException {
return createPoint(resultSet.getString(i), resultSet.getBytes(i));
}
@Override
public Point getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
return createPoint(callableStatement.getString(i), callableStatement.getBytes(i));
}
public Point createPoint(String pointStr, byte[] pointBytes) {
if (JSONUtil.isTypeJSON(pointStr)) {
return GeometryUtil.createGeometryFromGeoJson(pointStr, Point.class);
}
if (StrUtil.startWithIgnoreCase(pointStr, ClassUtil.getClassName(Point.class, true))) {
return GeometryUtil.createGeometryFromWkt(pointStr, Point.class);
}
return GeometryUtil.createPoint(pointBytes);
}
}

View File

@ -0,0 +1,133 @@
package com.changhu.support.mybatisplus.interceptor;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.locationtech.jts.geom.*;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static com.changhu.common.utils.GeometryUtil.ST_GeomFromText;
/**
* @author 20252
* @createTime 2024/6/6 下午4:32
* @desc 注意!!!此拦截器只针对mp提供的基本构造方法管用 可以进行参数对象拦截 如果是xml请自行添加ST_GeomFromText函数
*/
@Slf4j
public class GeometryInnerInterceptor extends JsqlParserSupport implements InnerInterceptor {
public static final List<Class<? extends Geometry>> GEOMETRY_CLASS_LIST = CollUtil.newArrayList(
Point.class,
MultiPoint.class,
Polygon.class,
MultiPolygon.class,
LineString.class,
MultiLineString.class
);
public GeometryInnerInterceptor() {
}
@Override
public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
MappedStatement ms = mpSh.mappedStatement();
//sql类型INSERT UPDATE DELETE
SqlCommandType sct = ms.getSqlCommandType();
//mp参数
Object parameter = mpSh.parameterHandler().getParameterObject();
//参数对象
Object object = null;
//获取参数的属性
switch (sct) {
case UPDATE:
try {
object = BeanUtil.beanToMap(parameter).get(Constants.ENTITY);
} catch (Exception ignored) {
}
break;
case INSERT:
object = parameter;
break;
}
//如果没有参数 就不进行sql修改
if (object == null) {
return;
}
//获取 parameter 对象 中有关 geometry 的属性名对应
List<String> geoFields = this.getGeoFields(object);
//如果没有geometry字段 就不进行sql修改
if (geoFields.isEmpty()) {
return;
}
//获取原始sql
BoundSql boundSql = ms.getBoundSql(parameter);
//UPDATE geo_test SET update_by=?,update_time=?,delete_flag=1 WHERE id=? AND delete_flag=0
String originalSql = StrUtil.removeAllLineBreaks(boundSql.getSql());
switch (sct) {
case UPDATE:
for (String geometryField : geoFields) {
String regex = geometryField + "\\s*=\\s*\\?";
originalSql = ReUtil.replaceAll(originalSql, regex, StrUtil.format("{}={}", geometryField, ST_GeomFromText));
}
break;
case INSERT:
for (String geometryField : geoFields) {
originalSql = this.replaceFindGeo(originalSql, geometryField);
}
break;
}
PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
//修改sql
mpBs.sql(originalSql);
}
private String replaceFindGeo(String originalSql, String geometryField) {
List<String> split = StrUtil.split(originalSql, geometryField);
//找出geo字段在sql中的位置
int geometryFieldIndex = StrUtil.count(split.get(0), ",");
StringBuilder stringBuffer = new StringBuilder();
Matcher matcher = Pattern.compile("\\?").matcher(originalSql);
int count = 0;
while (matcher.find()) {
if (count == geometryFieldIndex) {
matcher.appendReplacement(stringBuffer, ST_GeomFromText);
break;
}
count++;
}
matcher.appendTail(stringBuffer);
return stringBuffer.toString();
}
/**
* 获取对象中的geo字段
*/
private List<String> getGeoFields(Object object) {
return Arrays.stream(ReflectUtil.getFields(object.getClass()))
.filter(field -> GEOMETRY_CLASS_LIST.contains(field.getType()))
.map(Field::getName)
.collect(Collectors.toList());
}
}

View File

@ -18,3 +18,4 @@ VITE_APP_MINIO_BUCKET=police-security-dev
# 高德 # 高德
VITE_APP_GAODE_KEY=f379a3f860a68d7438526275d6a94b05 VITE_APP_GAODE_KEY=f379a3f860a68d7438526275d6a94b05
VITE_APP_GAODE_VERSION=2.0 VITE_APP_GAODE_VERSION=2.0
VITE_APP_SECURITY_JS_CODE=432125a0f8d8cad2dac38b77d6f6728f

View File

@ -18,3 +18,4 @@ VITE_APP_MINIO_BUCKET=police-security
# 高德 # 高德
VITE_APP_GAODE_KEY=f379a3f860a68d7438526275d6a94b05 VITE_APP_GAODE_KEY=f379a3f860a68d7438526275d6a94b05
VITE_APP_GAODE_VERSION=2.0 VITE_APP_GAODE_VERSION=2.0
VITE_APP_SECURITY_JS_CODE=432125a0f8d8cad2dac38b77d6f6728f

View File

@ -10,7 +10,7 @@ import {initMap} from "@/utils/aMapUtil";
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
plugins?: string[], plugins?: string[],
initCallback?: () => void, initCallback?: (map: AMap.Map) => void,
mapOptions?: AMap.MapOptions mapOptions?: AMap.MapOptions
}>(), { }>(), {
plugins: () => { plugins: () => {
@ -36,8 +36,8 @@ defineExpose({
onMounted(() => { onMounted(() => {
initMap(props.plugins).then(AMap => { initMap(props.plugins).then(AMap => {
props.initCallback && props.initCallback()
map.value = new AMap.Map(mapId, props.mapOptions) map.value = new AMap.Map(mapId, props.mapOptions)
props.initCallback && props.initCallback(map.value)
}) })
}) })

View File

@ -76,6 +76,7 @@ export interface EnterprisesUnitPagerVo extends BaseTableRowRecord {
streetName?: string; streetName?: string;
/** 地址 **/ /** 地址 **/
address?: string; address?: string;
point: [number, number]
/** 联系方式 **/ /** 联系方式 **/
contactPersonInfo?: { contactPersonInfo?: {
name: string; name: string;
@ -98,6 +99,7 @@ export interface EnterprisesUnitSaveOrUpdateParams {
administrativeDivisionCodes: string[]; administrativeDivisionCodes: string[];
/** 详细地址 **/ /** 详细地址 **/
address?: string; address?: string;
point?: [number, number]
/** 联系人 **/ /** 联系人 **/
contactPersonInfo?: { contactPersonInfo?: {
name: string; name: string;

View File

@ -4,7 +4,7 @@ type Amap = typeof AMap;
export const initMap = (plugins?: string[]): Promise<Amap> => new Promise((resolve, reject) => { export const initMap = (plugins?: string[]): Promise<Amap> => new Promise((resolve, reject) => {
//@ts-ignore //@ts-ignore
window._AMapSecurityConfig = { window._AMapSecurityConfig = {
securityJsCode: '432125a0f8d8cad2dac38b77d6f6728f' securityJsCode: __APP_ENV.VITE_APP_SECURITY_JS_CODE
} }
AMapLoader.load({ AMapLoader.load({
key: __APP_ENV.VITE_APP_GAODE_KEY, key: __APP_ENV.VITE_APP_GAODE_KEY,

View File

@ -29,6 +29,21 @@ const saveOrUpdateEnterprisesUnit = (params: _FormType, callback: Function) => {
let city = ''; let city = '';
const initMarker = (map: AMap.Map) => {
//添加maker点 设置point
const maker = new AMap.Marker({
position: _formParams.value.point,
draggable: true
})
maker.on("dragend", ({lnglat}) => {
_formParams.value.point = lnglat
})
map.clearMap()
map.add(maker)
map.setFitView()
console.log(123);
}
const _formOptions = ref<FormProMaxItemOptions<_FormType>>({ const _formOptions = ref<FormProMaxItemOptions<_FormType>>({
name: { name: {
type: 'input', type: 'input',
@ -62,7 +77,7 @@ const saveOrUpdateEnterprisesUnit = (params: _FormType, callback: Function) => {
customRender: () => <MapContainer customRender: () => <MapContainer
ref={_mapRef} ref={_mapRef}
style={{width: '100%', height: '300px', position: 'relative'}} style={{width: '100%', height: '300px', position: 'relative'}}
initCallback={() => { initCallback={(map) => {
AMap.plugin(['AMap.AutoComplete'], () => { AMap.plugin(['AMap.AutoComplete'], () => {
//@ts-ignore //@ts-ignore
const auto = new AMap.AutoComplete({ const auto = new AMap.AutoComplete({
@ -76,25 +91,19 @@ const saveOrUpdateEnterprisesUnit = (params: _FormType, callback: Function) => {
message.error('所选点位没有经纬度信息 建议选则附近的手动移动!'); message.error('所选点位没有经纬度信息 建议选则附近的手动移动!');
return return
} }
//添加maker点 设置point _formParams.value.point = e.poi.location
const maker = new AMap.Marker({ initMarker(map)
position: e.poi.location,
draggable: true
})
console.log(e);
maker.on("dragend", (e) => {
console.log(e);
})
_mapRef.value.mapInstance.add(maker)
_mapRef.value.mapInstance.setFitView()
}); });
}) })
if (_formParams.value.point) {
initMarker(map)
}
}} }}
> >
<div style={{position: 'absolute', left: '10px', top: '10px', zIndex: 9999}}> <div style={{position: 'absolute', left: '10px', top: '10px', zIndex: 9999}}>
<input id={'tipinput'} <input id={'tipinput'}
placeholder={'请输入详细地址'} placeholder={'请输入详细地址'}
autocomplete="off"
/> />
</div> </div>
</MapContainer> </MapContainer>
@ -176,6 +185,7 @@ export const showEnterprisesUnit = (policeUnitPagerVo: PoliceUnitPagerVo) => {
type: record.type.value, type: record.type.value,
administrativeDivisionCodes: [record.province, record.city, record.districts, record.street].filter(Boolean), administrativeDivisionCodes: [record.province, record.city, record.districts, record.street].filter(Boolean),
address: record.address, address: record.address,
point: record.point,
contactPersonInfoName: record.contactPersonInfo?.name, contactPersonInfoName: record.contactPersonInfo?.name,
contactPersonInfoTelephone: record.contactPersonInfo?.telephone, contactPersonInfoTelephone: record.contactPersonInfo?.telephone,
remark: record.remark remark: record.remark

View File

@ -129,6 +129,29 @@ const searchFormOptions = ref<TableProps["searchFormOptions"]>({
} }
}) })
const a = {
groupId1: {
itemId1: {
standardId: 123123,
deductionPoints: 2
},
itemId2: {
standardId: 345345,
deductionPoints: 4
}
},
groupId2: {
itemId1: {
standardId: 456456,
deductionPoints: 2
},
itemId2: {
standardId: 567567,
deductionPoints: 4
}
}
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -23,6 +23,7 @@ interface ImportMetaEnv {
// 高德 // 高德
VITE_APP_GAODE_KEY: string VITE_APP_GAODE_KEY: string
VITE_APP_GAODE_VERSION: string VITE_APP_GAODE_VERSION: string
VITE_APP_SECURITY_JS_CODE: string
} }
declare module '*.vue' { declare module '*.vue' {