ArcGIS Runtime与GeoTools融合实践:加密SHP文件的完整读写方案。目前主要是因为移动端反馈对于加密shp文件,arcgis-java无法实现内存解密后图层展示,因此结合多种技术解决该问题。
1.技术背景
在企业级GIS应用开发中,数据安全始终是重中之重。传统的Shapefile文件由于是明文存储,存在数据泄露风险。本文介绍一套完整的解决方案,通过ArcGIS Runtime与GeoTools的技术融合,实现对SHP文件的加密存储、内存解密、实时编辑和重新加密的全流程安全处理。
2.技术栈
ArcGIS Runtime SDK for Java - 用于GIS数据展示和编辑
GeoTools - 用于Shapefile格式解析
Apache Commons VFS - 用于内存文件系统操作
AES对称加密 - 用于数据安全保护
3.Maven依赖配置
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany</groupId>
<artifactId>app</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>My Map App</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<arcgis.version>100.3.0</arcgis.version>
</properties>
<repositories>
<repository>
<id>arcgis</id>
<url>https://esri.jfrog.io/artifactory/arcgis</url>
</repository>
<repository>
<id>osgeo</id>
<name>OSGeo Release Repository</name>
<url>https://repo.osgeo.org/repository/release/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.53</version>
</dependency>
<!-- GeoTools核心依赖 -->
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-shapefile</artifactId>
<version>25.0</version>
</dependency>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-main</artifactId>
<version>25.0</version>
</dependency>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-geojson</artifactId>
<version>25.0</version>
</dependency>
<!-- JTS几何库 -->
<dependency>
<groupId>org.locationtech.jts</groupId>
<artifactId>jts-core</artifactId>
<version>1.18.2</version>
</dependency>
<!--JavaFX dependencies -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>21.0.5</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-vfs2</artifactId>
<version>2.9.0</version>
</dependency>
<!-- 加密相关 -->
<!-- <dependency>-->
<!-- <groupId>org.bouncycastle</groupId>-->
<!-- <artifactId>bcprov-jdk15on</artifactId>-->
<!-- <version>1.70</version>-->
<!-- </dependency>-->
<!-- 加密相关 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.70</version>
</dependency>
<!--ArcGIS dependencies -->
<dependency>
<groupId>com.esri.arcgisruntime</groupId>
<artifactId>arcgis-java</artifactId>
<version>${arcgis.version}</version>
</dependency>
<dependency>
<groupId>com.esri.arcgisruntime</groupId>
<artifactId>arcgis-java-jnilibs</artifactId>
<version>${arcgis.version}</version>
<type>zip</type>
</dependency>
<dependency>
<groupId>com.esri.arcgisruntime</groupId>
<artifactId>arcgis-java-resources</artifactId>
<version>${arcgis.version}</version>
<type>zip</type>
</dependency>
<!--SLF4J dependencies-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>1.7.36</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.1</version>
<configuration>
<artifactItems>
<artifactItem>
<groupId>com.esri.arcgisruntime</groupId>
<artifactId>arcgis-java-jnilibs</artifactId>
<version>${arcgis.version}</version>
<type>zip</type>
<overWrite>false</overWrite>
<outputDirectory>${user.home}/.arcgis/${arcgis.version}</outputDirectory>
</artifactItem>
<artifactItem>
<groupId>com.esri.arcgisruntime</groupId>
<artifactId>arcgis-java-resources</artifactId>
<version>${arcgis.version}</version>
<type>zip</type>
<overWrite>false</overWrite>
<outputDirectory>${user.home}/.arcgis/${arcgis.version}</outputDirectory>
</artifactItem>
</artifactItems>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<release>1.8</release>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.6.0</version>
<executions>
<execution>
<goals>
<goal>java</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>com.mycompany.app.App</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>io.takari</groupId>
<artifactId>maven</artifactId>
<version>0.7.4</version>
</plugin>
</plugins>
</build>
</project>
4.核心架构设计
本方案采用三层架构:
(1)加密层 - 使用AES对称加密保护磁盘文件
(2)内存处理层 - 在内存中完成解密和格式转换
(3)业务层 - 提供完整的GIS数据编辑能力
加密SHP文件 → 内存解密 → GeoTools解析 → ArcGIS格式 → 业务编辑 → 格式转换 → 重新加密
5.核心代码实现
(1)AES加密工具类
关键特性:
支持128位AES加密
提供文件级加密解密方法
支持字符串密钥和随机密钥生成
ECB模式配合PKCS5Padding填充
package org.example.shp.demo;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.SecureRandom;
/**
* AES对称加密工具类
*/
public class AESEncryptionUtils {
private static final String ALGORITHM = "AES";
private static final String TRANSFORMATION = "AES/ECB/PKCS5Padding";
/**
* 生成AES密钥
*/
public static SecretKey generateKey() throws Exception {
SecureRandom secureRandom = new SecureRandom();
byte[] key = new byte[16]; // 128位
secureRandom.nextBytes(key);
return new SecretKeySpec(key, ALGORITHM);
}
/**
* 从字符串生成固定密钥(使用UTF-8编码)
*/
public static SecretKey generateKey(String password) throws Exception {
// 确保密钥长度为16字节(128位)
byte[] keyBytes = ensureKeyLength(password.getBytes(StandardCharsets.UTF_8), 16);
return new SecretKeySpec(keyBytes, ALGORITHM);
}
/**
* 确保密钥长度符合要求
*/
private static byte[] ensureKeyLength(byte[] key, int requiredLength) {
if (key.length == requiredLength) {
return key;
}
byte[] newKey = new byte[requiredLength];
System.arraycopy(key, 0, newKey, 0, Math.min(key.length, requiredLength));
// 如果原密钥较短,用0填充
if (key.length < requiredLength) {
for (int i = key.length; i < requiredLength; i++) {
newKey[i] = 0;
}
}
return newKey;
}
/**
* 从字节数组生成密钥
*/
public static SecretKey getKeyFromBytes(byte[] keyBytes) {
return new SecretKeySpec(keyBytes, ALGORITHM);
}
/**
* 加密数据
*/
public static byte[] encrypt(byte[] data, SecretKey key) throws Exception {
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, key);
return cipher.doFinal(data);
}
/**
* 解密数据
*/
public static byte[] decrypt(byte[] encryptedData, SecretKey key) throws Exception {
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, key);
return cipher.doFinal(encryptedData);
}
/**
* 处理文件:读取文件内容,加密或解密后写回原文件
* @param filePath 文件路径
* @param key 密钥
* @param encrypt true表示加密,false表示解密
*/
public static void processFile(String filePath, SecretKey key, boolean encrypt) throws Exception {
processFile(filePath, filePath, key, encrypt);
}
/**
* 处理文件:读取输入文件内容,加密或解密后写入输出文件
* @param inputFilePath 输入文件路径
* @param outputFilePath 输出文件路径
* @param key 密钥
* @param encrypt true表示加密,false表示解密
*/
public static void processFile(String inputFilePath, String outputFilePath, SecretKey key, boolean encrypt) throws Exception {
// 读取文件内容
Path inputPath = Paths.get(inputFilePath);
byte[] fileContent = Files.readAllBytes(inputPath);
byte[] processedData;
if (encrypt) {
// 加密文件内容
processedData = encrypt(fileContent, key);
System.out.println("文件已加密: " + inputFilePath);
} else {
// 解密文件内容
processedData = decrypt(fileContent, key);
System.out.println("文件已解密: " + inputFilePath);
}
// 写入处理后的数据到输出文件
Path outputPath = Paths.get(outputFilePath);
Files.write(outputPath, processedData);
System.out.println("结果已保存到: " + outputFilePath);
}
/**
* 测试示例
*/
public static void main(String[] args) {
try {
//加密
test01();
//解密
// test02();
} catch (Exception e) {
e.printStackTrace();
}
}
// 加密
static void test01() throws Exception{
// 生成密钥
SecretKey key = generateKey("123");
String originalFile = "./input/铁厂.shp";
// String originalFile = "./output/test.txt";
// String encryptedFile = "./output/test_encrypted.txt";
// String decryptedFile = "./output/test_decrypted.txt";
// String encryptedFile = "./output/L37.shp";
// String decryptedFile = "./output/L37.shp";
String encryptedFile = "./output/铁厂.shp";
String decryptedFile = "./output/铁厂.shp";
// originalFile = "/Users/carter/Downloads/产业上报GIS/L37.shp";
// String originalFile = "./output/test.txt";
// String encryptedFile = "./output/test_encrypted.txt";
// String decryptedFile = "./output/test_decrypted.txt";
// String encryptedFile = "./output/L37.shp";
// String decryptedFile = "./output/L37.shp";
// encryptedFile = "./output/L37.shp";
// decryptedFile = "./output/L37.shp";
// 创建测试文件
// String testContent = "这是一个测试文件内容,用于测试AES加密解密功能。";
// Files.write(Paths.get(originalFile), testContent.getBytes(StandardCharsets.UTF_8));
// System.out.println("创建测试文件: " + originalFile);
// 加密文件到新文件
processFile(originalFile, encryptedFile, key, true);
// 解密文件到新文件
// processFile(encryptedFile, decryptedFile, key, false);
// 读取解密后的内容验证
String decryptedContent = new String(Files.readAllBytes(Paths.get(decryptedFile)), StandardCharsets.UTF_8);
System.out.println("解密后内容: " + decryptedContent);
// System.out.println("加解密验证: " + testContent.equals(decryptedContent));
}
//解密
static void test02() throws Exception{
// 生成密钥
SecretKey key = generateKey("123");
// String encryptedFile = "./output/L37.shp";
// String decryptedFile = "./output/shapefile/L37.shp";
String encryptedFile = "./output/铁厂.shp";
String decryptedFile = "./output/shapefile/铁厂.shp";
// 解密文件到新文件
processFile(encryptedFile, decryptedFile, key, false);
}
}
(2)内存Shapefile解析器
技术亮点:
使用Apache Commons VFS创建内存文件系统
自动检测和处理中文字符编码
完整的JTS与ArcGIS几何对象转换
批量要素处理提升性能
支持空间参考系统自动识别
package org.example.shp.demo;
import com.alibaba.fastjson2.JSON;
import com.esri.arcgisruntime.concurrent.ListenableFuture;
import com.esri.arcgisruntime.data.*;
import com.esri.arcgisruntime.geometry.*;
import com.esri.arcgisruntime.loadable.LoadStatus;
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.impl.StandardFileSystemManager;
import org.geotools.data.DataStore;
import org.geotools.data.DataStoreFinder;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.data.simple.SimpleFeatureSource;
import org.locationtech.jts.geom.*;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import javax.crypto.SecretKey;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* 使用Apache Commons VFS和GeoTools解析Shapefile字节数组
* 完全内存操作,不生成临时文件
* 支持中文编码自动检测和转换
*/
public class MemoryShapefileParserWithVFS {
private static final SpatialReference DEFAULT_SPATIAL_REFERENCE = SpatialReference.create(4326);
// 常见的中文字符编码
private static final String[] CHINESE_ENCODINGS = {
"GBK", "GB2312", "GB18030", "UTF-8", "ISO-8859-1", "Big5", "Shift_JIS"
};
// ArcGIS系统字段名(这些字段会自动创建,不需要手动添加)
private static final Set<String> SYSTEM_FIELD_NAMES = new HashSet<>(Arrays.asList(
"objectid"
));
/**
* 主方法:使用内存文件系统解析Shapefile
*/
public static FeatureCollectionTable parseShapefileInMemory(byte[] shpBytes, byte[] shxBytes,
byte[] dbfBytes, byte[] prjBytes) throws Exception {
return parseShapefileInMemory(shpBytes, shxBytes, dbfBytes, prjBytes, null);
}
/**
* 主方法:使用内存文件系统解析Shapefile
*/
public static FeatureCollectionTable parseShapefileInMemory(String filePath, String memoryPath, SecretKey key) throws Exception {
return parseShapefileInMemory(filePath, memoryPath, key, null, true);
}
/**
* 主方法:使用内存文件系统解析Shapefile
*/
public static FeatureCollectionTable parseShapefileInMemory(byte[] shpBytes) throws Exception {
return parseShapefileInMemory(shpBytes, null, null, null, null);
}
/**
* 主方法:使用内存文件系统解析Shapefile,支持指定编码
*/
public static FeatureCollectionTable parseShapefileInMemory(String filePath, String memoryPath, SecretKey key, String charset, boolean encrypted) throws Exception {
System.out.println("=== 开始使用内存文件系统解析Shapefile ===");
StandardFileSystemManager fsManager = null;
FileObject memoryDir = null;
DataStore dataStore = null;
try {
// 1. 初始化内存文件系统
System.out.println("步骤1: 初始化内存文件系统...");
fsManager = new StandardFileSystemManager();
fsManager.init();
// 2. 创建内存目录
memoryDir = fsManager.resolveFile("ram://" + memoryPath);
byte[] shpBytes = java.nio.file.Files.readAllBytes(java.nio.file.Paths.get(filePath));
if (encrypted) {
shpBytes = AESEncryptionUtils.decrypt(shpBytes, key);
}
byte[] shxBytes = java.nio.file.Files.readAllBytes(java.nio.file.Paths.get(filePath.replace(".shp", ".shx")));
byte[] dbfBytes = java.nio.file.Files.readAllBytes(java.nio.file.Paths.get(filePath.replace(".shp", ".dbf")));
byte[] prjBytes = null; // 可选
File file = new File(filePath.replace(".shp", ".prj"));
if (file.exists()) {
prjBytes = java.nio.file.Files.readAllBytes(java.nio.file.Paths.get(filePath.replace(".shp", ".prj")));
}
// 3. 将字节数据写入内存文件系统
System.out.println("步骤2: 将字节数据写入内存文件系统...");
writeBytesToMemoryFS(memoryDir, memoryPath + ".shp", shpBytes);
writeBytesToMemoryFS(memoryDir, memoryPath + ".shx", shxBytes);
writeBytesToMemoryFS(memoryDir, memoryPath + ".dbf", dbfBytes);
if (prjBytes != null && prjBytes.length > 0) {
writeBytesToMemoryFS(memoryDir, memoryPath + ".prj", prjBytes);
}
// 4. 创建GeoTools DataStore - 使用VFS URL,支持字符编码
System.out.println("步骤3: 创建GeoTools DataStore...");
dataStore = createVFSDataStore(memoryDir, memoryPath, charset);
// 5. 解析并转换为ArcGIS格式
System.out.println("步骤4: 解析并转换为ArcGIS格式...");
return parseAndConvertToArcGIS(dataStore);
} finally {
// 6. 清理资源
System.out.println("步骤5: 清理资源...");
if (dataStore != null) {
dataStore.dispose();
}
if (memoryDir != null) {
try {
memoryDir.deleteAll();
memoryDir.close();
} catch (Exception e) {
System.err.println("清理内存目录失败: " + e.getMessage());
}
}
if (fsManager != null) {
try {
fsManager.close();
} catch (Exception e) {
System.err.println("关闭文件系统管理器失败: " + e.getMessage());
}
}
}
}
/**
* 主方法:使用内存文件系统解析Shapefile,支持指定编码
*/
public static FeatureCollectionTable parseShapefileInMemory(byte[] shpBytes, byte[] shxBytes,
byte[] dbfBytes, byte[] prjBytes, String charset) throws Exception {
System.out.println("=== 开始使用内存文件系统解析Shapefile ===");
StandardFileSystemManager fsManager = null;
FileObject memoryDir = null;
DataStore dataStore = null;
try {
// 1. 初始化内存文件系统
System.out.println("步骤1: 初始化内存文件系统...");
fsManager = new StandardFileSystemManager();
fsManager.init();
// 2. 创建内存目录
memoryDir = fsManager.resolveFile("ram://shapefile-memory");
// 3. 将字节数据写入内存文件系统
System.out.println("步骤2: 将字节数据写入内存文件系统...");
writeBytesToMemoryFS(memoryDir, "shapefile.shp", shpBytes);
writeBytesToMemoryFS(memoryDir, "shapefile.shx", shxBytes);
writeBytesToMemoryFS(memoryDir, "shapefile.dbf", dbfBytes);
if (prjBytes != null && prjBytes.length > 0) {
writeBytesToMemoryFS(memoryDir, "shapefile.prj", prjBytes);
}
// 4. 创建GeoTools DataStore - 使用VFS URL,支持字符编码
System.out.println("步骤3: 创建GeoTools DataStore...");
dataStore = createVFSDataStore(memoryDir, "shapefile", charset);
// 5. 解析并转换为ArcGIS格式
System.out.println("步骤4: 解析并转换为ArcGIS格式...");
return parseAndConvertToArcGIS(dataStore);
} finally {
// 6. 清理资源
System.out.println("步骤5: 清理资源...");
if (dataStore != null) {
dataStore.dispose();
}
if (memoryDir != null) {
try {
memoryDir.deleteAll();
memoryDir.close();
} catch (Exception e) {
System.err.println("清理内存目录失败: " + e.getMessage());
}
}
if (fsManager != null) {
try {
fsManager.close();
} catch (Exception e) {
System.err.println("关闭文件系统管理器失败: " + e.getMessage());
}
}
}
}
/**
* 将字节数据写入内存文件系统
*/
private static void writeBytesToMemoryFS(FileObject parentDir, String fileName, byte[] data) throws Exception {
if (data == null || data.length == 0) {
System.out.println(" 跳过空文件: " + fileName);
return;
}
FileObject file = parentDir.resolveFile(fileName);
try (OutputStream out = file.getContent().getOutputStream()) {
out.write(data);
out.flush();
}
System.out.println(" 写入文件: " + fileName + " (" + data.length + " bytes)");
}
/**
* 创建支持VFS的DataStore,支持字符编码设置
*/
private static DataStore createVFSDataStore(FileObject memoryDir, String memoryPath, String charset) throws Exception {
try {
// 获取SHP文件的FileObject
FileObject shpFile = memoryDir.resolveFile(memoryPath + ".shp");
// 使用DataStoreFinder来查找合适的DataStore
Map<String, Object> params = new HashMap<>();
params.put("url", shpFile.getURL());
// 设置字符编码
if (charset != null && !charset.trim().isEmpty()) {
params.put("charset", charset);
System.out.println(" 使用指定编码: " + charset);
} else {
// 默认使用UTF-8,这是中文Shapefile最常见的编码
params.put("charset", "UTF-8");
System.out.println(" 使用默认编码: UTF-8");
}
DataStore dataStore = DataStoreFinder.getDataStore(params);
if (dataStore == null) {
throw new RuntimeException("无法创建DataStore,请检查GeoTools配置");
}
System.out.println(" ✓ 创建DataStore成功: " + dataStore.getClass().getSimpleName());
return dataStore;
} catch (Exception e) {
throw new RuntimeException("创建VFS DataStore失败: " + e.getMessage(), e);
}
}
/**
* 检测DBF文件的字符编码
*/
private static String detectDBFCharset(FileObject memoryDir) {
try {
FileObject dbfFile = memoryDir.resolveFile("shapefile.dbf");
if (dbfFile == null || !dbfFile.exists()) {
System.out.println(" DBF文件不存在,跳过编码检测");
return null;
}
byte[] dbfBytes;
try (InputStream in = dbfFile.getContent().getInputStream();
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
dbfBytes = out.toByteArray();
}
// 只读取前1KB进行编码检测(包含字段名和部分数据)
int sampleSize = Math.min(dbfBytes.length, 1024);
byte[] sample = Arrays.copyOf(dbfBytes, sampleSize);
// 尝试常见的中文编码
for (String encoding : CHINESE_ENCODINGS) {
if (isValidEncoding(sample, encoding)) {
return encoding;
}
}
System.out.println(" 无法自动检测编码,将使用默认编码");
return null;
} catch (Exception e) {
System.err.println(" 编码检测失败: " + e.getMessage());
return null;
}
}
/**
* 检查字节数据是否可以用指定编码正确解码
*/
private static boolean isValidEncoding(byte[] data, String encoding) {
try {
String decoded = new String(data, encoding);
// 检查是否包含常见的中文字符
if (containsChineseCharacters(decoded)) {
System.out.println(" 可能编码: " + encoding + " - 包含中文字符");
return true;
}
// 检查是否是可打印字符(非乱码)
if (isPrintableText(decoded)) {
System.out.println(" 可能编码: " + encoding + " - 可读文本");
return true;
}
} catch (Exception e) {
// 编码不支持或转换失败
}
return false;
}
/**
* 检查字符串是否包含中文字符
*/
private static boolean containsChineseCharacters(String text) {
if (text == null || text.isEmpty()) return false;
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
// 中文字符的Unicode范围
if (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS ||
Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS ||
Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION) {
return true;
}
}
return false;
}
/**
* 检查字符串是否主要由可打印字符组成
*/
private static boolean isPrintableText(String text) {
if (text == null || text.isEmpty()) return false;
int printableCount = 0;
int totalCount = Math.min(text.length(), 100); // 只检查前100个字符
for (int i = 0; i < totalCount; i++) {
char c = text.charAt(i);
if (Character.isLetterOrDigit(c) || Character.isWhitespace(c) ||
c == '.' || c == ',' || c == '-' || c == '_') {
printableCount++;
}
}
return (printableCount * 1.0 / totalCount) > 0.8; // 80%以上是可打印字符
}
/**
* 解析并转换为ArcGIS格式
*/
private static FeatureCollectionTable parseAndConvertToArcGIS(DataStore dataStore) throws Exception {
// 1. 获取要素类型信息
System.out.println(" 获取要素类型信息...");
String typeName = dataStore.getTypeNames()[0];
SimpleFeatureType featureType = dataStore.getSchema(typeName);
System.out.println(" 要素类型: " + featureType.getTypeName());
System.out.println(" 几何类型: " + featureType.getGeometryDescriptor().getType().getBinding().getSimpleName());
// 2. 创建ArcGIS字段定义
System.out.println(" 创建ArcGIS字段定义...");
List<Field> arcgisFields = createArcGISFields(featureType);
GeometryType geometryType = convertGeoToolsGeometryType(featureType);
System.out.println(" ArcGIS几何类型: " + geometryType);
System.out.println(" 字段数量: " + arcgisFields.size());
// 3. 创建FeatureCollectionTable
System.out.println(" 创建FeatureCollectionTable...");
SpatialReference spatialReference = parseSpatialReference(dataStore, featureType);
FeatureCollectionTable table = new FeatureCollectionTable(arcgisFields, geometryType, spatialReference);
// 4. 加载表结构
System.out.println(" 加载表结构...");
loadTableStructure(table);
// 5. 读取要素并添加到表
System.out.println(" 读取要素并添加到表...");
int featureCount = readFeaturesAndAddToTable(dataStore, typeName, table);
// 6. 数据同步
System.out.println(" 数据同步...");
forceDataSync(table);
System.out.println("✓ 成功解析 " + featureCount + " 个要素");
return table;
}
/**
* 检查字符串是否只包含英文字母
*/
public static boolean isEnglishText(String input) {
if (input == null || input.isEmpty()) {
return true;
}
// 使用正则表达式验证:仅包含英文字母(大小写)
return input.matches("^[a-zA-Z]*$");
}
/**
* 创建ArcGIS字段定义
*/
private static List<Field> createArcGISFields(SimpleFeatureType featureType) {
List<Field> fields = new ArrayList<>();
Set<String> fieldNames = new HashSet<>();
// 添加要素属性字段
for (AttributeDescriptor descriptor : featureType.getAttributeDescriptors()) {
String fieldName = descriptor.getLocalName();
if (SYSTEM_FIELD_NAMES.contains(fieldName.toLowerCase())) {
continue;
}
// 跳过几何字段
if (descriptor == featureType.getGeometryDescriptor()) {
continue;
}
// 检查字段名是否重复(不区分大小写)
String normalizedFieldName = fieldName.toUpperCase();
if (fieldNames.contains(normalizedFieldName)) {
System.out.println(" ⚠ 跳过重复字段(不区分大小写): " + fieldName);
continue;
}
Field field = convertGeoToolsField(descriptor);
if (field != null) {
fields.add(field);
fieldNames.add(normalizedFieldName);
}
}
System.out.println(" 最终字段列表: " + JSON.toJSONString(fields.stream()
.map(f -> f.getName() + "(" + f.getFieldType() + ")")
.toArray()));
return fields;
}
/**
* 转换GeoTools字段为ArcGIS字段
*/
private static Field convertGeoToolsField(AttributeDescriptor descriptor) {
String fieldName = descriptor.getLocalName();
Class<?> binding = descriptor.getType().getBinding();
try {
// 统一转换为小写字段名以避免大小写问题
String normalizedFieldName = fieldName.toLowerCase();
if (String.class.equals(binding)) {
// 对于字符串字段,增加长度以支持中文
return Field.createString(normalizedFieldName, fieldName, 500);
} else if (Integer.class.equals(binding) || int.class.equals(binding)) {
return Field.createInteger(normalizedFieldName, fieldName);
} else if (Double.class.equals(binding) || double.class.equals(binding)) {
return Field.createDouble(normalizedFieldName, fieldName);
} else if (Float.class.equals(binding) || float.class.equals(binding)) {
return Field.createFloat(normalizedFieldName, fieldName);
} else if (Long.class.equals(binding) || long.class.equals(binding)) {
return Field.createInteger(normalizedFieldName, fieldName);
} else if (Date.class.equals(binding)) {
return Field.createDate(normalizedFieldName, fieldName);
} else if (Boolean.class.equals(binding) || boolean.class.equals(binding)) {
return Field.createString(normalizedFieldName, fieldName, 10);
} else {
System.out.println(" 未知字段类型 " + binding.getSimpleName() + ",使用字符串类型: " + fieldName);
return Field.createString(normalizedFieldName, fieldName, 500);
}
} catch (Exception e) {
System.err.println("转换字段失败: " + fieldName + ", 错误: " + e.getMessage());
return Field.createString(fieldName.toLowerCase(), fieldName, 500);
}
}
/**
* 转换GeoTools几何类型为ArcGIS几何类型
*/
private static GeometryType convertGeoToolsGeometryType(SimpleFeatureType featureType) {
Class<?> geometryClass = featureType.getGeometryDescriptor().getType().getBinding();
String geometryClassName = geometryClass.getSimpleName();
switch (geometryClassName) {
case "Point":
return GeometryType.POINT;
case "MultiPoint":
return GeometryType.MULTIPOINT;
case "LineString":
return GeometryType.POLYLINE;
case "MultiLineString":
return GeometryType.POLYLINE;
case "Polygon":
return GeometryType.POLYGON;
case "MultiPolygon":
return GeometryType.POLYGON;
default:
System.err.println("未知的几何类型: " + geometryClassName);
return GeometryType.UNKNOWN;
}
}
/**
* 解析空间参考
*/
private static SpatialReference parseSpatialReference(DataStore dataStore, SimpleFeatureType featureType) {
try {
CoordinateReferenceSystem crs = featureType.getCoordinateReferenceSystem();
if (crs != null) {
Integer epsgCode = getEPSGCode(crs);
if (epsgCode != null) {
return SpatialReference.create(epsgCode);
} else {
String wkt = crs.toWKT();
return SpatialReference.create(wkt);
}
}
} catch (Exception e) {
System.err.println("解析空间参考失败: " + e.getMessage());
}
return DEFAULT_SPATIAL_REFERENCE;
}
/**
* 获取EPSG代码
*/
private static Integer getEPSGCode(CoordinateReferenceSystem crs) {
try {
String crsWkt = crs.toWKT();
if (crsWkt.contains("EPSG") && crsWkt.contains("\"")) {
int start = crsWkt.indexOf("EPSG") + 5;
int end = crsWkt.indexOf("\"", start);
if (end > start) {
String epsgStr = crsWkt.substring(start, end).trim();
epsgStr = epsgStr.replace(":", "").trim();
return Integer.parseInt(epsgStr);
}
}
} catch (Exception e) {
// 忽略错误
}
return null;
}
/**
* 读取要素并添加到表
*/
private static int readFeaturesAndAddToTable(DataStore dataStore, String typeName, FeatureCollectionTable table) throws Exception {
int featureCount = 0;
SimpleFeatureSource featureSource = null;
SimpleFeatureIterator featureIterator = null;
try {
featureSource = dataStore.getFeatureSource(typeName);
SimpleFeatureCollection featureCollection = featureSource.getFeatures();
featureIterator = featureCollection.features();
System.out.println(" 开始读取要素...");
// 批量添加要素,提高性能
List<Feature> batchFeatures = new ArrayList<>();
final int BATCH_SIZE = 50;
while (featureIterator.hasNext()) {
try {
SimpleFeature simpleFeature = featureIterator.next();
// 创建ArcGIS要素
Feature feature = createArcGISFeature(simpleFeature, table);
if (feature != null) {
batchFeatures.add(feature);
featureCount++;
// 批量添加
if (batchFeatures.size() >= BATCH_SIZE) {
addFeaturesBatch(table, batchFeatures);
batchFeatures.clear();
}
if (featureCount % 100 == 0) {
System.out.println(" 已处理 " + featureCount + " 个要素");
}
}
} catch (Exception e) {
System.err.println(" 处理要素失败: " + e.getMessage());
// 不打印堆栈跟踪,避免日志过多
}
}
// 添加剩余的要素
if (!batchFeatures.isEmpty()) {
addFeaturesBatch(table, batchFeatures);
}
} finally {
if (featureIterator != null) {
featureIterator.close();
}
}
System.out.println(" ✓ 成功读取 " + featureCount + " 个要素");
return featureCount;
}
/**
* 批量添加要素 - 修复返回类型问题
*/
private static void addFeaturesBatch(FeatureCollectionTable table, List<Feature> features) {
if (features.isEmpty()) {
return;
}
try {
ListenableFuture<Void> addFuture = table.addFeaturesAsync(features);
addFuture.get(30, TimeUnit.SECONDS);
System.out.println(" ✓ 批量添加 " + features.size() + " 个要素成功");
} catch (Exception e) {
System.err.println(" ✗ 批量添加要素失败: " + e.getMessage());
// 尝试逐个添加
addFeaturesIndividually(table, features);
}
}
/**
* 逐个添加要素(批量添加失败时的回退方案)
*/
private static void addFeaturesIndividually(FeatureCollectionTable table, List<Feature> features) {
int successCount = 0;
int failCount = 0;
for (Feature feature : features) {
try {
ListenableFuture<Void> singleFuture = table.addFeatureAsync(feature);
singleFuture.get(5, TimeUnit.SECONDS);
successCount++;
} catch (Exception ex) {
System.err.println(" ✗ 单个要素添加失败: " + ex.getMessage());
failCount++;
}
}
System.out.println(" ⚠ 逐个添加结果: 成功 " + successCount + " 个, 失败 " + failCount + " 个");
}
/**
* 创建ArcGIS要素 - 修复字段名大小写问题和数据类型兼容性问题
*/
private static Feature createArcGISFeature(SimpleFeature simpleFeature, FeatureCollectionTable table) {
try {
Feature feature = table.createFeature();
// 转换几何
Geometry jtsGeometry = (Geometry) simpleFeature.getDefaultGeometry();
if (jtsGeometry != null) {
com.esri.arcgisruntime.geometry.Geometry arcgisGeometry =
convertJTSGeometryToArcGIS(jtsGeometry, table.getSpatialReference());
if (arcgisGeometry != null) {
feature.setGeometry(arcgisGeometry);
}
}
// 设置属性 - 修复字段名大小写问题和数据类型兼容性问题
Map<String, Field> fieldMap = createFieldMap(table);
for (AttributeDescriptor descriptor : simpleFeature.getFeatureType().getAttributeDescriptors()) {
String originalFieldName = descriptor.getLocalName();
// 跳过几何字段
if (descriptor == simpleFeature.getFeatureType().getGeometryDescriptor()) {
continue;
}
// 统一使用小写字段名来匹配
String normalizedFieldName = originalFieldName.toLowerCase();
Field field = fieldMap.get(normalizedFieldName);
if (field == null) {
// 如果找不到字段,跳过
continue;
}
Object value = simpleFeature.getAttribute(originalFieldName);
if (value != null) {
// 根据字段类型进行类型转换
Object convertedValue = convertAttributeValue(value, field.getFieldType());
if (convertedValue != null) {
try {
if (SYSTEM_FIELD_NAMES.contains(normalizedFieldName)) {
continue;
}
feature.getAttributes().put(normalizedFieldName, convertedValue);
} catch (Exception e) {
System.err.println(" 设置字段值失败: " + normalizedFieldName + " = " + convertedValue +
" (类型: " + convertedValue.getClass().getSimpleName() + ")");
// 尝试使用字符串类型
try {
feature.getAttributes().put(normalizedFieldName, value.toString());
} catch (Exception e2) {
System.err.println(" 备用方案也失败,跳过字段: " + normalizedFieldName);
}
}
}
} else {
// 处理空值 - 根据字段类型设置适当的空值
Object nullValue = getNullValueForFieldType(field.getFieldType());
try {
feature.getAttributes().put(normalizedFieldName, nullValue);
} catch (Exception e) {
// 忽略空值设置错误
}
}
}
return feature;
} catch (Exception e) {
System.err.println(" 创建ArcGIS要素失败: " + e.getMessage());
return null;
}
}
/**
* 根据字段名判断是否为 OID 字段
*/
private static boolean isOIDFieldByName(String fieldName) {
String lowerFieldName = fieldName.toLowerCase();
return SYSTEM_FIELD_NAMES.contains(lowerFieldName);
}
/**
* 创建字段名到Field对象的映射(使用小写字段名)
*/
private static Map<String, Field> createFieldMap(FeatureCollectionTable table) {
Map<String, Field> fieldMap = new HashMap<>();
for (Field field : table.getFields()) {
fieldMap.put(field.getName().toLowerCase(), field);
}
return fieldMap;
}
/**
* 根据字段类型获取适当的空值
*/
private static Object getNullValueForFieldType(Field.Type fieldType) {
switch (fieldType) {
case INTEGER:
case FLOAT:
case DOUBLE:
return 0;
case TEXT:
return "";
case DATE:
return null; // 日期字段允许null
default:
return null;
}
}
/**
* 转换属性值以确保数据类型兼容
*/
private static Object convertAttributeValue(Object value, Field.Type fieldType) {
if (value == null) {
return getNullValueForFieldType(fieldType);
}
try {
switch (fieldType) {
case INTEGER:
if (value instanceof Number) {
return ((Number) value).intValue();
} else if (value instanceof String) {
try {
return Integer.parseInt((String) value);
} catch (NumberFormatException e) {
// 如果无法转换为整数,尝试转换为double再转int
try {
return (int) Double.parseDouble((String) value);
} catch (NumberFormatException e2) {
return 0; // 默认值
}
}
}
break;
case OID:
return value;
case FLOAT:
if (value instanceof Number) {
return ((Number) value).floatValue();
} else if (value instanceof String) {
try {
return Float.parseFloat((String) value);
} catch (NumberFormatException e) {
return 0.0f; // 默认值
}
}
break;
case DOUBLE:
if (value instanceof Number) {
return ((Number) value).doubleValue();
} else if (value instanceof String) {
try {
return Double.parseDouble((String) value);
} catch (NumberFormatException e) {
return 0.0; // 默认值
}
}
break;
case DATE:
if (value instanceof Date) {
return value;
} else if (value instanceof String) {
// 尝试解析日期字符串
try {
String dateStr = (String) value;
// 简化日期解析逻辑
if (dateStr.matches("\\d{4}-\\d{2}-\\d{2}")) {
return java.sql.Date.valueOf(dateStr);
}
return new Date(); // 返回当前日期作为默认值
} catch (Exception e) {
return new Date(); // 返回当前日期作为默认值
}
}
break;
case TEXT:
default:
// 对于字符串,确保正确显示中文
String stringValue = value.toString().trim();
// 限制字符串长度,避免过长
if (stringValue.length() > 500) {
stringValue = stringValue.substring(0, 500);
}
return stringValue;
}
} catch (Exception e) {
System.err.println(" 属性值转换失败: " + value + " -> " + fieldType + ", 错误: " + e.getMessage());
}
// 默认返回字符串表示
return value.toString();
}
/**
* 检查字符串是否可能是编码错误的中文
*/
private static boolean isLikelyMisencodedChinese(String text) {
if (text == null || text.isEmpty()) return false;
// 检查是否包含典型的乱码模式(如连续的"å"、"ä"等字符)
if (text.matches(".*[åäöüÅÄÖÜ].*")) {
return true;
}
// 检查是否包含不可打印字符
for (int i = 0; i < Math.min(text.length(), 50); i++) {
char c = text.charAt(i);
if (c < 32 && c != 9 && c != 10 && c != 13) { // 排除制表符、换行符等
return true;
}
}
return false;
}
/**
* 尝试修复中文编码问题
*/
private static String tryFixChineseEncoding(String text) {
if (text == null || text.isEmpty()) return text;
// 尝试从UTF-8解码(如果被错误地以ISO-8859-1读取)
try {
byte[] bytes = text.getBytes("ISO-8859-1");
String utf8Decoded = new String(bytes, "UTF-8");
if (containsChineseCharacters(utf8Decoded) && isPrintableText(utf8Decoded)) {
return utf8Decoded;
}
} catch (Exception e) {
// 忽略
}
// 尝试从GBK解码
try {
byte[] bytes = text.getBytes("ISO-8859-1");
String gbkDecoded = new String(bytes, "GBK");
if (containsChineseCharacters(gbkDecoded) && isPrintableText(gbkDecoded)) {
return gbkDecoded;
}
} catch (Exception e) {
// 忽略
}
// 如果都无法修复,返回原文本
return text;
}
/**
* 转换JTS几何为ArcGIS几何
*/
private static com.esri.arcgisruntime.geometry.Geometry convertJTSGeometryToArcGIS(
Geometry jtsGeometry, SpatialReference spatialReference) {
if (jtsGeometry == null) {
return null;
}
SpatialReference sr = spatialReference != null ? spatialReference : DEFAULT_SPATIAL_REFERENCE;
try {
if (jtsGeometry instanceof Point) {
Point jtsPoint = (Point) jtsGeometry;
return new com.esri.arcgisruntime.geometry.Point(jtsPoint.getX(), jtsPoint.getY(), sr);
} else if (jtsGeometry instanceof LineString) {
LineString jtsLine = (LineString) jtsGeometry;
PointCollection points = new PointCollection(sr);
Coordinate[] coords = jtsLine.getCoordinates();
for (Coordinate coord : coords) {
points.add(new com.esri.arcgisruntime.geometry.Point(coord.x, coord.y));
}
Part part = new Part(points);
PartCollection partCollection = new PartCollection(sr);
partCollection.add(part);
return new Polyline(partCollection);
} else if (jtsGeometry instanceof Polygon) {
Polygon jtsPolygon = (Polygon) jtsGeometry;
PartCollection partCollection = new PartCollection(sr);
// 处理外环
LineString exteriorRing = jtsPolygon.getExteriorRing();
PointCollection exteriorPoints = new PointCollection(sr);
Coordinate[] exteriorCoords = exteriorRing.getCoordinates();
for (Coordinate coord : exteriorCoords) {
exteriorPoints.add(new com.esri.arcgisruntime.geometry.Point(coord.x, coord.y));
}
Part exteriorPart = new Part(exteriorPoints);
partCollection.add(exteriorPart);
// 处理内环
for (int i = 0; i < jtsPolygon.getNumInteriorRing(); i++) {
LineString interiorRing = jtsPolygon.getInteriorRingN(i);
PointCollection interiorPoints = new PointCollection(sr);
Coordinate[] interiorCoords = interiorRing.getCoordinates();
for (Coordinate coord : interiorCoords) {
interiorPoints.add(new com.esri.arcgisruntime.geometry.Point(coord.x, coord.y));
}
Part interiorPart = new Part(interiorPoints);
partCollection.add(interiorPart);
}
return new com.esri.arcgisruntime.geometry.Polygon(partCollection);
} else if (jtsGeometry instanceof MultiPoint) {
MultiPoint jtsMultiPoint = (MultiPoint) jtsGeometry;
PointCollection points = new PointCollection(sr);
for (int i = 0; i < jtsMultiPoint.getNumGeometries(); i++) {
Point jtsPoint = (Point) jtsMultiPoint.getGeometryN(i);
points.add(new com.esri.arcgisruntime.geometry.Point(jtsPoint.getX(), jtsPoint.getY()));
}
return new Multipoint(points);
} else if (jtsGeometry instanceof MultiLineString) {
MultiLineString jtsMultiLine = (MultiLineString) jtsGeometry;
PartCollection partCollection = new PartCollection(sr);
for (int i = 0; i < jtsMultiLine.getNumGeometries(); i++) {
LineString jtsLine = (LineString) jtsMultiLine.getGeometryN(i);
PointCollection points = new PointCollection(sr);
Coordinate[] coords = jtsLine.getCoordinates();
for (Coordinate coord : coords) {
points.add(new com.esri.arcgisruntime.geometry.Point(coord.x, coord.y));
}
Part part = new Part(points);
partCollection.add(part);
}
return new Polyline(partCollection);
} else if (jtsGeometry instanceof MultiPolygon) {
MultiPolygon jtsMultiPolygon = (MultiPolygon) jtsGeometry;
PartCollection partCollection = new PartCollection(sr);
for (int i = 0; i < jtsMultiPolygon.getNumGeometries(); i++) {
Polygon jtsPolygon = (Polygon) jtsMultiPolygon.getGeometryN(i);
// 处理外环
LineString exteriorRing = jtsPolygon.getExteriorRing();
PointCollection exteriorPoints = new PointCollection(sr);
Coordinate[] exteriorCoords = exteriorRing.getCoordinates();
for (Coordinate coord : exteriorCoords) {
exteriorPoints.add(new com.esri.arcgisruntime.geometry.Point(coord.x, coord.y));
}
Part exteriorPart = new Part(exteriorPoints);
partCollection.add(exteriorPart);
// 处理内环
for (int j = 0; j < jtsPolygon.getNumInteriorRing(); j++) {
LineString interiorRing = jtsPolygon.getInteriorRingN(j);
PointCollection interiorPoints = new PointCollection(sr);
Coordinate[] interiorCoords = interiorRing.getCoordinates();
for (Coordinate coord : interiorCoords) {
interiorPoints.add(new com.esri.arcgisruntime.geometry.Point(coord.x, coord.y));
}
Part interiorPart = new Part(interiorPoints);
partCollection.add(interiorPart);
}
}
return new com.esri.arcgisruntime.geometry.Polygon(partCollection);
} else {
System.err.println("不支持的几何类型: " + jtsGeometry.getGeometryType());
return null;
}
} catch (Exception e) {
System.err.println("几何转换失败: " + e.getMessage());
return null;
}
}
/**
* 加载表结构
*/
private static void loadTableStructure(FeatureTable table) throws Exception {
System.out.println(" 开始加载表结构...");
if (table.getLoadStatus() == LoadStatus.LOADED) {
System.out.println(" ✓ 表已加载");
return;
}
final java.util.concurrent.CompletableFuture<Boolean> loadFuture = new java.util.concurrent.CompletableFuture<>();
table.addDoneLoadingListener(() -> {
LoadStatus status = table.getLoadStatus();
System.out.println(" 表加载状态: " + status);
if (status == LoadStatus.LOADED) {
loadFuture.complete(true);
} else {
Throwable error = table.getLoadError();
String errorMsg = error != null ? error.getMessage() : "未知错误";
System.err.println(" ✗ 表加载失败: " + errorMsg);
loadFuture.complete(false);
}
});
table.loadAsync();
try {
loadFuture.get(30, TimeUnit.SECONDS);
} catch (java.util.concurrent.TimeoutException e) {
throw new RuntimeException("表加载超时", e);
}
}
/**
* 强制数据同步
*/
private static void forceDataSync(FeatureTable table) throws Exception {
System.out.println(" 强制数据同步...");
for (int i = 0; i < 3; i++) {
Thread.sleep(1000);
System.gc();
System.out.println(" 同步等待 " + (i + 1) + "/3, 当前要素数量: " + table.getTotalFeatureCount());
}
if (table.getLoadStatus() != LoadStatus.LOADED) {
System.out.println(" 表状态异常,尝试重新加载...");
table.cancelLoad();
loadTableStructure(table);
}
System.out.println(" ✓ 数据同步完成,当前要素数量: " + table.getTotalFeatureCount());
}
// ========== 使用示例 ==========
public static void main(String[] args) {
testInputPath();
}
static void testInputPath() {
try {
System.out.println("=== 内存文件系统Shapefile解析示例 ===");
SecretKey key = AESEncryptionUtils.generateKey("123");
// 方法1:使用自动编码检测
FeatureCollectionTable table = parseShapefileInMemory("./output/铁厂.shp", "tichang", key);
// 查询并打印要素
queryAndPrintFeatures(table, 5);
System.out.println("✓ 解析完成");
System.out.println("================================");
// 转换为SHP文件并保存到指定目录
String outputDir = "./output/shapefile";
String baseName = "铁厂";
FeatureTableToShapefileConverter.convertToShapefile(table, outputDir, baseName, key, false);
} catch (Exception e) {
System.err.println("处理失败: " + e.getMessage());
e.printStackTrace();
}
}
static void testInputByte() {
try {
System.out.println("=== 内存文件系统Shapefile解析示例 ===");
byte[] shpBytes = java.nio.file.Files.readAllBytes(java.nio.file.Paths.get("./output/铁厂.shp"));
byte[] shxBytes = java.nio.file.Files.readAllBytes(java.nio.file.Paths.get("./output/铁厂.shx"));
byte[] dbfBytes = java.nio.file.Files.readAllBytes(java.nio.file.Paths.get("./output/铁厂.dbf"));
byte[] prjBytes = null; // 可选
SecretKey key = AESEncryptionUtils.generateKey("123");
shpBytes = AESEncryptionUtils.decrypt(shpBytes, key);
System.out.println("文件大小:");
System.out.println(" SHP: " + shpBytes.length + " bytes");
System.out.println(" SHX: " + (shxBytes != null ? shxBytes.length : 0) + " bytes");
System.out.println(" DBF: " + (dbfBytes != null ? dbfBytes.length : 0) + " bytes");
// 方法1:使用自动编码检测
FeatureCollectionTable table = parseShapefileInMemory(shpBytes, shxBytes, dbfBytes, prjBytes);
// 查询并打印要素
queryAndPrintFeatures(table, 5);
System.out.println("✓ 解析完成");
System.out.println("================================");
// 转换为SHP文件并保存到指定目录
String outputDir = "./output/shapefile";
String baseName = "铁厂";
FeatureTableToShapefileConverter.convertToShapefile(table, outputDir, baseName, key, true);
} catch (Exception e) {
System.err.println("处理失败: " + e.getMessage());
e.printStackTrace();
}
}
/**
* 查询并打印要素
*/
public static void queryAndPrintFeatures(FeatureCollectionTable table, int maxFeatures) {
System.out.println("\n=== 开始查询要素 ===");
System.out.println("表状态检查:");
System.out.println(" 加载状态: " + table.getLoadStatus());
System.out.println(" 总要素数: " + table.getTotalFeatureCount());
System.out.println(" 字段数量: " + table.getFields().size());
if (table.getLoadStatus() != LoadStatus.LOADED) {
System.err.println(" 表未加载,无法查询");
return;
}
if (table.getTotalFeatureCount() == 0) {
System.err.println(" 表中没有要素");
return;
}
try {
QueryParameters queryParams = new QueryParameters();
queryParams.setWhereClause("1=1");
queryParams.setMaxFeatures(maxFeatures);
ListenableFuture<FeatureQueryResult> future = table.queryFeaturesAsync(queryParams);
FeatureQueryResult result = future.get(30, TimeUnit.SECONDS);
int count = 0;
for (Feature feature : result) {
count++;
printFeatureInfo(feature, count);
if (count >= maxFeatures) break;
}
if (count == 0) {
System.out.println(" 查询未返回任何要素");
} else {
System.out.println(" ✓ 查询返回 " + count + " 个要素");
}
} catch (Exception e) {
System.err.println(" 查询失败: " + e.getMessage());
e.printStackTrace();
}
}
/**
* 打印要素信息
*/
private static void printFeatureInfo(Feature feature, int index) {
System.out.println(" 要素 " + index + ":");
com.esri.arcgisruntime.geometry.Geometry geometry = feature.getGeometry();
if (geometry != null) {
System.out.println(" 几何类型: " + geometry.getGeometryType());
if (geometry instanceof com.esri.arcgisruntime.geometry.Point) {
com.esri.arcgisruntime.geometry.Point point = (com.esri.arcgisruntime.geometry.Point) geometry;
System.out.println(" 坐标: (" + point.getX() + ", " + point.getY() + ")");
} else if (geometry instanceof Polyline) {
Polyline line = (Polyline) geometry;
System.out.println(" 折线点数: " + line.getParts().get(0).getPointCount());
} else if (geometry instanceof com.esri.arcgisruntime.geometry.Polygon) {
com.esri.arcgisruntime.geometry.Polygon polygon = (com.esri.arcgisruntime.geometry.Polygon) geometry;
System.out.println(" 多边形点数: " + polygon.getParts().get(0).getPointCount());
}
} else {
System.out.println(" 几何: 无");
}
Map<String, Object> attributes = feature.getAttributes();
if (attributes != null && !attributes.isEmpty()) {
System.out.println(" 属性列表:");
for (Map.Entry<String, Object> entry : attributes.entrySet()) {
System.out.println(" " + entry.getKey() + ": " + entry.getValue() + " (" +
(entry.getValue() != null ? entry.getValue().getClass().getSimpleName() : "null") + ")");
}
} else {
System.out.println(" 属性: 无");
}
System.out.println();
}
}
(3)FeatureTable到Shapefile转换器
核心功能:
原生Shapefile二进制格式写入
支持点、线、面多种几何类型
多边形闭合自动修复
加密输出支持
package org.example.shp.demo;
import com.esri.arcgisruntime.concurrent.ListenableFuture;
import com.esri.arcgisruntime.data.Feature;
import com.esri.arcgisruntime.data.FeatureCollectionTable;
import com.esri.arcgisruntime.data.Field;
import com.esri.arcgisruntime.geometry.*;
import com.esri.arcgisruntime.loadable.LoadStatus;
import javax.crypto.SecretKey;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* FeatureCollectionTable直接转换为SHP文件的工具类
* 修复多边形几何显示问题
*/
public class FeatureTableToShapefileConverter {
// Shapefile常量定义
private static final int SHP_FILE_CODE = 9994;
private static final int SHP_VERSION = 1000;
private static final int SHAPE_TYPE_POINT = 1;
private static final int SHAPE_TYPE_POLYLINE = 3;
private static final int SHAPE_TYPE_POLYGON = 5;
private static final int SHAPE_TYPE_MULTIPOINT = 8;
private static final int SHAPE_TYPE_NULL = 0;
// DBF常量定义
private static final byte DBF_VERSION = 0x03;
private static final byte DBF_HEADER_TERMINATOR = 0x0D;
private static final byte DBF_FIELD_TERMINATOR = 0x00;
/**
* 将FeatureCollectionTable转换为SHP文件并保存到指定目录
*/
public static void convertToShapefile(FeatureCollectionTable featureTable, String outputDir, String baseName, SecretKey key, boolean encrypt) throws Exception {
System.out.println("=== 开始将FeatureCollectionTable转换为SHP文件 ===");
// 验证输入
validateFeatureTable(featureTable);
// 确保输出目录存在
Path outputPath = Paths.get(outputDir);
if (!Files.exists(outputPath)) {
Files.createDirectories(outputPath);
}
// 查询所有要素
List<Feature> features = queryAllFeatures(featureTable);
System.out.println(" 处理 " + features.size() + " 个要素");
// 在内存中生成Shapefile各个组件
byte[] shpBytes = generateShpFile(featureTable, features);
// byte[] shxBytes = generateShxFile(featureTable, features);
// byte[] dbfBytes = generateDbfFile(featureTable, features);
// byte[] prjBytes = generatePrjFile(featureTable);
// byte[] cpgBytes = generateCpgFile();
if (encrypt) {
shpBytes = AESEncryptionUtils.encrypt(shpBytes, key);
}
// 分别保存各个文件
saveToFile(shpBytes, outputDir, baseName + ".shp");
// saveToFile(shxBytes, outputDir, baseName + ".shx");
// saveToFile(dbfBytes, outputDir, baseName + ".dbf");
// if (prjBytes != null && prjBytes.length > 0) {
// saveToFile(prjBytes, outputDir, baseName + ".prj");
// }
//
// saveToFile(cpgBytes, outputDir, baseName + ".cpg");
System.out.println("✓ 转换完成,文件已保存到: " + outputDir);
}
/**
* 将字节数组保存到文件
*/
private static void saveToFile(byte[] data, String outputDir, String fileName) throws IOException {
Path filePath = Paths.get(outputDir, fileName);
try (FileOutputStream fos = new FileOutputStream(filePath.toFile())) {
fos.write(data);
}
System.out.println(" ✓ 保存文件: " + fileName + " (" + data.length + " bytes)");
}
/**
* 验证FeatureTable
*/
private static void validateFeatureTable(FeatureCollectionTable featureTable) {
if (featureTable == null) {
throw new IllegalArgumentException("FeatureCollectionTable不能为空");
}
if (featureTable.getLoadStatus() != LoadStatus.LOADED) {
throw new IllegalStateException("FeatureTable未加载完成");
}
if (featureTable.getGeometryType() == null) {
throw new IllegalStateException("FeatureTable没有几何类型信息");
}
}
/**
* 查询所有要素 - 修复查询方法
*/
private static List<Feature> queryAllFeatures(FeatureCollectionTable featureTable) throws Exception {
List<Feature> features = new ArrayList<>();
try {
// 使用迭代器遍历所有要素
Iterator<Feature> iterator = featureTable.iterator();
while (iterator.hasNext()) {
Feature feature = iterator.next();
features.add(feature);
}
System.out.println(" 成功查询到 " + features.size() + " 个要素");
} catch (Exception e) {
throw new Exception("查询要素失败: " + e.getMessage(), e);
}
return features;
}
/**
* 生成SHP文件字节数组 - 完全重写,修复多边形问题
*/
private static byte[] generateShpFile(FeatureCollectionTable featureTable, List<Feature> features) throws Exception {
System.out.println(" 生成SHP文件...");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
try {
// 1. 计算文件总长度
int fileLength = 50; // 文件头长度(100字节 = 50 words)
// 存储每个记录的内容和长度
List<byte[]> recordContents = new ArrayList<>();
List<Integer> recordLengths = new ArrayList<>();
for (Feature feature : features) {
byte[] recordContent = generateShpRecordContent(feature);
int contentLength = recordContent.length / 2; // 转换为16位字
int recordLength = 4 + contentLength; // 记录头4 words + 内容
recordContents.add(recordContent);
recordLengths.add(recordLength);
fileLength += recordLength;
}
// 2. 写入文件头
writeShpHeader(dos, featureTable, features, fileLength);
// 3. 写入所有记录
for (int i = 0; i < features.size(); i++) {
writeShpRecord(dos, recordContents.get(i), i + 1);
}
dos.flush();
byte[] shpBytes = baos.toByteArray();
System.out.println(" ✓ SHP文件生成完成: " + shpBytes.length + " bytes, 包含 " + features.size() + " 个要素");
return shpBytes;
} finally {
dos.close();
baos.close();
}
}
/**
* 生成SHP记录内容
*/
private static byte[] generateShpRecordContent(Feature feature) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
Geometry geometry = feature.getGeometry();
if (geometry == null) {
writeLittleEndianInt(dos, SHAPE_TYPE_NULL);
} else if (geometry instanceof Point) {
writePointRecord(dos, (Point) geometry);
} else if (geometry instanceof Polyline) {
writePolylineRecord(dos, (Polyline) geometry);
} else if (geometry instanceof Polygon) {
writePolygonRecord(dos, (Polygon) geometry);
} else if (geometry instanceof Multipoint) {
writeMultipointRecord(dos, (Multipoint) geometry);
} else {
writeLittleEndianInt(dos, SHAPE_TYPE_NULL);
}
dos.flush();
return baos.toByteArray();
}
/**
* 写入SHP文件头
*/
private static void writeShpHeader(DataOutputStream dos, FeatureCollectionTable featureTable,
List<Feature> features, int fileLength) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(100);
buffer.order(ByteOrder.BIG_ENDIAN);
// 文件码
buffer.putInt(SHP_FILE_CODE);
// 5个未使用字段
for (int i = 0; i < 5; i++) {
buffer.putInt(0);
}
// 文件长度
buffer.putInt(fileLength);
// 切换到小端序
buffer.order(ByteOrder.LITTLE_ENDIAN);
// 版本
buffer.putInt(SHP_VERSION);
// 形状类型
int shapeType = getShapeType(featureTable.getGeometryType());
buffer.putInt(shapeType);
// 边界框
writeBoundingBox(buffer, features);
dos.write(buffer.array());
}
/**
* 写入SHP记录
*/
private static void writeShpRecord(DataOutputStream dos, byte[] recordContent, int recordNumber) throws IOException {
// 计算内容长度(以16位字为单位)
int contentLength = recordContent.length / 2;
// 写入记录头
ByteBuffer headerBuffer = ByteBuffer.allocate(8);
headerBuffer.order(ByteOrder.BIG_ENDIAN);
headerBuffer.putInt(recordNumber);
headerBuffer.putInt(contentLength);
dos.write(headerBuffer.array());
// 写入记录内容
dos.write(recordContent);
}
/**
* 写入点记录
*/
private static void writePointRecord(DataOutputStream dos, Point point) throws IOException {
writeLittleEndianInt(dos, SHAPE_TYPE_POINT);
writeLittleEndianDouble(dos, point.getX());
writeLittleEndianDouble(dos, point.getY());
}
/**
* 写入折线记录
*/
private static void writePolylineRecord(DataOutputStream dos, Polyline polyline) throws IOException {
writeLittleEndianInt(dos, SHAPE_TYPE_POLYLINE);
// 写入边界框
writeGeometryBoundingBox(dos, polyline);
// 部分数量和点数量
int partCount = polyline.getParts().size();
int totalPoints = getTotalPointCount(polyline);
writeLittleEndianInt(dos, partCount);
writeLittleEndianInt(dos, totalPoints);
// 写入部分索引
int currentIndex = 0;
for (ImmutablePart part : polyline.getParts()) {
writeLittleEndianInt(dos, currentIndex);
currentIndex += getPartPointCount(part);
}
// 写入所有点坐标
for (ImmutablePart part : polyline.getParts()) {
for (Point point : part.getPoints()) {
writeLittleEndianDouble(dos, point.getX());
writeLittleEndianDouble(dos, point.getY());
}
}
}
/**
* 写入多边形记录 - 关键修复:确保多边形正确闭合
*/
private static void writePolygonRecord(DataOutputStream dos, Polygon polygon) throws IOException {
writeLittleEndianInt(dos, SHAPE_TYPE_POLYGON);
// 写入边界框
writeGeometryBoundingBox(dos, polygon);
// 收集所有点和部分信息
List<List<Point>> allPartsPoints = new ArrayList<>();
int totalPoints = 0;
for (ImmutablePart part : polygon.getParts()) {
List<Point> points = new ArrayList<>();
for (Point point : part.getPoints()) {
points.add(point);
}
// 关键修复:确保多边形闭合
if (points.size() >= 3 && !points.get(0).equals(points.get(points.size() - 1))) {
points.add(points.get(0)); // 添加第一个点来闭合多边形
}
allPartsPoints.add(points);
totalPoints += points.size();
}
// 部分数量和点数量
int partCount = allPartsPoints.size();
writeLittleEndianInt(dos, partCount);
writeLittleEndianInt(dos, totalPoints);
// 写入部分索引
int currentIndex = 0;
for (List<Point> points : allPartsPoints) {
writeLittleEndianInt(dos, currentIndex);
currentIndex += points.size();
}
// 写入所有点坐标
for (List<Point> points : allPartsPoints) {
for (Point point : points) {
writeLittleEndianDouble(dos, point.getX());
writeLittleEndianDouble(dos, point.getY());
}
}
}
/**
* 写入多点记录
*/
private static void writeMultipointRecord(DataOutputStream dos, Multipoint multipoint) throws IOException {
writeLittleEndianInt(dos, SHAPE_TYPE_MULTIPOINT);
// 写入边界框
writeGeometryBoundingBox(dos, multipoint);
// 点的数量
int pointCount = getPointCount(multipoint);
writeLittleEndianInt(dos, pointCount);
// 所有点坐标
for (Point point : multipoint.getPoints()) {
writeLittleEndianDouble(dos, point.getX());
writeLittleEndianDouble(dos, point.getY());
}
}
/**
* 写入几何边界框
*/
private static void writeGeometryBoundingBox(DataOutputStream dos, Geometry geometry) throws IOException {
Envelope envelope = geometry.getExtent();
if (envelope != null) {
writeLittleEndianDouble(dos, envelope.getXMin());
writeLittleEndianDouble(dos, envelope.getYMin());
writeLittleEndianDouble(dos, envelope.getXMax());
writeLittleEndianDouble(dos, envelope.getYMax());
} else {
// 使用默认边界
writeLittleEndianDouble(dos, -180.0);
writeLittleEndianDouble(dos, -90.0);
writeLittleEndianDouble(dos, 180.0);
writeLittleEndianDouble(dos, 90.0);
}
}
/**
* 写入边界框信息
*/
private static void writeBoundingBox(ByteBuffer buffer, List<Feature> features) {
// 计算所有要素的实际边界
double xMin = Double.MAX_VALUE, yMin = Double.MAX_VALUE;
double xMax = -Double.MAX_VALUE, yMax = -Double.MAX_VALUE;
boolean hasValidBounds = false;
for (Feature feature : features) {
Geometry geometry = feature.getGeometry();
if (geometry != null) {
Envelope envelope = geometry.getExtent();
if (envelope != null) {
xMin = Math.min(xMin, envelope.getXMin());
yMin = Math.min(yMin, envelope.getYMin());
xMax = Math.max(xMax, envelope.getXMax());
yMax = Math.max(yMax, envelope.getYMax());
hasValidBounds = true;
}
}
}
if (!hasValidBounds) {
// 使用默认边界
xMin = -180.0; yMin = -90.0;
xMax = 180.0; yMax = 90.0;
}
buffer.putDouble(xMin);
buffer.putDouble(yMin);
buffer.putDouble(xMax);
buffer.putDouble(yMax);
buffer.putDouble(0.0); // Z最小值
buffer.putDouble(0.0); // Z最大值
buffer.putDouble(0.0); // M最小值
buffer.putDouble(0.0); // M最大值
}
/**
* 生成SHX文件字节数组
*/
private static byte[] generateShxFile(FeatureCollectionTable featureTable, List<Feature> features) throws IOException {
System.out.println(" 生成SHX文件...");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
try {
// 计算SHX文件长度
int shxFileLength = 50 + (4 * features.size()); // 50 words header + 4 words per record
// 写入SHX文件头
writeShxHeader(dos, featureTable, features, shxFileLength);
// 记录索引
int offset = 50; // 文件头长度(100字节)的一半,因为SHX以16位字为单位
// 计算每个记录的偏移量
for (Feature feature : features) {
byte[] recordContent = generateShpRecordContent(feature);
int contentLength = recordContent.length / 2; // 转换为16位字
ByteBuffer recordBuffer = ByteBuffer.allocate(8);
recordBuffer.order(ByteOrder.BIG_ENDIAN);
recordBuffer.putInt(offset);
recordBuffer.putInt(contentLength);
dos.write(recordBuffer.array());
offset += contentLength + 4; // 内容长度 + 记录头长度
}
dos.flush();
byte[] shxBytes = baos.toByteArray();
System.out.println(" ✓ SHX文件生成完成: " + shxBytes.length + " bytes");
return shxBytes;
} finally {
dos.close();
baos.close();
}
}
/**
* 写入SHX文件头
*/
private static void writeShxHeader(DataOutputStream dos, FeatureCollectionTable featureTable,
List<Feature> features, int fileLength) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(100);
buffer.order(ByteOrder.BIG_ENDIAN);
buffer.putInt(SHP_FILE_CODE);
for (int i = 0; i < 5; i++) {
buffer.putInt(0);
}
buffer.putInt(fileLength);
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.putInt(SHP_VERSION);
int shapeType = getShapeType(featureTable.getGeometryType());
buffer.putInt(shapeType);
writeBoundingBox(buffer, features);
dos.write(buffer.array());
}
/**
* 获取几何类型对应的Shapefile形状类型
*/
private static int getShapeType(GeometryType geometryType) {
switch (geometryType) {
case POINT:
return SHAPE_TYPE_POINT;
case POLYLINE:
return SHAPE_TYPE_POLYLINE;
case POLYGON:
return SHAPE_TYPE_POLYGON;
case MULTIPOINT:
return SHAPE_TYPE_MULTIPOINT;
default:
return SHAPE_TYPE_POINT;
}
}
// ========== 辅助方法 ==========
/**
* 获取多点几何的点数量
*/
private static int getPointCount(Multipoint multiPoint) {
int count = 0;
for (Point point : multiPoint.getPoints()) {
count++;
}
return count;
}
/**
* 获取部分中的点数量
*/
private static int getPartPointCount(ImmutablePart part) {
int count = 0;
for (Point point : part.getPoints()) {
count++;
}
return count;
}
/**
* 获取折线或多边形的总点数
*/
private static int getTotalPointCount(Geometry geometry) {
int totalPoints = 0;
if (geometry instanceof Polyline) {
Polyline polyline = (Polyline) geometry;
for (ImmutablePart part : polyline.getParts()) {
totalPoints += getPartPointCount(part);
}
} else if (geometry instanceof Polygon) {
Polygon polygon = (Polygon) geometry;
for (ImmutablePart part : polygon.getParts()) {
totalPoints += getPartPointCount(part);
}
}
return totalPoints;
}
// ========== 字节序转换工具方法 ==========
private static void writeLittleEndianInt(DataOutputStream dos, int value) throws IOException {
byte[] bytes = new byte[4];
ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).putInt(value);
dos.write(bytes);
}
private static void writeLittleEndianDouble(DataOutputStream dos, double value) throws IOException {
byte[] bytes = new byte[8];
ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).putDouble(value);
dos.write(bytes);
}
// ========== DBF文件生成方法 ==========
/**
* 字段信息类
*/
private static class FieldInfo {
String name;
char type;
int length;
int decimalCount;
FieldInfo(String name, char type, int length, int decimalCount) {
this.name = name;
this.type = type;
this.length = length;
this.decimalCount = decimalCount;
}
}
private static byte[] generateDbfFile(FeatureCollectionTable featureTable, List<Feature> features) throws IOException {
System.out.println(" 生成DBF文件...");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
try {
// 计算字段信息
List<FieldInfo> fieldInfos = getFieldInfos(featureTable);
int recordLength = calculateDbfRecordLength(fieldInfos);
int headerLength = 32 + (32 * fieldInfos.size()) + 1; // 基本头 + 字段描述符 + 终止符
// 1. 文件头
writeDbfHeader(dos, features.size(), headerLength, recordLength);
// 2. 字段描述符
for (FieldInfo fieldInfo : fieldInfos) {
writeDbfFieldDescriptor(dos, fieldInfo);
}
// 3. 字段终止符
dos.writeByte(DBF_HEADER_TERMINATOR);
// 4. 记录数据
for (Feature feature : features) {
writeDbfRecord(dos, feature, fieldInfos);
}
// 5. 文件终止符
dos.writeByte(0x1A);
dos.flush();
byte[] dbfBytes = baos.toByteArray();
System.out.println(" ✓ DBF文件生成完成: " + dbfBytes.length + " bytes");
return dbfBytes;
} finally {
dos.close();
baos.close();
}
}
/**
* 获取字段信息列表
*/
private static List<FieldInfo> getFieldInfos(FeatureCollectionTable featureTable) {
List<FieldInfo> fieldInfos = new ArrayList<>();
// 添加FID字段
fieldInfos.add(new FieldInfo("FID", 'N', 10, 0));
// 添加其他属性字段
for (Field field : featureTable.getFields()) {
String fieldName = field.getName();
if ("SHAPE".equalsIgnoreCase(fieldName)) {
continue; // 跳过几何字段
}
char fieldType = getDbfFieldType(field.getFieldType());
int fieldLength = getDbfFieldLength(field);
int decimalCount = getDbfDecimalCount(field);
fieldInfos.add(new FieldInfo(fieldName, fieldType, fieldLength, decimalCount));
}
return fieldInfos;
}
/**
* 计算DBF记录长度
*/
private static int calculateDbfRecordLength(List<FieldInfo> fieldInfos) {
int length = 1; // 删除标志
for (FieldInfo fieldInfo : fieldInfos) {
length += fieldInfo.length;
}
return length;
}
/**
* 写入DBF文件头
*/
private static void writeDbfHeader(DataOutputStream dos, int recordCount, int headerLength, int recordLength) throws IOException {
// 版本号
dos.writeByte(DBF_VERSION);
// 最后更新日期 (今天)
Calendar cal = Calendar.getInstance();
dos.writeByte(cal.get(Calendar.YEAR) - 1900);
dos.writeByte(cal.get(Calendar.MONTH) + 1);
dos.writeByte(cal.get(Calendar.DAY_OF_MONTH));
// 记录数 (little-endian)
writeLittleEndianInt(dos, recordCount);
// 头长度 (little-endian)
writeLittleEndianShort(dos, (short) headerLength);
// 记录长度 (little-endian)
writeLittleEndianShort(dos, (short) recordLength);
// 保留字段
for (int i = 0; i < 20; i++) {
dos.writeByte(0);
}
}
/**
* 写入DBF字段描述符
*/
private static void writeDbfFieldDescriptor(DataOutputStream dos, FieldInfo fieldInfo) throws IOException {
// 字段名 (11字节)
byte[] nameBytes = fieldInfo.name.getBytes(StandardCharsets.US_ASCII);
int nameLength = Math.min(10, nameBytes.length); // 最大10字符
dos.write(nameBytes, 0, nameLength);
for (int i = nameLength; i < 11; i++) {
dos.writeByte(0);
}
// 字段类型
dos.writeByte(fieldInfo.type);
// 字段地址 (4字节,通常为0)
writeLittleEndianInt(dos, 0);
// 字段长度
dos.writeByte(fieldInfo.length);
// 小数位数
dos.writeByte(fieldInfo.decimalCount);
// 保留字段
for (int i = 0; i < 14; i++) {
dos.writeByte(0);
}
}
/**
* 写入DBF记录
*/
private static void writeDbfRecord(DataOutputStream dos, Feature feature, List<FieldInfo> fieldInfos) throws IOException {
// 删除标志 (空格表示未删除)
dos.writeByte(' ');
// 字段数据
for (FieldInfo fieldInfo : fieldInfos) {
Object value;
if ("FID".equals(fieldInfo.name)) {
// 使用记录的索引作为FID
value = feature.getAttributes().get("FID");
if (value == null) {
value = feature.getAttributes().get("OBJECTID");
}
if (value == null) {
value = "0";
}
} else {
value = feature.getAttributes().get(fieldInfo.name);
}
writeDbfFieldValue(dos, value, fieldInfo);
}
}
/**
* 写入DBF字段值
*/
private static void writeDbfFieldValue(DataOutputStream dos, Object value, FieldInfo fieldInfo) throws IOException {
String stringValue;
if (value == null) {
stringValue = "";
} else {
stringValue = value.toString();
}
switch (fieldInfo.type) {
case 'C': // 字符类型
writeDbfString(dos, stringValue, fieldInfo.length);
break;
case 'N': // 数字类型
case 'F': // 浮点类型
writeDbfNumber(dos, stringValue, fieldInfo.length, fieldInfo.decimalCount);
break;
case 'D': // 日期类型
writeDbfDate(dos, value);
break;
default:
writeDbfString(dos, stringValue, fieldInfo.length);
}
}
/**
* 写入DBF字符串
*/
private static void writeDbfString(DataOutputStream dos, String value, int length) throws IOException {
// 使用UTF-8编码以支持中文
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
int writeLength = Math.min(length, bytes.length);
dos.write(bytes, 0, writeLength);
// 填充空格
for (int i = writeLength; i < length; i++) {
dos.writeByte(' ');
}
}
/**
* 写入DBF数字
*/
private static void writeDbfNumber(DataOutputStream dos, String value, int length, int decimalCount) throws IOException {
try {
if (value == null || value.trim().isEmpty()) {
// 空值,填充空格
for (int i = 0; i < length; i++) {
dos.writeByte(' ');
}
return;
}
// 尝试解析数字
double num = Double.parseDouble(value);
String format;
if (decimalCount > 0) {
format = "%" + length + "." + decimalCount + "f";
} else {
format = "%" + length + ".0f";
}
String formatted = String.format(Locale.US, format, num);
// 右对齐
if (formatted.length() > length) {
formatted = formatted.substring(0, length);
} else if (formatted.length() < length) {
formatted = String.format("%" + length + "s", formatted);
}
dos.write(formatted.getBytes(StandardCharsets.US_ASCII));
} catch (NumberFormatException e) {
// 如果解析失败,写入空格
for (int i = 0; i < length; i++) {
dos.writeByte(' ');
}
}
}
/**
* 写入DBF日期
*/
private static void writeDbfDate(DataOutputStream dos, Object value) throws IOException {
String dateStr;
if (value instanceof Date) {
Date date = (Date) value;
Calendar cal = Calendar.getInstance();
cal.setTime(date);
dateStr = String.format("%04d%02d%02d",
cal.get(Calendar.YEAR),
cal.get(Calendar.MONTH) + 1,
cal.get(Calendar.DAY_OF_MONTH));
} else {
// 写入当前日期
Calendar cal = Calendar.getInstance();
dateStr = String.format("%04d%02d%02d",
cal.get(Calendar.YEAR),
cal.get(Calendar.MONTH) + 1,
cal.get(Calendar.DAY_OF_MONTH));
}
dos.write(dateStr.getBytes(StandardCharsets.US_ASCII));
}
/**
* 获取DBF字段类型
*/
private static char getDbfFieldType(Field.Type fieldType) {
switch (fieldType) {
case TEXT:
return 'C';
case INTEGER:
case SHORT:
case OID:
return 'N';
case FLOAT:
case DOUBLE:
return 'F';
case DATE:
return 'D';
default:
return 'C'; // 字符类型
}
}
/**
* 获取DBF字段长度
*/
private static int getDbfFieldLength(Field field) {
switch (field.getFieldType()) {
case TEXT:
return Math.max(1, Math.min(254, field.getLength())); // 1-254字符
case INTEGER:
case SHORT:
return 10;
case FLOAT:
return 12;
case DOUBLE:
return 16;
case DATE:
return 8;
case OID:
return 10;
default:
return 10;
}
}
/**
* 获取DBF小数位数
*/
private static int getDbfDecimalCount(Field field) {
switch (field.getFieldType()) {
case FLOAT:
return 6;
case DOUBLE:
return 10;
default:
return 0;
}
}
/**
* 写入小端序short
*/
private static void writeLittleEndianShort(DataOutputStream dos, short value) throws IOException {
byte[] bytes = new byte[2];
ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).putShort(value);
dos.write(bytes);
}
private static byte[] generatePrjFile(FeatureCollectionTable featureTable) {
System.out.println(" 生成PRJ文件...");
SpatialReference spatialReference = featureTable.getSpatialReference();
if (spatialReference == null) {
System.out.println(" ⚠ 无空间参考信息,跳过PRJ文件生成");
return new byte[0];
}
String wkt = getWktForSpatialReference(spatialReference);
if (wkt == null || wkt.isEmpty()) {
System.out.println(" ⚠ 无法生成WKT,跳过PRJ文件生成");
return new byte[0];
}
byte[] prjBytes = wkt.getBytes(StandardCharsets.UTF_8);
System.out.println(" ✓ PRJ文件生成完成: " + prjBytes.length + " bytes");
return prjBytes;
}
private static String getWktForSpatialReference(SpatialReference spatialReference) {
if (spatialReference == null) {
return null;
}
int wkid = spatialReference.getWkid();
switch (wkid) {
case 4326:
return "GEOGCS[\"GCS_WGS_1984\",DATUM[\"D_WGS_1984\",SPHEROID[\"WGS_1984\",6378137,298.257223563]],PRIMEM[\"Greenwich\",0],UNIT[\"Degree\",0.017453292519943295]]";
case 3857:
return "PROJCS[\"WGS_1984_Web_Mercator_Auxiliary_Sphere\",GEOGCS[\"GCS_WGS_1984\",DATUM[\"D_WGS_1984\",SPHEROID[\"WGS_1984\",6378137,298.257223563]],PRIMEM[\"Greenwich\",0],UNIT[\"Degree\",0.017453292519943295]],PROJECTION[\"Mercator_Auxiliary_Sphere\"],PARAMETER[\"False_Easting\",0],PARAMETER[\"False_Northing\",0],PARAMETER[\"Central_Meridian\",0],PARAMETER[\"Standard_Parallel_1\",0],PARAMETER[\"Auxiliary_Sphere_Type\",0],UNIT[\"Meter\",1]]";
default:
return "GEOGCS[\"GCS_Unknown\",DATUM[\"D_Unknown\",SPHEROID[\"Unknown\",6378137,298.257223563]],PRIMEM[\"Greenwich\",0],UNIT[\"Degree\",0.017453292519943295]]";
}
}
private static byte[] generateCpgFile() {
System.out.println(" 生成CPG文件...");
String encoding = "UTF-8";
byte[] cpgBytes = encoding.getBytes(StandardCharsets.UTF_8);
System.out.println(" ✓ CPG文件生成完成: " + cpgBytes.length + " bytes");
return cpgBytes;
}
// ========== 使用示例 ==========
public static void main(String[] args) {
try {
System.out.println("=== FeatureCollectionTable转换为SHP文件示例 ===");
SecretKey key = AESEncryptionUtils.generateKey("123");
// 创建示例FeatureCollectionTable
FeatureCollectionTable featureTable = createSamplePolygonTable();
// 转换为SHP文件并保存到指定目录
String outputDir = "./output/shapefile";
String baseName = "polygon_output";
convertToShapefile(featureTable, outputDir, baseName, key, false);
System.out.println("✓ 转换完成,文件已保存到: " + outputDir);
} catch (Exception e) {
System.err.println("示例执行失败: " + e.getMessage());
e.printStackTrace();
}
}
/**
* 创建包含多个多边形的示例表 - 修复版本
*/
private static FeatureCollectionTable createSamplePolygonTable() {
// 创建字段
List<Field> fields = new ArrayList<>();
fields.add(Field.createString("name", "名称", 50));
fields.add(Field.createInteger("value", "数值"));
fields.add(Field.createDouble("area", "面积"));
// 创建要素表
FeatureCollectionTable table = new FeatureCollectionTable(fields,
GeometryType.POLYGON, SpatialReference.create(4326));
try {
// 同步添加要素,确保所有要素都被添加
List<ListenableFuture<Void>> futures = new ArrayList<>();
// 示例1:矩形
Feature feature1 = table.createFeature();
feature1.getAttributes().put("name", "矩形地块");
feature1.getAttributes().put("value", 100);
feature1.getAttributes().put("area", 2500.0);
PointCollection rectPoints = new PointCollection(SpatialReference.create(4326));
rectPoints.add(new Point(116.0, 39.0));
rectPoints.add(new Point(116.1, 39.0));
rectPoints.add(new Point(116.1, 39.1));
rectPoints.add(new Point(116.0, 39.1));
rectPoints.add(new Point(116.0, 39.0)); // 闭合
// 使用Part而不是ImmutablePart
Part rectPart = new Part(rectPoints);
PartCollection rectParts = new PartCollection(SpatialReference.create(4326));
rectParts.add(rectPart);
Polygon rectPolygon = new Polygon(rectParts);
feature1.setGeometry(rectPolygon);
futures.add(table.addFeatureAsync(feature1));
// 示例2:三角形
Feature feature2 = table.createFeature();
feature2.getAttributes().put("name", "三角区域");
feature2.getAttributes().put("value", 200);
feature2.getAttributes().put("area", 1250.5);
PointCollection trianglePoints = new PointCollection(SpatialReference.create(4326));
trianglePoints.add(new Point(116.2, 39.0));
trianglePoints.add(new Point(116.3, 39.1));
trianglePoints.add(new Point(116.1, 39.1));
trianglePoints.add(new Point(116.2, 39.0)); // 闭合
Part trianglePart = new Part(trianglePoints);
PartCollection triangleParts = new PartCollection(SpatialReference.create(4326));
triangleParts.add(trianglePart);
Polygon trianglePolygon = new Polygon(triangleParts);
feature2.setGeometry(trianglePolygon);
futures.add(table.addFeatureAsync(feature2));
// 示例3:五边形
Feature feature3 = table.createFeature();
feature3.getAttributes().put("name", "五边形地块");
feature3.getAttributes().put("value", 300);
feature3.getAttributes().put("area", 1800.75);
PointCollection pentagonPoints = new PointCollection(SpatialReference.create(4326));
double centerX = 116.15, centerY = 39.2, radius = 0.05;
for (int i = 0; i < 5; i++) {
double angle = 2 * Math.PI * i / 5;
double x = centerX + radius * Math.cos(angle);
double y = centerY + radius * Math.sin(angle);
pentagonPoints.add(new Point(x, y));
}
pentagonPoints.add(pentagonPoints.get(0)); // 闭合
Part pentagonPart = new Part(pentagonPoints);
PartCollection pentagonParts = new PartCollection(SpatialReference.create(4326));
pentagonParts.add(pentagonPart);
Polygon pentagonPolygon = new Polygon(pentagonParts);
feature3.setGeometry(pentagonPolygon);
futures.add(table.addFeatureAsync(feature3));
// 示例4:不规则多边形
Feature feature4 = table.createFeature();
feature4.getAttributes().put("name", "不规则地块");
feature4.getAttributes().put("value", 400);
feature4.getAttributes().put("area", 1950.3);
PointCollection irregularPoints = new PointCollection(SpatialReference.create(4326));
irregularPoints.add(new Point(116.4, 39.0));
irregularPoints.add(new Point(116.5, 39.02));
irregularPoints.add(new Point(116.55, 39.08));
irregularPoints.add(new Point(116.52, 39.12));
irregularPoints.add(new Point(116.45, 39.1));
irregularPoints.add(new Point(116.42, 39.06));
irregularPoints.add(new Point(116.4, 39.0)); // 闭合
Part irregularPart = new Part(irregularPoints);
PartCollection irregularParts = new PartCollection(SpatialReference.create(4326));
irregularParts.add(irregularPart);
Polygon irregularPolygon = new Polygon(irregularParts);
feature4.setGeometry(irregularPolygon);
futures.add(table.addFeatureAsync(feature4));
// 示例5:另一个矩形
Feature feature5 = table.createFeature();
feature5.getAttributes().put("name", "第二个矩形");
feature5.getAttributes().put("value", 500);
feature5.getAttributes().put("area", 3000.0);
PointCollection rect2Points = new PointCollection(SpatialReference.create(4326));
rect2Points.add(new Point(116.6, 39.0));
rect2Points.add(new Point(116.8, 39.0));
rect2Points.add(new Point(116.8, 39.2));
rect2Points.add(new Point(116.6, 39.2));
rect2Points.add(new Point(116.6, 39.0)); // 闭合
Part rect2Part = new Part(rect2Points);
PartCollection rect2Parts = new PartCollection(SpatialReference.create(4326));
rect2Parts.add(rect2Part);
Polygon rect2Polygon = new Polygon(rect2Parts);
feature5.setGeometry(rect2Polygon);
futures.add(table.addFeatureAsync(feature5));
// 等待所有要素添加完成
for (ListenableFuture<Void> future : futures) {
try {
future.get(5, TimeUnit.SECONDS);
} catch (Exception e) {
System.err.println("添加要素失败: " + e.getMessage());
}
}
// 强制加载表数据
if (table.getLoadStatus() != LoadStatus.LOADED) {
table.loadAsync();
Thread.sleep(1000);
}
System.out.println("创建了 " + table.getTotalFeatureCount() + " 个多边形要素");
} catch (Exception e) {
System.err.println("创建多边形数据失败: " + e.getMessage());
e.printStackTrace();
}
return table;
}
}
6.关键技术难点与解决方案
难点1 :内存中处理加密文件
问题: 传统方法需要生成临时解密文件,存在安全风险。
解决方案: 使用Apache Commons VFS创建内存文件系统,完全在内存中完成解密和解析流程。
难点2 :几何数据格式转换
问题: GeoTools(JTS)与ArcGIS Runtime的几何对象不兼容。
解决方案: 实现完整的几何转换方法,支持点、线、面、多点等所有几何类型。
难点3 :中文字符编码
问题: Shapefile的DBF文件编码不统一,中文容易乱码。
解决方案: 实现自动编码检测机制,支持GBK、UTF-8等多种编码格式。
难点4 :多边形闭合问题
问题: 多边形在转换过程中可能出现未闭合情况。
解决方案: 在写入Shapefile时自动检查并闭合多边形环。
7.性能优化建议
批量处理 - 使用批量操作减少内存开销
资源管理 - 及时关闭文件句柄和内存资源
编码缓存 - 对已识别的编码进行缓存提升解析速度
几何简化 - 对复杂几何进行适当简化提升处理效率
8.应用场景
政府测绘数据 - 保护敏感的地理信息数据
企业资产管理系统 - 保护商业位置数据
移动GIS应用 - 在移动设备上安全处理GIS数据
云GIS服务 - 在云端安全存储和传输GIS数据
9.总结
本文提出的解决方案成功解决了加密SHP文件的全流程处理问题,主要优势包括:
安全性 - 端到端的加密保护
完整性 - 支持完整的GIS数据操作
兼容性 - 与现有GIS工具链兼容
性能 - 内存操作提升处理效率
10.资源下载
暂未上传git,目前提供3个文件代码已可正常执行,可私信提供更多帮助。