ArcGIS Runtime与GeoTools融合实践:加密SHP文件的完整读写方案

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个文件代码已可正常执行,可私信提供更多帮助。

相关推荐
Juchecar2 小时前
Spring是Java语境下的“最优解”的原因与启示
java·spring·node.js
邪恶喵喵2 小时前
Tomcat和负载均衡
java·tomcat·负载均衡
尼古拉斯·纯情暖男·天真·阿玮2 小时前
动态规划——子序列问题
java·算法·动态规划
代码不停2 小时前
Java中文件操作和IO
java
夏天的味道٥2 小时前
IDEA 开发工具常用插件整理
java·ide·intellij-idea
勇者无畏4042 小时前
基于 Spring AI Alibaba 搭建 Text-To-SQL 智能系统(初始化)
java·后端·spring
一枚懒人2 小时前
Java的Lamdba语法和函数式编程理解
java
土豆南瓜饼2 小时前
关于mybatis-plus的一些默认配置
java
Juchecar2 小时前
Java示例:设计模式是如何在实战中“自然生长”出来
java·设计模式