package cn.edu.whu.boot.allocation.controllers;

import cn.edu.whu.boot.allocation.controllers.dtos.CustomFeatureCollection;
import cn.edu.whu.boot.allocation.controllers.dtos.Pair;
import cn.edu.whu.boot.allocation.controllers.dtos.Worker;
import cn.edu.whu.boot.allocation.entities.TaskCell;
import cn.edu.whu.boot.allocation.entities.TaskCellData;
import cn.edu.whu.boot.allocation.entities.TaskUserCell;
import cn.edu.whu.boot.allocation.entities.TaskUserRole;
import cn.edu.whu.boot.allocation.exceptions.InvalidAssignmentStatus;
import cn.edu.whu.boot.allocation.exceptions.InvalidShapefileException;
import cn.edu.whu.boot.allocation.services.*;
import cn.edu.whu.boot.collection.dto.GeometryType;
import cn.edu.whu.boot.collection.service.IBaseService;
import cn.edu.whu.boot.common.domain.ResultMessage;
import cn.edu.whu.boot.common.service.CacheService;
import cn.edu.whu.boot.common.utils.allocation.ArcGisGeoJsonParser;
import cn.edu.whu.boot.common.utils.allocation.GeohashUtil;
import cn.edu.whu.boot.common.utils.allocation.KMeansClustering;
import cn.edu.whu.boot.common.utils.allocation.ShapefileGeometryCursor;
import cn.edu.whu.boot.common.utils.collection.PostgreSqlUtil;
import cn.edu.whu.boot.common.utils.collection.ShapeTypeUtil;
import cn.edu.whu.boot.common.utils.collection.TableOperationUtil;
import cn.edu.whu.boot.common.utils.xml.CheckTableDataUtil;
import cn.edu.whu.boot.xml.domain.RedisKeyType;
import cn.edu.whu.boot.xml.domain.Task;
import cn.edu.whu.boot.xml.service.ITaskService;
import cn.edu.whu.boot.xml.xmlreader.bean.Config;
import cn.edu.whu.boot.xml.xmlreader.bean.Table;
import com.alibaba.fastjson.JSON;
import com.esri.core.geometry.*;
import com.esri.core.geometry.ogc.OGCMultiPolygon;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.geojson.Feature;
import org.geojson.FeatureCollection;
import org.geojson.GeoJsonObject;
import org.geojson.LngLatAlt;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.IOException;
import java.io.InputStream;
import java.text.MessageFormat;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@RestController
@RequestMapping("/allocation")
@Validated
public class TaskController {

    private final Logger log = LoggerFactory.getLogger(TaskController.class);

    private final GeometryFactory geometryFactory = new GeometryFactory();

    private final Gson gson = new Gson();

    private final ObjectMapper objectMapper = new ObjectMapper();

    private int precision = 1;

    private boolean precisionDecided = false;

    private final TaskCellDataService taskCellDataService;

    private final TaskUserCellService taskUserCellService;

    private final TaskCellService taskCellService;

    private final TaskUserRoleService taskUserRoleService;

    private final CacheService cacheService;

    @Autowired
    private TaskInfoService taskInfoService;

    @Autowired
    private ITaskService taskService;

    @Autowired
    private IBaseService baseService;

    public TaskController(TaskCellDataService taskCellDataService, TaskUserCellService taskUserCellService,
                          TaskCellService taskCellService, TaskUserRoleService taskUserRoleService, CacheService cacheService) {
        this.taskCellDataService = taskCellDataService;
        this.taskUserCellService = taskUserCellService;
        this.taskCellService = taskCellService;
        this.taskUserRoleService = taskUserRoleService;
        this.cacheService = cacheService;
    }

    /**
     * 判断任务是否已经分配
     *
     * @param taskId 任务 ID
     */
    @RequestMapping("/is_assigned")
    @ResponseBody
    public ResultMessage isAssigned(@RequestParam("taskId") int taskId) {
        boolean result = _isAssigned(taskId);
        return new ResultMessage<>(true, String.format("任务%s分配", result ? "已" : "未"), result);
    }

    private boolean _isAssigned(int taskId) {
        return !taskCellService.findByTaskId(taskId).isEmpty();
    }

    /**
     * 设置任务是否需要分配
     *
     * @param taskId   任务 ID
     * @param optional true/false
     */
    @RequestMapping("/optional")
    @ResponseBody
    public ResultMessage setTaskAllocationOptional(@RequestParam("taskId") int taskId, @RequestParam("optional") boolean optional) {
        taskInfoService.setTaskAllocationOptional(taskId, optional);
        return new ResultMessage<>(true, "修改成功");
    }


