From 65048fbe89f95bc553c77381cb45cad93df418e3 Mon Sep 17 00:00:00 2001 From: ZZX9599 <536509593@qq.com> Date: Tue, 30 Sep 2025 11:29:25 +0800 Subject: [PATCH] =?UTF-8?q?GDAL=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business/controller/GdalController.java | 2 +- .../controller/IconLibraryController.java | 6 +- .../controller/MilitaryLibraryController.java | 6 +- .../com/yj/earth/common/util/GdalUtil.java | 265 ++++++++++++++---- .../com/yj/earth/common/util/SQLiteUtil.java | 28 +- 5 files changed, 228 insertions(+), 79 deletions(-) diff --git a/src/main/java/com/yj/earth/business/controller/GdalController.java b/src/main/java/com/yj/earth/business/controller/GdalController.java index c3143eb..a936d13 100644 --- a/src/main/java/com/yj/earth/business/controller/GdalController.java +++ b/src/main/java/com/yj/earth/business/controller/GdalController.java @@ -29,7 +29,7 @@ public class GdalController { public void importDataStreamGzip(@Parameter(description = "矢量文件路径", required = true) @RequestParam("path") String path, HttpServletResponse response) throws IOException { // 解析矢量文件、得到JSON数据 - String jsonData = GdalUtil.readVectorToGeoJson(path); + String jsonData = GdalUtil.readVectorToSpecifiedGeoJson(path); // 设置响应头 String filename = URLEncoder.encode("data.gz", StandardCharsets.UTF_8); diff --git a/src/main/java/com/yj/earth/business/controller/IconLibraryController.java b/src/main/java/com/yj/earth/business/controller/IconLibraryController.java index 320be14..c055ad1 100644 --- a/src/main/java/com/yj/earth/business/controller/IconLibraryController.java +++ b/src/main/java/com/yj/earth/business/controller/IconLibraryController.java @@ -77,7 +77,7 @@ public class IconLibraryController { // 校验图标库文件是否已存在 if (iconFile.exists()) { if (iconFile.isDirectory()) { - return ApiResponse.failure("同名目录已存在,无法创建文件:" + iconPath); + return ApiResponse.failure("同名目录已存在、无法创建文件:" + iconPath); } return ApiResponse.failure("图标库文件已存在:" + iconPath); } @@ -391,7 +391,7 @@ public class IconLibraryController { private String getIconLibrary() { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); - queryWrapper.eq(IconLibrary::getIsEnable, 1); // 1=启用,0=未启用 + queryWrapper.eq(IconLibrary::getIsEnable, 1); // 1=启用、0=未启用 IconLibrary library = iconLibraryService.getOne(queryWrapper); return library == null ? null : library.getPath(); } @@ -404,7 +404,7 @@ public class IconLibraryController { iconLibraryService.updateById(library); } - // 检查路径是否已存在(存在则启用,不存在则新增) + // 检查路径是否已存在(存在则启用、不存在则新增) LambdaQueryWrapper pathWrapper = new LambdaQueryWrapper<>(); pathWrapper.eq(IconLibrary::getPath, iconPath); IconLibrary existLibrary = iconLibraryService.getOne(pathWrapper); diff --git a/src/main/java/com/yj/earth/business/controller/MilitaryLibraryController.java b/src/main/java/com/yj/earth/business/controller/MilitaryLibraryController.java index 25dee6f..fc78d6b 100644 --- a/src/main/java/com/yj/earth/business/controller/MilitaryLibraryController.java +++ b/src/main/java/com/yj/earth/business/controller/MilitaryLibraryController.java @@ -77,7 +77,7 @@ public class MilitaryLibraryController { // 校验军标库文件是否已存在 if (militaryFile.exists()) { if (militaryFile.isDirectory()) { - return ApiResponse.failure("同名目录已存在,无法创建文件:" + militaryPath); + return ApiResponse.failure("同名目录已存在、无法创建文件:" + militaryPath); } return ApiResponse.failure("军标库文件已存在:" + militaryPath); } @@ -390,7 +390,7 @@ public class MilitaryLibraryController { private String getMilitaryLibrary() { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); - queryWrapper.eq(MilitaryLibrary::getIsEnable, 1); // 1=启用,0=未启用 + queryWrapper.eq(MilitaryLibrary::getIsEnable, 1); // 1=启用、0=未启用 MilitaryLibrary library = militaryLibraryService.getOne(queryWrapper); return library == null ? null : library.getPath(); } @@ -403,7 +403,7 @@ public class MilitaryLibraryController { militaryLibraryService.updateById(library); } - // 检查路径是否已存在(存在则启用,不存在则新增) + // 检查路径是否已存在(存在则启用、不存在则新增) LambdaQueryWrapper pathWrapper = new LambdaQueryWrapper<>(); pathWrapper.eq(MilitaryLibrary::getPath, militaryPath); MilitaryLibrary existLibrary = militaryLibraryService.getOne(pathWrapper); diff --git a/src/main/java/com/yj/earth/common/util/GdalUtil.java b/src/main/java/com/yj/earth/common/util/GdalUtil.java index 0655640..acb9875 100644 --- a/src/main/java/com/yj/earth/common/util/GdalUtil.java +++ b/src/main/java/com/yj/earth/common/util/GdalUtil.java @@ -2,7 +2,8 @@ package com.yj.earth.common.util; import org.gdal.gdal.gdal; import org.gdal.ogr.*; -import org.gdal.osr.SpatialReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.core.JsonProcessingException; import java.util.ArrayList; import java.util.HashMap; @@ -11,88 +12,236 @@ import java.util.Map; public class GdalUtil { - // 静态初始化 + // 静态初始化ObjectMapper,避免重复创建 + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + // 静态初始化GDAL static { + // 可以考虑设置缓存目录来提升性能 + gdal.SetConfigOption("GDAL_CACHEMAX", "1024"); // 设置1GB缓存 gdal.SetConfigOption("PROJ_LIB", "./lib"); gdal.AllRegister(); } /** - * 读取矢量文件并转换为标准GeoJSON格式 + * 读取矢量文件、将每个图层转换为符合指定格式的 GeoJSON */ - public static String readVectorToGeoJson(String filePath) { - // 打开矢量文件 - DataSource dataSource = ogr.Open(filePath); - if (dataSource == null) { - throw new RuntimeException("无法打开矢量文件: " + filePath); - } - - // 创建GeoJSON根对象 - FeatureCollection - Map geoJson = new HashMap<>(); - geoJson.put("type", "FeatureCollection"); - - // 存储所有要素 - List> features = new ArrayList<>(); - geoJson.put("features", features); - - // 获取所有图层 - int layerCount = dataSource.GetLayerCount(); - for (int i = 0; i < layerCount; i++) { - Layer layer = dataSource.GetLayer(i); - if (layer == null) continue; - - // 获取空间参考信息 - SpatialReference srs = layer.GetSpatialRef(); - if (srs != null && features.isEmpty()) { // 只添加一次CRS信息 - Map crs = new HashMap<>(); - Map crsProps = new HashMap<>(); - crsProps.put("name", srs.GetAttrValue("AUTHORITY", 0) + ":" + srs.GetAttrValue("AUTHORITY", 1)); - crs.put("type", "name"); - crs.put("properties", crsProps); - geoJson.put("crs", crs); + public static String readVectorToSpecifiedGeoJson(String filePath) { + DataSource dataSource = null; + try { + // 打开矢量文件,使用只读模式 + dataSource = ogr.Open(filePath, 0); + if (dataSource == null) { + throw new RuntimeException("无法打开矢量文件: " + filePath + ", 错误信息: " + gdal.GetLastErrorMsg()); } + // 创建结果结构,预先估计容量 + Map result = new HashMap<>(2); + int layerCount = dataSource.GetLayerCount(); + List> layerList = new ArrayList<>(layerCount); + + // 处理每个图层 + for (int i = 0; i < layerCount; i++) { + Layer layer = dataSource.GetLayer(i); + if (layer == null) continue; + + // 为当前图层创建符合格式的 GeoJSON + Map layerGeoJson = convertLayerToSpecifiedFormat(layer); + if (layerGeoJson != null) { + layerList.add(layerGeoJson); + } + // 提前释放图层资源 + layer.delete(); + } + + result.put("list", layerList); + + // 使用单例ObjectMapper转换为JSON字符串 + return OBJECT_MAPPER.writeValueAsString(result); + + } catch (Exception e) { + throw new RuntimeException("处理矢量文件时发生错误: " + e.getMessage() + ", 详细错误: " + gdal.GetLastErrorMsg(), e); + } finally { + // 确保数据源被正确释放 + if (dataSource != null) { + dataSource.delete(); + } + } + } + + /** + * 将单个图层转换为符合指定格式的 GeoJSON + */ + private static Map convertLayerToSpecifiedFormat(Layer layer) { + try { + // 获取要素数量,预分配集合大小 + long featureCount = layer.GetFeatureCount(); + if (featureCount <= 0) { + return null; // 跳过空图层 + } + + // 创建符合格式的 GeoJSON FeatureCollection + Map geoJson = new HashMap<>(3); + geoJson.put("type", "FeatureCollection"); + geoJson.put("name", layer.GetName()); + + // 预分配要素列表容量 + List> features = new ArrayList<>((int) Math.min(featureCount, Integer.MAX_VALUE)); + // 获取字段定义 FeatureDefn featureDefn = layer.GetLayerDefn(); int fieldCount = featureDefn.GetFieldCount(); - // 读取所有要素 + // 缓存字段信息,避免重复获取 + String[] fieldNames = new String[fieldCount]; + int[] fieldTypes = new int[fieldCount]; + for (int j = 0; j < fieldCount; j++) { + FieldDefn fieldDef = featureDefn.GetFieldDefn(j); + fieldNames[j] = fieldDef.GetName(); + fieldTypes[j] = fieldDef.GetFieldType(); + } + + // 读取图层中的所有要素 layer.ResetReading(); Feature feature; + while ((feature = layer.GetNextFeature()) != null) { - // 创建Feature对象 - Map featureObj = new HashMap<>(); - featureObj.put("type", "Feature"); - - // 存储属性信息 - Map properties = new HashMap<>(); - // 可以添加图层名称作为属性 - properties.put("layerName", layer.GetName()); + Map featureMap = new HashMap<>(3); + featureMap.put("type", "Feature"); + // 处理属性 + Map properties = new HashMap<>(fieldCount); for (int j = 0; j < fieldCount; j++) { - String fieldName = featureDefn.GetFieldDefn(j).GetName(); - properties.put(fieldName, feature.GetFieldAsString(j)); + properties.put(fieldNames[j], getFieldValue(feature, j, fieldTypes[j])); } - featureObj.put("properties", properties); + featureMap.put("properties", properties); - // 存储几何信息,转换为GeoJSON格式 + // 处理几何信息 - 直接构建Map而不是先转JSON再解析 Geometry geometry = feature.GetGeometryRef(); - if (geometry != null) { + featureMap.put("geometry", convertGeometryToMap(geometry)); - String geometryJson = geometry.ExportToJson(); - Map geometryObj = JsonUtil.jsonToMap(geometryJson); - featureObj.put("geometry", geometryObj); - } - - features.add(featureObj); - feature.delete(); + features.add(featureMap); + feature.delete(); // 及时释放要素资源 } + + geoJson.put("features", features); + return geoJson; + + } catch (Exception e) { + System.err.println("转换图层 " + layer.GetName() + " 时发生错误: " + e.getMessage() + ", 详细错误: " + gdal.GetLastErrorMsg()); + return null; + } + } + + /** + * 将Geometry直接转换为Map,避免JSON序列化/反序列化开销 + */ + private static Map convertGeometryToMap(Geometry geometry) { + if (geometry == null) { + return createEmptyMultiPolygon(); } - // 关闭数据源 - dataSource.delete(); + try { + String geometryType = geometry.GetGeometryName(); + Map geometryMap = new HashMap<>(2); - // 转换为JSON并返回 - return JsonUtil.toJson(geoJson); + // 如果不是MultiPolygon,尝试转换 + if (!"MULTIPOLYGON".equals(geometryType)) { + // 转换为MultiPolygon + Geometry multiPolygon = geometry.MakeValid(); + if (!"MULTIPOLYGON".equals(multiPolygon.GetGeometryName())) { + multiPolygon = geometry.Buffer(0); // 修复几何问题 + } + if (!"MULTIPOLYGON".equals(multiPolygon.GetGeometryName())) { + // 如果仍然不是MultiPolygon,创建空的 + return createEmptyMultiPolygon(); + } + geometry = multiPolygon; + } + + geometryMap.put("type", "MultiPolygon"); + geometryMap.put("coordinates", getCoordinates(geometry)); + + return geometryMap; + } catch (Exception e) { + return createEmptyMultiPolygon(); + } + } + + /** + * 直接获取几何坐标,避免JSON转换 + */ + private static List getCoordinates(Geometry geometry) { + List coordinates = new ArrayList<>(); + + if (geometry == null) { + return coordinates; + } + + int geometryCount = geometry.GetGeometryCount(); + for (int i = 0; i < geometryCount; i++) { + Geometry polygon = geometry.GetGeometryRef(i); + if (polygon == null) continue; + + List polygonCoords = new ArrayList<>(); + int ringCount = polygon.GetGeometryCount(); + for (int j = 0; j < ringCount; j++) { + Geometry ring = polygon.GetGeometryRef(j); + if (ring == null) continue; + + List> ringCoords = new ArrayList<>(); + int pointCount = ring.GetPointCount(); + for (int k = 0; k < pointCount; k++) { + double[] point = ring.GetPoint(k); + List pointCoords = new ArrayList<>(2); + pointCoords.add(point[0]); + pointCoords.add(point[1]); + ringCoords.add(pointCoords); + } + polygonCoords.add(ringCoords); + } + coordinates.add(polygonCoords); + } + + return coordinates; + } + + /** + * 获取字段值、根据字段类型处理 + */ + private static Object getFieldValue(Feature feature, int fieldIndex, int fieldType) { + try { + switch (fieldType) { + case ogr.OFTInteger: + return feature.GetFieldAsInteger(fieldIndex); + case ogr.OFTInteger64: + return feature.GetFieldAsInteger64(fieldIndex); + case ogr.OFTReal: + return feature.GetFieldAsDouble(fieldIndex); + case ogr.OFTString: + case ogr.OFTStringList: + case ogr.OFTWideString: + return feature.GetFieldAsString(fieldIndex); + case ogr.OFTDate: + case ogr.OFTTime: + case ogr.OFTDateTime: + return feature.GetFieldAsString(fieldIndex); + default: + // 对于其他类型,统一返回字符串表示 + return feature.GetFieldAsString(fieldIndex); + } + } catch (Exception e) { + return feature.GetFieldAsString(fieldIndex); + } + } + + /** + * 创建空的 MultiPolygon 几何对象 + */ + private static Map createEmptyMultiPolygon() { + Map geometry = new HashMap<>(2); + geometry.put("type", "MultiPolygon"); + geometry.put("coordinates", new ArrayList<>()); + return geometry; } } diff --git a/src/main/java/com/yj/earth/common/util/SQLiteUtil.java b/src/main/java/com/yj/earth/common/util/SQLiteUtil.java index 9d1a815..028dd64 100644 --- a/src/main/java/com/yj/earth/common/util/SQLiteUtil.java +++ b/src/main/java/com/yj/earth/common/util/SQLiteUtil.java @@ -27,7 +27,7 @@ public class SQLiteUtil { // 统一日期格式(LocalDateTime) private static final DateTimeFormatter LOCAL_DATE_TIME_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME; - // 连接池缓存:key=数据库文件路径,value=DBCP2数据源(支持多连接复用) + // 连接池缓存:key=数据库文件路径、value=DBCP2数据源(支持多连接复用) private static final Map DATA_SOURCE_POOL = new ConcurrentHashMap<>(); // 字段缓存:缓存类的字段映射(避免反射重复开销) @@ -46,7 +46,7 @@ public class SQLiteUtil { if (dbFilePath == null || dbFilePath.trim().isEmpty()) { throw new IllegalArgumentException("数据库文件路径不能为空"); } - // 不存在则创建数据源,存在则直接从池获取连接 + // 不存在则创建数据源、存在则直接从池获取连接 BasicDataSource dataSource = DATA_SOURCE_POOL.computeIfAbsent(dbFilePath, SQLiteUtil::createDataSource); return dataSource.getConnection(); } @@ -63,7 +63,7 @@ public class SQLiteUtil { dataSource.setDriverClassName("org.sqlite.JDBC"); dataSource.setUrl("jdbc:sqlite:" + dbFilePath); - // 2. 连接池核心参数(根据并发量调整,SQLite不建议过多连接) + // 2. 连接池核心参数(根据并发量调整、SQLite不建议过多连接) dataSource.setMaxTotal(30); // 最大连接数:20-50(根据服务器CPU/内存调整) dataSource.setMaxIdle(15); // 最大空闲连接:保留部分连接避免频繁创建 dataSource.setMinIdle(5); // 最小空闲连接:保证基础并发响应速度 @@ -84,7 +84,7 @@ public class SQLiteUtil { try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement()) { stmt.execute("PRAGMA journal_mode=WAL;"); // 启用WAL模式:支持多读者+单写者(核心优化) - stmt.execute("PRAGMA cache_size=-20000;"); // 页面缓存20MB(负号表示KB单位,内存足可调大) + stmt.execute("PRAGMA cache_size=-20000;"); // 页面缓存20MB(负号表示KB单位、内存足可调大) stmt.execute("PRAGMA synchronous=NORMAL;"); // 同步级别:平衡性能与安全(崩溃最多丢WAL日志) stmt.execute("PRAGMA temp_store=MEMORY;"); // 临时表/索引存内存(减少磁盘IO) stmt.execute("PRAGMA busy_timeout=2000;"); // 忙等待超时:2秒(避免瞬时并发锁等待) @@ -165,7 +165,7 @@ public class SQLiteUtil { throw new IllegalArgumentException("查询SQL不能为空"); } - // 预加载字段映射(缓存生效,避免重复反射) + // 预加载字段映射(缓存生效、避免重复反射) Map fieldMap = getFieldMap(clazz); // try-with-resources:自动关闭Connection、PreparedStatement、ResultSet @@ -175,7 +175,7 @@ public class SQLiteUtil { ResultSetMetaData metaData = rs.getMetaData(); int columnCount = metaData.getColumnCount(); - // 预构建「列名-字段」映射(一次构建,循环复用) + // 预构建「列名-字段」映射(一次构建、循环复用) ColumnFieldMapping[] mappings = buildColumnFieldMappings(metaData, columnCount, fieldMap); // 处理结果集(反射赋值) @@ -254,7 +254,7 @@ public class SQLiteUtil { * * @param dbFilePath 数据库路径 * @param sql DDL语句 - * @param params SQL参数(可选,如动态表名) + * @param params SQL参数(可选、如动态表名) */ public static void executeDDL(String dbFilePath, String sql, List params) throws SQLException { if (dbFilePath == null || dbFilePath.trim().isEmpty()) { @@ -269,7 +269,7 @@ public class SQLiteUtil { pstmt.execute(); } catch (SQLException e) { closeDataSource(dbFilePath); - throw new SQLException("执行DDL失败(SQL:" + sql + ",路径:" + dbFilePath + ")", e); + throw new SQLException("执行DDL失败(SQL:" + sql + "、路径:" + dbFilePath + ")", e); } } @@ -284,7 +284,7 @@ public class SQLiteUtil { // ========================== 工具辅助方法 ========================== /** - * 创建PreparedStatement(复用Connection,避免重复获取) + * 创建PreparedStatement(复用Connection、避免重复获取) * * @param conn 已获取的Connection(从连接池来) * @param sql SQL语句 @@ -362,7 +362,7 @@ public class SQLiteUtil { } /** - * 获取类的字段缓存(含父类字段,一次反射永久缓存) + * 获取类的字段缓存(含父类字段、一次反射永久缓存) * * @param clazz 目标类 * @return 字段名→Field的映射(不可修改) @@ -385,7 +385,7 @@ public class SQLiteUtil { } /** - * 优化的字段赋值(处理类型转换,避免重复异常捕获) + * 优化的字段赋值(处理类型转换、避免重复异常捕获) * * @param obj 目标对象 * @param field 字段 @@ -407,7 +407,7 @@ public class SQLiteUtil { field.set(obj, convertedValue); } } catch (IllegalAccessException e) { - System.err.println("警告:字段赋值失败(字段:" + field.getName() + ",值类型:" + value.getClass().getName() + "):" + e.getMessage()); + System.err.println("警告:字段赋值失败(字段:" + field.getName() + "、值类型:" + value.getClass().getName() + "):" + e.getMessage()); } } @@ -451,7 +451,7 @@ public class SQLiteUtil { } // 不支持的类型转换(打印警告) - System.err.println("警告:不支持的类型转换(目标类型:" + targetType.getName() + ",原始值类型:" + value.getClass().getName() + ")"); + System.err.println("警告:不支持的类型转换(目标类型:" + targetType.getName() + "、原始值类型:" + value.getClass().getName() + ")"); return null; } @@ -547,7 +547,7 @@ public class SQLiteUtil { // ========================== 内部辅助类 ========================== /** - * 列-字段映射模型(一次性构建,减少循环内计算) + * 列-字段映射模型(一次性构建、减少循环内计算) */ private static class ColumnFieldMapping { String columnName; // 数据库列名