    /**
     * 自动分配任务区域 如果是审核任务，还要将数据分配给审核人员将分配的结果，
     * 即网格-人员和数据-人员的对应关系返回给前端
     *
     * @param taskId     任务 ID
     * @param jsonString JSON 字符串，包含外业人员信息（id，角色）
     * @param shp        区域的 shp 文件或其压缩文件
     * @throws Exception 某某异常
     */
    @RequestMapping("/assignment")
    @ResponseBody
    public ResultMessage assignCollectingTasks(@RequestParam("taskId") @NotNull(message = "必须上传任务 ID") int taskId,
                                               @RequestParam("workers") @NotBlank(message = "必须上传外业人员") String jsonString,
                                               @RequestParam(name = "precision", defaultValue = "0") int precision,
                                               @RequestParam(name = "shp", required = false) MultipartFile shp) throws Exception {
        this.precision = 1;
        this.precisionDecided = false;

        // 检查该任务是否已经分配
        if (_isAssigned(taskId)) {
            // 如果已经分配，清除原先的分配结果
            taskInfoService.deleteTaskInfo(taskId);
        }

        // 解析 shp 文件，读取其中的多边形区域
        boolean isShp = false;
        List<com.esri.core.geometry.Polygon> regions = new LinkedList<>();
        if (shp != null) {
            regions = readShp(resolveUploadedFile(shp));
            isShp = !regions.isEmpty();
        }

        List<Worker> workers = gson.fromJson(jsonString, new TypeToken<ArrayList<Worker>>() {
        }.getType());

        // 获取任务的数据
        Map<String, Map<Long, String>> tbToData = getIdAndGeometryData(taskId); // Key: 表名，Value: 数据 ID - JSON 字符串的映射

        // 有数据，说明这是一个审核任务
        tbToData = tbToData
                .entrySet()
                .stream()
                .filter(it -> {
                    Map<Long, String> map = it.getValue();
                    return map != null && !map.isEmpty();
                })
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
        boolean isData = !tbToData.isEmpty();
        // 先检查用户是否指定了审核人员
        if (isData) {
            if (workers.stream().noneMatch(Worker::isInspector)) {
                throw new InvalidAssignmentStatus("没有指定巡查人员");
            }
        }

        if (!isShp && !isData) {
            throw new InvalidAssignmentStatus("没有工作区域，数据库中也无数据，无法进行分配");
        }

        // 检查前端是否指定 Geohash 精度
        if (precision != 0) {
            this.precision = precision;
            this.precisionDecided = true;
        }

        // 工作人员-网格的对应关系
        Map<Worker, List<Polygon>> workerToCells = new HashMap<>();
        // 任务-网格-数据的对应关系
        List<TaskCellData> cellToData = new LinkedList<>();
        Task task = null;
        // Key：表名，Value：数据 ID - 审核人员 ID 的映射
        Map<String, Map<Long, Integer>> assignedData = new HashMap<>();
        // 标识工作区域的格网
        List<Polygon> cells;
        List<Worker> inspectors = workers.stream().filter(Worker::isInspector).collect(Collectors.toList());
        if (isShp) {
            // 上传了 shp，首先将工作区域分配给外业人员
            // 如果这是一个采集任务，把工作区域分给所有人
            List<Worker> _workers = workers;
            if (isData) {
                // 否则，先把工作区域分给采集人员
                _workers = _workers.stream().filter(Worker::isCollector).collect(Collectors.toList());
                if (_workers.size() == 0) {
                    _workers = workers;
                }
            }
            if (this.precisionDecided) {
                // 根据指定的 Geohash 精度，将区域的 MBR 划分成矩形格网
                // 1. 获取 MBR
                double xmin = Double.MAX_VALUE;
                double xmax = Double.MIN_VALUE;
                double ymin = Double.MAX_VALUE;
                double ymax = Double.MIN_VALUE;
                for (com.esri.core.geometry.Polygon region : regions) {
                    Envelope2D envelope = getEnvelope(region);
                    if (envelope.xmin < xmin) {
                        xmin = envelope.xmin;
                    }
                    if (envelope.xmax > xmax) {
                        xmax = envelope.xmax;
                    }
                    if (envelope.ymin < ymin) {
                        ymin = envelope.ymin;
                    }
                    if (envelope.ymax > ymax) {
                        ymax = envelope.ymax;
                    }
                }
                // 2. 将 MBR 划分成格网
                Envelope2D MBR = new Envelope2D(xmin, ymin, xmax, ymax);
                assignCells(_workers, regions, MBR, workerToCells);
            } else {
                // 走之前的流程
                assignRegions(_workers, regions, workerToCells);
            }
            cells = workerToCells
                    .values()
                    .stream()
                    .flatMap(List::stream)
                    .collect(Collectors.toList());
        } else {
            // 没有上传 shp，但是有数据
            // 1. 先计算出能包含所有数据的 MBR
            double xmin = Double.MAX_VALUE;
            double xmax = Double.MIN_VALUE;
            double ymin = Double.MAX_VALUE;
            double ymax = Double.MIN_VALUE;
            for (Map.Entry<String, Map<Long, String>> entry : tbToData.entrySet()) {
                for (Map.Entry<Long, String> e : entry.getValue().entrySet()) {
                    // 将 JSON 字符串转换成几何对象
                    org.locationtech.jts.geom.Geometry geometry = ArcGisGeoJsonParser
                            .toGeometryObject(ShapeTypeUtil.changeGeometryToArcGIS(e.getValue()));
                    if (geometry == null) {
                        continue;
                    }
                    // 更新 MBR 的坐标
                    org.locationtech.jts.geom.Geometry envelope = geometry.getEnvelope();
                    for (Coordinate coordinate : envelope.getCoordinates()) {
                        double y = coordinate.y;
                        if (y < ymin) {
                            ymin = y;
                        } else if (y > ymax) {
                            ymax = y;
                        }
                        double x = coordinate.x;
                        if (x < xmin) {
                            xmin = x;
                        } else if (x > xmax) {
                            xmax = x;
                        }
                    }
                }
            }
            com.esri.core.geometry.Polygon mbr = new com.esri.core.geometry.Polygon();
            mbr.startPath(xmin, ymax);
            mbr.lineTo(xmax, ymax);
            mbr.lineTo(xmax, ymin);
            mbr.lineTo(xmin, ymin);
            // 2. 如果用户没有指定 Geohash 精度，根据审核人员的数量确定 Geohash 精度，
            //假设审核人员有 X 个，那么最好能将 MBR 划分成 X 个矩形
            if (!this.precisionDecided) {
                double width = (111.320 * Math.cos(((ymin + ymax) / 2.0) * Math.PI / 180.0)) * (xmax - xmin);
                double height = 110.574 * (ymax - ymin);
                findGeohashPrecision(inspectors, width, height);
            }
            // 3. 将 mbr 划分成矩形格网
            List<com.esri.core.geometry.Polygon> region = Collections.singletonList(mbr);
            while (true) {
                // 如果划分出的格网数量少于人员数量
                // 增加 Geohash 的精度继续尝试划分
                cells = generateCells(region, xmin, xmax, ymin, ymax);
                if (cells.size() >= inspectors.size()) {
                    break;
                } else {
                    this.precision++;
                    if (this.precision > 12) {
                        throw new InvalidAssignmentStatus("无法找到合适的划分等级，请检查工作区域是否过小");
                    }
                }
            }
        }

        // 计算每个网格的 Geohash
        cells.forEach(c -> {
            Point centroid = c.getCentroid();
            // 某些情况下两个相邻网格的 Geohash 会相同，所以加上随机 8 位随机字符串作为后缀
            c.setUserData(GeohashUtil.geohashPoint(centroid.getX(), centroid.getY(), this.precision) + UUID.randomUUID().toString().split("-")[4]);
        });

        // 分配数据
        if (isData) {
            if (workers.stream().noneMatch(Worker::isCollector)) {
                // 因为在没有指定采集人员的情况下，在前面分配网格时就已经审核人员分配了网格
                // 但是在后面分配数据时，需要把数据对应的网格也分配给审核人员
                // 为了防止重复分配，这里先把前面分配给审核人员的网格清除
                inspectors.forEach(workerToCells::remove);
            }
            assignData(taskId, inspectors, tbToData, workerToCells, cells, cellToData, assignedData);
            task = taskService.getTaskById(taskId);
        }

        // 获取外业人员-网格的对应关系
        // 同时将对应关系转换为 TaskUserCell 对象和 TaskCell 对象
        Set<TaskCell> taskCells = new HashSet<>();
        List<TaskUserCell> taskUserCells = workerToCells
                .entrySet()
                .stream()
                .map(entry -> entry
                        .getValue()
                        .stream()
                        .map(cell -> {
                            Worker worker = entry.getKey();
                            String geohash = (String) cell.getUserData();
                            String geojson = generateCellGeoJsonString(toGeoJsonPolygon(cell), null);
                            taskCells.add(new TaskCell(taskId, geohash, geojson));
                            return new TaskUserCell(taskId, worker.getId(), geohash);
                        })
                        .collect(Collectors.toList())
                )
                .flatMap(List::stream)
                .distinct()
                .collect(Collectors.toList());

        // 外业人员-工作角色的对应关系
        List<TaskUserRole> taskUserRole = workers.stream().map(worker -> new TaskUserRole(taskId, worker.getId(), worker.getRole())).collect(Collectors.toList());

        // 将任务的相关信息都保存到数据库中
        boolean result = taskInfoService.updateTaskInfo(cellToData, new LinkedList<>(taskCells), taskUserCells, taskUserRole, task, assignedData);
        if (!result) {
            return new ResultMessage<>(false, "任务分配失败");
        } else {
            // 返回人员-网格和数据的对应关系
            log.info("成功分配任务 {}", taskId);
            return new ResultMessage<>(true, "成功分配任务");
        }
    }

    /**
     * 根据工作区域面积和工作人员数量，确定合适的 GeoHash 精度
     */
    private void findGeohashPrecision(List<Worker> workers, double width, double height) {
        this.precision = GeohashUtil.findPrecision(width * height / workers.size());
        double[] cellSize = GeohashUtil.getCellSize(this.precision);
        if (cellSize == null || cellSize[0] > width || cellSize[1] > height) {
            if (width < height) {
                this.precision = GeohashUtil.findPrecisionByWidth(width / workers.size());
            } else {
                this.precision = GeohashUtil.findPrecisionByHeight(height / workers.size());
            }
        }
        this.precisionDecided = true;
    }

    /**
     * 将数据分配给审核人员，同时记录下审核人员-网格、数据-网格的对应关系
     */
    private void assignData(int taskId,
                            List<Worker> inspectors,
                            Map<String, Map<Long, String>> tbToData,
                            Map<Worker, List<Polygon>> workerToCells,
                            List<Polygon> cells,
                            List<TaskCellData> cellToData,
                            Map<String, Map<Long, Integer>> assignedData) {
        Map<String, Map<Long, String>> nonNullData = tbToData
                .entrySet()
                .stream()
                .filter(it -> Objects.nonNull(it.getValue()))
                .filter(it -> !it.getValue().isEmpty())
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
        // 建立质心到几何要素之间的对应关系
        Map<Point, List<org.locationtech.jts.geom.Geometry>> centroidToStrId = new HashMap<>();
        for (Map.Entry<String, Map<Long, String>> entry : nonNullData.entrySet()) {
            String tableName = entry.getKey();
            for (Map.Entry<Long, String> jsonData : entry.getValue().entrySet()) {
                org.locationtech.jts.geom.Geometry geometry = ArcGisGeoJsonParser
                        .toGeometryObject(ShapeTypeUtil.changeGeometryToArcGIS(jsonData.getValue()));
                Optional.ofNullable(geometry)
                        .ifPresent(it -> {
                                    geometry.setUserData(tableName + "/" + jsonData.getKey());
                                    centroidToStrId
                                            .computeIfAbsent(geometry.getCentroid(), k -> new LinkedList<>())
                                            .add(geometry);
                                }
                        );
            }
        }
        List<Point> centroids = new LinkedList<>(centroidToStrId.keySet());
        if (centroids.size() < inspectors.size()) {
            // 1. 质心数量少于审核人员数量，均分数据
            List<org.locationtech.jts.geom.Geometry> data = centroidToStrId
                    .values()
                    .stream()
                    .flatMap(List::stream)
                    .collect(Collectors.toList());
            int workload = (int) Math.ceil((data.size() * 1.0) / inspectors.size());
            for (int i = 0; i < inspectors.size(); i++) {
                Worker inspector = inspectors.get(i);
                for (int j = i * workload; j < data.size(); j++) {
                    org.locationtech.jts.geom.Geometry geometry = data.get(j);
                    assignDataAndCell(taskId, workerToCells, cells, cellToData, assignedData, inspector, geometry);
                }
            }
        } else if (inspectors.size() == 1) {
            // 2. 只有一个审核人员
            centroidToStrId
                    .values()
                    .stream()
                    .flatMap(List::stream)
                    .forEach(geometry -> assignDataAndCell(taskId, workerToCells, cells, cellToData, assignedData, inspectors.get(0), geometry));
        } else {
            // 3. 否则先聚类，再将类簇随机分配给审核人员
            Map<Integer, List<Point>> clusters = KMeansClustering
                    .clustering(centroids, inspectors.size());
            for (int i = 0; i < inspectors.size(); i++) {
                Worker inspector = inspectors.get(i);
                for (Point centroid : clusters.get(i)) {
                    for (org.locationtech.jts.geom.Geometry geometry : centroidToStrId.get(centroid)) {
                        assignDataAndCell(taskId, workerToCells, cells, cellToData, assignedData, inspector, geometry);
                    }
                }
            }
        }
    }

    // 将数据分配给审核人员
    // 如果在前面分配网格时没有涉及到审核人员，
    // 那么在这里还要将数据对应的网格分配给审核人员
    private void assignDataAndCell(int taskId, Map<Worker, List<Polygon>> workerToCells, List<Polygon> cells, List<TaskCellData> cellToData, Map<String, Map<Long, Integer>> assignedData, Worker inspector, org.locationtech.jts.geom.Geometry geometry) {
        // strId 的格式为 "tableName/dataId"
        String strId = (String) geometry.getUserData();
        String tableName = strId.substring(0, strId.indexOf("/"));
        long dataId = Long.parseLong(strId.substring(strId.indexOf("/") + 1));
        // 将数据分配给审核人员
        assignedData.computeIfAbsent(tableName, k -> new HashMap<>()).put(dataId, inspector.getId());
        // 找出与数据相交的网格，分配给该审核人员
        if (!cells.isEmpty()) {
            for (Polygon cell : cells) {
                if (cell.intersects(geometry)) {
                    cellToData.add(new TaskCellData(
                            taskId,
                            tableName,
                            dataId,
                            (String) cell.getUserData())
                    );
                    workerToCells.computeIfAbsent(inspector, k -> new LinkedList<>()).add(cell);
                }
            }
        }
    }

    /**
     * 分配任务区域
     *
     * @param collectors    采集人员
     * @param regions       区域列表
     * @param workerToCells 外业人员-网格的对应关系
     * @throws Exception Geohash 精度过低
     */
    private void assignRegions(List<Worker> collectors,
                               List<com.esri.core.geometry.Polygon> regions,
                               Map<Worker, List<Polygon>> workerToCells) throws Exception {
        if (regions.size() == collectors.size()) {
            // 区域数量等于外业人员数量，直接随机分配区域
            for (int i = 0; i < regions.size(); i++) {
                com.esri.core.geometry.Polygon region = regions.get(i);
                Worker worker = collectors.get(i);
                workerToCells.put(worker, Stream.of(region).map(this::fromEsriPolygon).collect(Collectors.toList()));
            }
        } else if (regions.size() < collectors.size() && regions.size() > 1) {
            // 有多个区域，但区域数量小于外业人员数量
            List<Polygon> polygons = regions.stream().map(this::fromEsriPolygon).collect(Collectors.toList());
            // 首先，每个区域里都分配一个人
            int i = 0; // worker 列表的索引
            for (Polygon polygon : polygons) {
                Worker worker = collectors.get(i);
                workerToCells.put(worker, Collections.singletonList(polygon));
                i++;
            }
            // 然后，以区域的面积为权重分配剩下的人，面积大的区域分到的人就相应的多一些
            // 计算每个区域理论上应该能分到多少人
            final int headcount = collectors.size() - i;
            double sumArea = polygons.stream().mapToDouble(Polygon::getArea).sum(); // 总面积
            Map<Polygon, Integer> polygonToHeadcount = polygons
                    .stream()
                    .map(polygon -> new AbstractMap.SimpleEntry<>(polygon, (int) Math.ceil(headcount * (polygon.getArea() / sumArea))))
                    .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
                            (oldValue, newValue) -> oldValue, LinkedHashMap::new));
            // 分配剩下的人，分完了就停止
            for (Map.Entry<Polygon, Integer> entry : polygonToHeadcount.entrySet()) {
                int count = entry.getValue();
                for (int end = i + count; i < collectors.size() && i < end; i++) {
                    Worker worker = collectors.get(i);
                    workerToCells.put(worker, Collections.singletonList(entry.getKey()));
                }
                if (i == collectors.size()) {
                    break;
                }
            }
        } else if (regions.size() > collectors.size()) {
            // 区域数量大于外业人员数量
            // 对区域聚类，分配给外业人员
            // 建立多边形形心到多边形之间的映射
            // 不同多边形可能具有相同形心
            Map<Point, List<Polygon>> centroidToCells = regions
                    .stream()
                    .map(this::fromEsriPolygon)
                    .collect(Collectors.groupingBy(org.locationtech.jts.geom.Geometry::getCentroid));
            // 聚类
            Map<Integer, List<org.locationtech.jts.geom.Point>> clusters = KMeansClustering
                    .clustering(new LinkedList<>(centroidToCells.keySet()), collectors.size());
            // 分配区域
            for (Map.Entry<Integer, List<Point>> entry : clusters.entrySet()) {
                Worker worker = collectors.get(entry.getKey());
                workerToCells.put(
                        worker,
                        entry.getValue().stream().map(centroidToCells::get).flatMap(List::stream).collect(Collectors.toList())
                );
            }
        } else {
            // 只有一个区域
            // 将区域划分为网格，然后分配网格
            // 首先要确定 Geohash 精度
            // 原则：划分出的网格数量大于等于外业人员的数量
            Envelope2D MBR = getEnvelope(regions.get(0));
            // 计算区域外接矩形的面积
            // Longitude: 1 deg = 111.320*cos(latitude) km
            double width = (111.320 * Math.cos(((MBR.ymin + MBR.ymax) / 2.0) * Math.PI / 180.0)) * (MBR.xmax - MBR.xmin);
            // Latitude: 1 deg = 110.574 km
            double height = 110.574 * (MBR.ymax - MBR.ymin);
            findGeohashPrecision(collectors, width, height);
            assignCells(collectors, regions, MBR, workerToCells);
        }
    }

    /**
     * 将 MBR 划分为格网，然后分配格网
     *
     * @param collectors       采集人员
     * @param polygons         原始多边形
     * @param MBR              需要划分的 MBR
     * @param collectorToCells 采集人员-格网
     * @throws Exception Geohash 精度过低
     */
    private void assignCells(List<Worker> collectors,
                             List<com.esri.core.geometry.Polygon> polygons,
                             Envelope2D MBR,
                             Map<Worker, List<Polygon>> collectorToCells) throws Exception {
        // 划分网格
        // 如果划分出的格网数量小于外业人员数量，
        // 增加 Geohash 精度继续尝试划分
        List<Polygon> cells;
        while (true) {
            cells = generateCells(
                    polygons,
                    MBR.xmin,
                    MBR.xmax,
                    MBR.ymin,
                    MBR.ymax
            );
            if (cells.size() >= collectors.size()) {
                break;
            } else {
                this.precision++;
                if (this.precision > 12) {
                    throw new InvalidAssignmentStatus("无法找到合适的划分等级，请检查指定的工作区域是否过小");
                }
            }
        }

        // 尽可能平均地将网格分配给外业人员
        int workload = (int) Math.ceil((cells.size() * 1.0) / collectors.size()); // 每个外业人员负责的网格数量
        for (int i = 0; i < collectors.size(); i++) {
            int count = 0;
            List<Polygon> _cells = new LinkedList<>();
            for (int j = i * workload; j < cells.size() && count < workload; j++) {
                _cells.add(cells.get(j));
                count++;
            }
            collectorToCells.put(collectors.get(i), _cells);
        }
    }

    /**
     * 获取多边形的 MBR
     *
     * @param polygon 多边形
     * @return MBR
     */
    private Envelope2D getEnvelope(com.esri.core.geometry.Polygon polygon) {
        Envelope2D env = new Envelope2D();
        polygon.queryEnvelope2D(env);
        return env;
    }

    /**
     * 获取任务涉及到的所有网格
     * 如果该任务是不需要分配的，返回包含所有数据的外接矩形
     *
     * @param taskId 任务 ID
     */
    @RequestMapping("/info/cell/all")
    @ResponseBody
    public ResultMessage getTaskCell(@RequestParam("taskId")
                                     @NotNull(message = "必须上传任务 ID")
                                             int taskId) throws Exception {
        CustomFeatureCollection result = null;
        if (taskService.getTaskById(taskId).getIsAllocationOptional() == 1) {
            // 该任务无需分配，没有网格，所以直接获取包含所有数据的外接矩形
            Map<String, Map<Long, String>> tbToData = getIdAndGeometryData(taskId);
            if (!tbToData.isEmpty()) {
                double[] bbox = getBBoxOfData(tbToData);
                if (bbox == null) {
                    result = new CustomFeatureCollection(Collections.emptyList());
                } else {
                    double xmin = bbox[0];
                    double ymin = bbox[1];
                    double xmax = bbox[2];
                    double ymax = bbox[3];
                    Feature feature = new Feature();
                    feature.setGeometry(new org.geojson.Polygon(
                            new LngLatAlt(xmin, ymin),
                            new LngLatAlt(xmax, ymin),
                            new LngLatAlt(xmax, ymax),
                            new LngLatAlt(xmin, ymax),
                            new LngLatAlt(xmin, ymin))
                    );
                    feature.setProperty("count", tbToData.values().stream().filter(Objects::nonNull).mapToInt(Map::size).sum());
                    feature.setProperty("geohash", "");
                    feature.setProperty("users", Collections.emptyList());
                    result = new CustomFeatureCollection(Collections.singletonList(feature));
                }
            }
        } else {
            // 先查缓存
            String cellCacheKey = RedisKeyType.CELL.toString() + taskId;
            Object cachedCells = cacheService.get(cellCacheKey);
            if (cachedCells != null) {
                FeatureCollection collection = (FeatureCollection) objectMapper.readValue((String) cachedCells, GeoJsonObject.class);
                return new ResultMessage<>(true, "网格获取成功", new CustomFeatureCollection(collection.getFeatures()));
            }

            // 获取所有的网格 (geohash, geojson)
            List<TaskCell> cells = taskCellService.findByTaskId(taskId);
            Map<String, List<Worker>> cell2Workers = new HashMap<>();
            for (TaskCell cell : cells) {
                Worker dummy = new Worker();
                dummy.setGeojson(cell.getGeojson());
                List<Worker> workers = new ArrayList<>();
                workers.add(dummy);
                cell2Workers.put(cell.getGeohash(), workers);
            }
            // 获取工作人员，包含字段（id, username, work_role, cell_geohash）
            List<Worker> workers = taskUserCellService.findWorkerByTaskId(taskId);
            // 根据 geohash 字段进行连接
            for (Worker worker : workers) {
                for (TaskCell cell : cells) {
                    String geohash = worker.getGeohash();
                    if (geohash.equals(cell.getGeohash())) {
                        worker.setGeojson(cell.getGeojson());
                        cell2Workers.get(geohash).add(worker);
                        break;
                    }
                }
            }
            // 将数据转换成 GeoJSON
            List<Feature> features = new LinkedList<>();
            addFeature(features, cell2Workers, true, taskId);
            result = new CustomFeatureCollection(features);

            // 更新缓存
            FeatureCollection collection = new FeatureCollection();
            collection.addAll(result.getFeatures());
            // 设置缓存过期时间为一天
            cacheService.set(cellCacheKey, objectMapper.writeValueAsString(collection), 24 * 60 * 60);
        }

        return new ResultMessage<>(true, "成功获取所有网格", result);
    }

    /**
     * 计算包含所有数据的外接矩形
     *
     * @param tbToData: 表名和数据间的映射
     * @return (xmin, ymin, xmax, ymax)
     */
    private double[] getBBoxOfData(Map<String, Map<Long, String>> tbToData) {
        double xmin = Double.MAX_VALUE;
        double ymin = Double.MAX_VALUE;
        double xmax = Double.MIN_VALUE;
        double ymax = Double.MIN_VALUE;
        for (Map.Entry<String, Map<Long, String>> entry : tbToData.entrySet()) {
            Map<Long, String> idToJson = entry.getValue();
            if (idToJson != null && !idToJson.isEmpty()) {
                for (String json : idToJson.values()) {
                    org.locationtech.jts.geom.Geometry geometry = ArcGisGeoJsonParser.toGeometryObject(ShapeTypeUtil.changeGeometryToArcGIS(json));
                    if (geometry != null) {
                        org.locationtech.jts.geom.Geometry envelope = geometry.getEnvelope();
                        if (envelope instanceof Point) {
                            // If geometry is empty, envelope is an empty Point.
                            // If geometry is a point, envelope is a Point.
                            Point point = (Point) envelope;
                            if (!point.isEmpty()) {
                                xmin = Math.min(xmin, point.getX());
                                ymin = Math.min(ymin, point.getY());
                                xmax = Math.max(xmax, point.getX());
                                ymax = Math.max(ymax, point.getY());
                            }
                        } else if (envelope instanceof org.locationtech.jts.geom.LineString) {
                            // If geometry is a line parallel to an axis, envelope is a two-vertex LineString
                            org.locationtech.jts.geom.LineString lineString = (org.locationtech.jts.geom.LineString) envelope;
                            Point p1 = lineString.getStartPoint();
                            Point p2 = lineString.getEndPoint();
                            xmin = Math.min(xmin, Math.min(p1.getX(), p2.getX()));
                            ymin = Math.min(ymin, Math.min(p1.getY(), p2.getY()));
                            xmax = Math.max(xmax, Math.max(p1.getX(), p2.getX()));
                            ymax = Math.max(ymax, Math.max(p1.getY(), p2.getY()));
                        } else {
                            // otherwise, envelope a Polygon
                            // whose vertices are (minx miny, maxx miny, maxx maxy, minx maxy, minx miny)
                            org.locationtech.jts.geom.Polygon polygon = (org.locationtech.jts.geom.Polygon) envelope;
                            Coordinate[] coords = polygon.getCoordinates();
                            xmin = Math.min(xmin, coords[0].x);
                            ymin = Math.min(ymin, coords[0].y);
                            xmax = Math.max(xmax, coords[1].x);
                            ymax = Math.max(ymax, coords[2].y);
                        }
                    }
                }
            }
        }

        if (xmin == Double.MAX_VALUE || ymin == Double.MAX_VALUE || xmax == Double.MIN_VALUE || ymax == Double.MIN_VALUE) {
            return null;
        }

        // 最大坐标和最小坐标之间至少相差 0.0005
        double threshold = 0.0005;
        double halfThreshold = threshold / 2.0;
        if (xmax - xmin < threshold) {
            xmax += halfThreshold;
            xmin -= halfThreshold;
        }
        if (ymax - ymin < threshold) {
            ymax += halfThreshold;
            ymin -= halfThreshold;
        }
        return new double[]{xmin, ymin, xmax, ymax};
    }

    /**
     * 获取外业人员在任务中分配到的网格
     *
     * @param taskId 任务 ID
     * @param userId 外业人员 ID
     */
    @RequestMapping("/info/cell/user")
    @ResponseBody
    public ResultMessage getTaskCellByUser(@RequestParam("taskId")
                                           @NotNull(message = "必须上传任务 ID")
                                                   int taskId,
                                           @RequestParam("userId")
                                           @NotNull(message = "必须上传外业人员 ID")
                                                   int userId) throws Exception {
        // 以 Geohash 为 key 进行聚合
        Map<String, List<Worker>> cell2Worker = taskUserCellService
                .findWorkerByTaskIdAndUserId(taskId, userId)
                .stream()
                .collect(Collectors.groupingBy(Worker::getGeohash));
        List<Feature> features = new LinkedList<>();
        addFeature(features, cell2Worker, false, taskId);
        CustomFeatureCollection collection = new CustomFeatureCollection(features);
        return new ResultMessage<>(true, "成功获取网格", collection);
    }

    /**
     * 将网格和人员信息转换为 GeoJSON Feature
     *
     * @param features   Feature 列表
     * @param cell2Users key：Geohash，value：相关的人员列表
     * @throws Exception 无法解析 GeoJSON 字符串
     */
    private void addFeature(List<Feature> features, Map<String, List<Worker>> cell2Users,
                            boolean statisticsDataNum, Integer taskId) throws Exception {
        Task task = taskService.getTaskById(taskId);
        Map<String, Table> tables = JSON.toJavaObject(JSON.parseObject(task.getConfig()), Config.class).getRootTables();
        for (Map.Entry<String, List<Worker>> entry : cell2Users.entrySet()) {
            List<Worker> cellAndUsers = entry.getValue();
            String geojson = cellAndUsers.get(0).getGeojson();
            Feature feature;
            try {
                feature = objectMapper.readValue(geojson, Feature.class);
            } catch (IOException e) {
                log.error("无法解析 {}，应为标准的 GeoJSON Feature 格式", geojson);
                throw new Exception("解析 GeoJSON 数据时发生异常");
            }
            // 如果statisticsDataNum为true，统计每个GeoHash内的数据个数
            if (statisticsDataNum) {
                Long dataSum = 0L;
                String featureString = taskCellService.findGeojsonByTaskIdAndGeohash(taskId, entry.getKey()).get();
                GeometryType geometryType = JSON.toJavaObject(JSON.parseObject(featureString), cn.edu.whu.boot.collection.dto.Feature.class).getGeometry();
                for (String tableName : tables.keySet()) {
                    dataSum += baseService.getStatisticDataNumByGeoJson(PostgreSqlUtil.buildTableNameString(task.getDatabaseName(), tableName, false, false),
                            PostgreSqlUtil.transformString(CheckTableDataUtil.getGeometryFieldName(tables.get(tableName))),
                            JSON.toJSONString(geometryType));
                }
                feature.setProperty("count", dataSum);
            }
            // 设置属性
            feature.setProperty("geohash", entry.getKey());
            List<Map<String, Object>> users = new ArrayList<>();
            for (Worker user : cellAndUsers) {
                if (user.getId() == null) {
                    continue;
                }
                users.add(new HashMap<String, Object>() {{
                    put("userId", user.getId());
                    put("username", user.getUsername());
                    put("role", user.getRole());
                }});
            }
            feature.setProperty("users", users);
            features.add(feature);
        }
    }

    /**
     * <p>修改网格和外业人员的对应关系，上传的 JSON 参数格式为:
     * <pre>
     * {
     *    "geohash1": 1,
     *    "geohash2": 1,
     * }
     * </pre>
     * <p>表示将编号为 geohash1 和 geohash2 的网格分别分配给外业人员 1 和 外业人员 2
     *
     * @param taskId     任务 ID
     * @param jsonString 表示网格-人员对应关系的 JSON 字符串
     */
    @RequestMapping("/change/cell")
    @ResponseBody
    public ResultMessage modifyCellToWorker(@RequestParam("taskId") @NotNull(message = "必须上传任务 ID") int taskId, @RequestParam("change") @NotBlank(message = "网格和外业人员的对应关系不能为空") String jsonString) throws Exception {
        Map<String, Integer> cellToWorker = gson.fromJson(jsonString, new TypeToken<HashMap<String, Integer>>() {
        }.getType());

        // 获取网格和数据的对应关系，再以此找到人员和数据的对应关系
        Map<String, Map<Long, Integer>> dataToWorker = new HashMap<>();
        taskCellDataService.findByTaskIdAndCellGeohashIn(taskId, new LinkedList<>(cellToWorker.keySet())).forEach(it -> dataToWorker.computeIfAbsent(it.getTableName(), k -> new HashMap<>()).put(it.getDataId(), cellToWorker.get(it.getCellGeohash())));

        // 在数据库中更新网格-人员的对应关系和数据-人员的对应关系
        taskInfoService.updateCell2WorkerAndData2Worker(taskId, cellToWorker, dataToWorker);

        return new ResultMessage<>(true, "修改成功");
    }

    /**
     * <p>修改网格内的工作人员，上传的 JSON 参数格式为:
     * <pre>
     * {
     *    "1": "1",
     *    "2": "0",
     * }
     * </pre>
     * <p> 代表 (worker, role) 键值对，role 为 1 为审核人员，0 为 采集人员
     */
    @RequestMapping("/change/cell_worker")
    @ResponseBody
    public ResultMessage<?> changeWorkersInCell(@RequestParam("taskId") int taskId,
                                                @RequestParam("geohash") @NotBlank String geohash,
                                                @RequestParam("change") @NotBlank String jsonString) throws Exception {
        Map<Integer, Integer> worker2role = gson.fromJson(jsonString, new TypeToken<HashMap<Integer, Integer>>() {
        }.getType());

        // 1. 获取网格内的数据
        Map<String, Map<Long, String>> table2data = baseService.getIdAndGeojsonByGeohash(taskId, geohash);

        Map<String, Map<Long, Integer>> assignedData = null;
        if (table2data != null && !table2data.isEmpty()) {
            // 2. 获取网格内的审核人员
            List<Integer> inspectors = new ArrayList<>();
            for (Map.Entry<Integer, Integer> e : worker2role.entrySet()) {
                if (e.getValue().equals(1)) {
                    inspectors.add(e.getKey());
                }
            }

            // 将数据重新分配给审核人员
            assignedData = reassignData(table2data, inspectors);
        }

        taskInfoService.completeChangingWorkersInCell(taskId, geohash, worker2role.keySet(), assignedData);
        return new ResultMessage<>(true, "修改成功");
    }

    private Map<String, Map<Long, Integer>> reassignData(Map<String, Map<Long, String>> table2data, List<Integer> inspectors) {
        if (inspectors.isEmpty()) {
            // 3. 没有审核人员，将所有数据的审核人员设为空
            return assignDataToOne(-1, table2data);
        }

        // 4. 将数据分配给审核人员
        if (inspectors.size() == 1) {
            // 4.1 如果只有一个审核人员，将数据都分配给 TA
            return assignDataToOne(inspectors.get(0), table2data);
        }

        Map<String, Map<Long, Integer>> assignedData = new HashMap<>();
        // 4.2 计算所有数据的质心
        Map<Point, List<Pair<String, Long>>> centroid2data = new HashMap<>();
        for (Map.Entry<String, Map<Long, String>> e1 : table2data.entrySet()) {
            String table = e1.getKey();
            Map<Long, String> data2geojson = e1.getValue();
            if (data2geojson != null && !data2geojson.isEmpty()) {
                for (Map.Entry<Long, String> e2 : data2geojson.entrySet()) {
                    org.locationtech.jts.geom.Geometry geom = ArcGisGeoJsonParser.toGeometryObject(ShapeTypeUtil.changeGeometryToArcGIS(e2.getValue()));
                    if (geom != null) {
                        long dataId = e2.getKey();
                        centroid2data.computeIfAbsent(geom.getCentroid(), k -> new ArrayList<>()).add(new Pair<>(table, dataId));
                    }
                }
            }
        }
        List<Point> centroids = new ArrayList<>(centroid2data.keySet());
        if (centroids.size() < inspectors.size()) {
            // 4.2.1 质心数量少于审核人员数量，将数据随机分给所有审核人员
            int inspIdx = 0;
            int inspSize = inspectors.size();
            for (List<Pair<String, Long>> value : centroid2data.values()) {
                for (Pair<String, Long> p : value) {
                    String table = p.x;
                    long dataId = p.y;
                    assignedData.computeIfAbsent(table, k -> new HashMap<>()).put(dataId, inspectors.get(inspIdx));
                    inspIdx = (inspIdx + 1) % inspSize;
                }
            }
        } else {
            // 4.2.2 如果有多个质心且质心数量大于等于审核人员数量，先将质心聚类再分配给审核人员
            Map<Integer, List<Point>> clusters = KMeansClustering.clustering(centroids, inspectors.size());
            for (int i = 0; i < inspectors.size(); i++) {
                int inspId = inspectors.get(i);
                for (Point centroid : clusters.get(i)) {
                    for (Pair<String, Long> p : centroid2data.get(centroid)) {
                        String table = p.x;
                        long dataId = p.y;
                        assignedData.computeIfAbsent(table, k -> new HashMap<>()).put(dataId, inspId);
                    }
                }
            }
        }

        return assignedData;
    }

    // 将所有数据都分配给一个审核人员
    private Map<String, Map<Long, Integer>> assignDataToOne(int inspId, Map<String, Map<Long, String>> table2data) {
        Map<String, Map<Long, Integer>> assignedData = new HashMap<>();
        for (Map.Entry<String, Map<Long, String>> e : table2data.entrySet()) {
            String table = e.getKey();
            Map<Long, String> data2geojson = e.getValue();
            if (data2geojson != null && !data2geojson.isEmpty()) {
                Map<Long, Integer> data2insp = new HashMap<>();
                for (long dataId : data2geojson.keySet()) {
                    data2insp.put(dataId, inspId);
                }
                assignedData.put(table, data2insp);
            }
        }
        return assignedData;
    }

    /**
     * <p>修改数据和外业人员的对应关系，上传的 JSON 参数的格式为
     * <pre>
     * {
     *     "table1": {
     *         1: 1,
     *         2: 2
     *     }
     * }
     * </pre>
     * <p>表示将数据表 table1 下 ID 为 1 和 2 的数据分别分配给外业人员 1 和外业人员 2
     *
     * @param taskId     任务 ID
     * @param jsonString 表示数据-人员对应关系的 JSON 字符串
     */
    @RequestMapping("/change/data")
    @ResponseBody
    public ResultMessage modifyDataToWorker(@RequestParam("taskId") @NotNull(message = "必须上传任务 ID") int taskId, @RequestParam("change") @NotBlank(message = "数据和外业人员的对应关系不能为空") String jsonString) throws Exception {
        Map<String, Map<Long, Integer>> tbToDataAndWorker = gson.fromJson(jsonString, new TypeToken<HashMap<String, HashMap<Long, Integer>>>() {
        }.getType());
        if (tbToDataAndWorker == null || tbToDataAndWorker.isEmpty()) {
            return new ResultMessage<>(false, "参数为空");
        }

        // 在数据库中更新
        try {
            taskInfoService.updateCell2WorkerAndData2Worker(taskId, tbToDataAndWorker);
        } catch (InvalidAssignmentStatus e) {
            return new ResultMessage<>(false, e.getMessage());
        }
        return new ResultMessage<>(true, "修改成功");
    }

    /**
     * 获取某个任务下的所有外业人员
     *
     * @param taskId 任务 ID
     */
    @RequestMapping("/info/worker")
    @ResponseBody
    public ResultMessage getWorkerByTask(@RequestParam("taskId") int taskId) {
        List<Map<String, Object>> workers = taskUserRoleService
                .findWorkerByTaskId(taskId)
                .stream()
                .map(item -> (Map<String, Object>) new HashMap<String, Object>() {{
                    put("id", item.getId());
                    put("username", item.getUsername());
                    put("role", item.getRole());
                }})
                .collect(Collectors.toList());
        return new ResultMessage<>(true, "成功获取外业人员列表", workers);
    }

    /**
     * 检查某个外业人员在某个任务中是否有正在负责的网格
     * 或正在审核的数据
     *
     * @param taskId 任务 ID
     * @param userId 外业人员 ID
     * @param role   工作角色
     */
    @RequestMapping("/worker_status")
    @ResponseBody
    public ResultMessage checkWorkerStatus(@RequestParam("taskId")
                                                   int taskId,
                                           @RequestParam("userId")
                                                   int userId,
                                           @RequestParam("role")
                                                   int role) {
        // 检查该外业人员是否有负责的格网
        if (!taskUserCellService.findByTaskIdAndUserId(taskId, userId).isEmpty()) {
            return new ResultMessage<>(true, "该人员有正在负责的格网", true);
        }
        // 如果是审核人员，还要检查他是否有正在审核的数据
        if (role == 1) {
            // TODO: 检查该审核人员是否有正在审核的数据，如果有，则直接返回
        }
        return new ResultMessage<>(true, "该外业人员无任务", false);
    }

    /**
     * 管理任务的外业人员，上传的 append 参数表示新增的工作人员，格式为
     * <pre>
     * {
     *    "1": "1",
     *    "2": "0",
     * }
     * </pre>
     * <p> 代表 (worker, role) 键值对，role 为 1 为审核人员，0 为 采集人员
     * delete 参数是需要删除的人员，格式同上
     */
    @RequestMapping("/change/worker")
    @ResponseBody
    public ResultMessage<?> changeWorkers(@RequestParam("taskId") int taskId,
                                          @RequestParam("append") String jsonString1,
                                          @RequestParam("delete") String jsonString2) throws Exception {

        Map<Integer, Integer> appendWorker2role = (jsonString1 == null || jsonString1.isEmpty()) ? null : gson.fromJson(jsonString1, new TypeToken<HashMap<Integer, Integer>>() {
        }.getType());
        Map<Integer, Integer> deleteWorker2role = (jsonString2 == null || jsonString2.isEmpty()) ? null : gson.fromJson(jsonString2, new TypeToken<HashMap<Integer, Integer>>() {
        }.getType());
        if ((appendWorker2role == null || appendWorker2role.isEmpty()) && (deleteWorker2role == null || deleteWorker2role.isEmpty())) {
            return new ResultMessage<>(true, "修改成功");
        }

        taskInfoService.completeChangingWorkers(taskId, appendWorker2role, deleteWorker2role);
        return new ResultMessage<>(true, "修改成功");
    }

    /**
     * 将包含所有多边形的 MBR 划分为格网
     *
     * @param polygons 原始多边形
     * @return 划分得到的格网
     * @throws Exception Geohash 精度不在 1 到 12 位之间
     */
    private List<Polygon> generateCells(List<com.esri.core.geometry.Polygon> polygons,
                                        double xmin,
                                        double xmax,
                                        double ymin,
                                        double ymax) throws Exception {
        double[] cellSize = GeohashUtil.getCellSize(this.precision); // 单位为 km
        if (cellSize == null) {
            throw new Exception("Geohash 精度不在 1 到 12 位之间");
        }
        // 计算每个网格的经纬度跨度
        // Latitude: 1 deg = 110.574 km
        double latDelta = cellSize[1] / 110.574;
        // Longitude: 1 deg = 111.320*cos(latitude) km
        double lonDelta = cellSize[0] / (111.320 * Math.cos(((ymax + ymin) / 2.0) * Math.PI / 180.0));
        // 划分网格
        List<Polygon> cells = new LinkedList<>();
        int lonCount = (int) Math.ceil((xmax - xmin) / lonDelta); // 经度方向上的网格数量
        int latCount = (int) Math.ceil((ymax - ymin) / latDelta); // 纬度方向上的网格数量
        for (int i = 0; i < lonCount; i++) {
            double left = xmin + i * lonDelta;
            double right = left + lonDelta;
            for (int j = 0; j < latCount; j++) {
                double bottom = ymin + j * latDelta;
                double top = bottom + latDelta;
                // 创建一个矩形要素作为网格
                Polygon cell = geometryFactory.createPolygon(new Coordinate[]{new Coordinate(left, top), new Coordinate(right, top), new Coordinate(right, bottom), new Coordinate(left, bottom), new Coordinate(left, top)});
                // 如果该网格不与任何多边形相交，则略去
                if (polygons.stream().map(this::fromEsriPolygon).noneMatch(it -> it.intersects(cell))) {
                    continue;
                }
                cells.add(cell);
            }
        }
        return cells;
    }

    /**
     * 生成表示网格的 GeoJSON 字符串
     *
     * @param geometry   GeoJSON 对象列表
     * @param properties 需要添加的属性
     * @return GeoJSON 字符串
     */
    private String generateCellGeoJsonString(GeoJsonObject geometry, Map<String, Object> properties) {
        if (geometry == null) {
            return null;
        }
        Feature feature = new Feature();
        feature.setGeometry(geometry);
        Optional.ofNullable(properties).ifPresent(feature::setProperties);
        String jsonString = "";
        try {
            jsonString = objectMapper.writeValueAsString(feature);
        } catch (JsonProcessingException e) {
            log.error(e.getMessage());
        }
        return jsonString;
    }

    /**
     * 检查上传的文件，检查完成后将文件转换为 InputStream，用于进一步解析
     *
     * @throws Exception 文件名为空、不是 shp 文件
     */
    private InputStream resolveUploadedFile(MultipartFile shp) throws Exception {
        String filename = shp.getOriginalFilename();
        if (filename == null) {
            throw new InvalidShapefileException("文件名为空");
        }
        if (filename.endsWith(".shp")) {
            // 未压缩的 .shp 文件
            return shp.getInputStream();
        } else {
            throw new InvalidShapefileException("不支持的文件格式");
        }
    }

    /**
     * 解析 shp，读取其中的多边形
     *
     * @param inputStream shp 文件的输入流
     * @return shp 包含的多边形
     * @throws Exception 不包含多边形、包含多面
     */
    private List<com.esri.core.geometry.Polygon> readShp(InputStream inputStream) throws Exception {
        List<com.esri.core.geometry.Polygon> polygons = new LinkedList<>();
        ShapefileGeometryCursor cursor = new ShapefileGeometryCursor(inputStream);
        while (cursor.hasNext()) {
            Geometry next = cursor.next();
            if (!cursor.getGeometryType().equals(Geometry.Type.Polygon)) {
                continue;
            }
            com.esri.core.geometry.Polygon polygon = (com.esri.core.geometry.Polygon) next;
            OGCMultiPolygon ogcMultiPolygon = new OGCMultiPolygon(polygon, null);
            if (ogcMultiPolygon.numGeometries() > 1) {
                throw new InvalidShapefileException("上传的 shp 中包含 MultiPolygon");
            }
            polygons.add(polygon);
        }
        if (polygons.size() == 0) {
            throw new InvalidShapefileException("上传的 shp 中没有 Polygon");
        }
        return polygons;
    }

    /**
     * 将 com.esri.core.geometry.Polygon 类型转换为 org.locationtech.jts.geom.Polygon 类型
     */
    private org.locationtech.jts.geom.Polygon fromEsriPolygon(com.esri.core.geometry.Polygon esriPolygon) {
        // 获取外环
        List<Coordinate> shellCoordinates = Arrays
                .stream(getOutermostRings(esriPolygon).get(0).getCoordinates2D())
                .map(it -> new Coordinate(it.x, it.y))
                .collect(Collectors.toList());
        shellCoordinates.add(shellCoordinates.get(0));
        org.locationtech.jts.geom.LinearRing shell = geometryFactory
                .createLinearRing(shellCoordinates.toArray(new Coordinate[0]));
        // 获取内环
        org.locationtech.jts.geom.LinearRing[] holes = getAllInteriorRings(esriPolygon)
                .stream()
                .map(polygon -> {
                    List<Coordinate> holeCoordinates = Arrays
                            .stream(polygon.getCoordinates2D())
                            .map(it -> new Coordinate(it.x, it.y))
                            .collect(Collectors.toList());
                    holeCoordinates.add(holeCoordinates.get(0));
                    return geometryFactory.createLinearRing(holeCoordinates.toArray(new Coordinate[0]));
                })
                .toArray(org.locationtech.jts.geom.LinearRing[]::new);
        return geometryFactory.createPolygon(shell, holes);
    }

    private List<com.esri.core.geometry.Polygon> getOutermostRings(com.esri.core.geometry.Polygon polygon) {
        GeometryCursor outerRingsCursor = new SimpleGeometryCursor(new LinkedList<>(getAllOuterRings(polygon)));
        GeometryCursor unionCursor = OperatorUnion
                .local()
                .execute(outerRingsCursor, null, null);
        Geometry unionPoly = unionCursor.next();
        // Holes could have been produced during the union, so rerun just in case.
        return getAllOuterRings((com.esri.core.geometry.Polygon) unionPoly);
    }

    private List<com.esri.core.geometry.Polygon> getAllOuterRings(com.esri.core.geometry.Polygon polygon) {
        com.esri.core.geometry.Polygon simplePolygon = (com.esri.core.geometry.Polygon) OperatorSimplifyOGC
                .local()
                .execute(polygon, null, true, null);
        List<com.esri.core.geometry.Polygon> rings = new ArrayList<>();
        int n = simplePolygon.getPathCount();
        for (int i = 0; i < n; i++) {
            if (simplePolygon.calculateRingArea2D(i) <= 0) {
                continue;
            }
            com.esri.core.geometry.Polygon ring = new com.esri.core.geometry.Polygon();
            ring.addPath(simplePolygon, i, true);
            rings.add(ring);
        }
        return rings;
    }

    private List<com.esri.core.geometry.Polygon> getAllInteriorRings(com.esri.core.geometry.Polygon polygon) {
        com.esri.core.geometry.Polygon simplePolygon = (com.esri.core.geometry.Polygon) OperatorSimplifyOGC
                .local()
                .execute(polygon, null, true, null);
        List<com.esri.core.geometry.Polygon> rings = new ArrayList<>();
        int n = simplePolygon.getPathCount();
        for (int i = 0; i < n; i++) {
            if (simplePolygon.calculateRingArea2D(i) > 0) {
                continue;
            }
            com.esri.core.geometry.Polygon ring = new com.esri.core.geometry.Polygon();
            ring.addPath(simplePolygon, i, true);
            rings.add(ring);
        }
        return rings;
    }

    /**
     * 将 org.locationtech.jts.geom.Polygon 转为 org.geojson.Polygon
     */
    private org.geojson.Polygon toGeoJsonPolygon(Polygon polygon) {
        // 设置外环
        org.geojson.Polygon geojsonPolygon = new org.geojson.Polygon(
                Arrays
                        .stream(polygon.getExteriorRing().getCoordinates())
                        .map(p -> new LngLatAlt(p.getX(), p.getY()))
                        .toArray(LngLatAlt[]::new)
        );
        // 添加内环
        for (int i = 0; i < polygon.getNumInteriorRing(); i++) {
            geojsonPolygon.addInteriorRing(
                    Arrays
                            .stream(polygon.getInteriorRingN(i).getCoordinates())
                            .map(p -> new LngLatAlt(p.getX(), p.getY()))
                            .toArray(LngLatAlt[]::new)
            );
        }
        return geojsonPolygon;
    }

    /**
     * 主要是用于获取 taskId 对应任务数据库下所有数据的 id 和 geometry
     */
    private Map<String, Map<Long, String>> getIdAndGeometryData(Integer id) {
        Task task = taskService.getTaskById(id);
        Config config = JSON.toJavaObject(JSON.parseObject(task.getConfig()), Config.class);
        Map<String, Table> rootTables = config.getRootTables();
        Map<String, Map<Long, String>> result = new HashMap<>();
        for (String tableName : rootTables.keySet()) {
            Map<Long, String> resultMap = new HashMap<>();
            Map<String, Table> tables = config.getTables();
            Table table = tables.get(tableName);
            String primaryKey = TableOperationUtil.getPrimaryKey(table);
            String primaryKeyInDb = PostgreSqlUtil.buildColumnNameString(task.getDatabaseName(), tableName, primaryKey);
            String geometry = TableOperationUtil.getShapeFieldName(table);
            String geometryInDb = PostgreSqlUtil.buildColumnNameString(task.getDatabaseName(), tableName, geometry);
            String selectIdAndGeometryDataSql = MessageFormat.format("select {0},{1} from {2}",
                    primaryKeyInDb, geometryInDb,
                    PostgreSqlUtil.buildTableNameString(task.getDatabaseName(), tableName, false, false));
            List<Map<String, Object>> rootTableDataList = baseService.selectData(selectIdAndGeometryDataSql);
            if (rootTableDataList == null) {
                result.put(tableName, null);
                continue;
            }
            for (Map<String, Object> rootTableData : rootTableDataList) {
                resultMap.put(Long.parseLong(rootTableData.get(primaryKey).toString()),
                        rootTableData.get(geometry).toString());
            }
            result.put(tableName, resultMap);
        }
        return result;
    }
}

