Files
smart-crop-ui/src/GEOMETRY_MAP_PICKER_IMPLEMENTATION.md

12 KiB
Raw Blame History

🔧 几何计算工具 - 地图选点实现指南

📋 实现概述

本文档说明如何在几何计算工具中集成地图选点功能。


已完成的工作

1. 创建MapPointPicker组件

文件位置: /components/field/MapPointPicker.tsx

功能:

  • 高德地图集成
  • 多边形和单点两种模式
  • 标记管理和多边形绘制
  • 点击添加/删除坐标点
  • 坐标列表显示

2. 更新状态管理

在 FieldSpatialQuery.tsx 中添加:

// 地图选点模式
const [mapPickMode, setMapPickMode] = useState<'polygon' | 'distance-p1' | 'distance-p2' | null>(null);
const [editingPointIndex, setEditingPointIndex] = useState<number | null>(null);

3. 导入组件

import { MapPointPicker } from './MapPointPicker';
import { MousePointer2, Edit3 } from 'lucide-react';

🔨 需要手动完成的集成

由于文件中存在重复的TabsContent结构需要手动修改。以下是集成步骤

步骤1: 修改面积计算TabsContent

找到这部分代码约第1470行

{/* 面积计算 */}
<TabsContent value="area" className="space-y-4">
  <Card className="p-6">
    <div className="flex items-center justify-between mb-4">
      <h3>多边形坐标点</h3>
      <Button size="sm" onClick={handleAddGeomPoint}>
        <Plus className="w-4 h-4 mr-2" />
        添加点
      </Button>
    </div>
    
    <div className="space-y-3 max-h-96 overflow-y-auto">
      {geomPoints.map((point, index) => (
        // ... 坐标输入框
      ))}
    </div>

    <Button onClick={handleCalculateArea} ...>
      计算面积 (ST_Area)
    </Button>
  </Card>
  
  {/* 结果显示 */}
</TabsContent>

替换为:

{/* 面积计算 */}
<TabsContent value="area" className="space-y-4">
  {/* 选择方式切换 */}
  <div className="flex gap-2">
    <Button
      variant={mapPickMode === 'polygon' ? 'default' : 'outline'}
      onClick={() => setMapPickMode(mapPickMode === 'polygon' ? null : 'polygon')}
      className="flex-1"
    >
      <MousePointer2 className="w-4 h-4 mr-2" />
      {mapPickMode === 'polygon' ? '正在使用地图选点' : '地图选点'}
    </Button>
    <Button
      variant={mapPickMode === null ? 'default' : 'outline'}
      onClick={() => setMapPickMode(null)}
      className="flex-1"
    >
      <Edit3 className="w-4 h-4 mr-2" />
      手动输入坐标
    </Button>
  </div>

  {mapPickMode === 'polygon' ? (
    /* 地图选点模式 */
    <MapPointPicker
      points={geomPoints}
      mode="polygon"
      onPointsChange={setGeomPoints}
      height="400px"
      title="在地图上选择多边形顶点"
    />
  ) : (
    /* 手动输入模式 */
    <Card className="p-6">
      <div className="flex items-center justify-between mb-4">
        <h3>多边形坐标点</h3>
        <Button size="sm" onClick={handleAddGeomPoint}>
          <Plus className="w-4 h-4 mr-2" />
          添加点
        </Button>
      </div>
      
      <div className="space-y-3 max-h-96 overflow-y-auto">
        {geomPoints.map((point, index) => (
          <div key={index} className="flex items-center gap-2">
            <Badge variant="outline" className="w-16">
               {index + 1}
            </Badge>
            <Input
              type="number"
              step="0.000001"
              value={point.lat}
              onChange={(e) => handleUpdateGeomPoint(index, 'lat', e.target.value)}
              placeholder="纬度"
              className="flex-1"
            />
            <Input
              type="number"
              step="0.000001"
              value={point.lng}
              onChange={(e) => handleUpdateGeomPoint(index, 'lng', e.target.value)}
              placeholder="经度"
              className="flex-1"
            />
            <Button
              size="sm"
              variant="ghost"
              onClick={() => handleRemoveGeomPoint(index)}
              disabled={geomPoints.length <= 3}
            >
              <Trash2 className="w-4 h-4 text-red-500" />
            </Button>
          </div>
        ))}
      </div>
    </Card>
  )}

  <Button 
    onClick={handleCalculateArea}
    className="w-full bg-green-600 hover:bg-green-700"
  >
    <Calculator className="w-4 h-4 mr-2" />
    计算面积 (ST_Area)
  </Button>
  
  {/* 结果显示部分保持不变 */}
  {geomResult && geomResult.type === 'area' && (
    // ... 原有的结果显示代码
  )}
</TabsContent>

步骤2: 修改周长计算TabsContent

使用相同的模式修改周长计算部分约第1550行

步骤3: 修改中心点计算TabsContent

使用相同的模式修改中心点计算部分约第1610行

步骤4: 修改包围盒计算TabsContent

使用相同的模式修改包围盒计算部分(最后一个多边形计算)

步骤5: 修改距离计算TabsContent

找到距离计算部分:

{/* 距离计算 */}
<TabsContent value="distance" className="space-y-4">
  <Card className="p-6">
    <h3 className="mb-4">两点坐标</h3>
    
    <div className="space-y-4">
      <div>
        <div className="mb-2 font-medium">1坐标</div>
        <div className="grid grid-cols-2 gap-3">
          <div>
            <label className="text-sm text-muted-foreground">纬度</label>
            <Input ... />
          </div>
          <div>
            <label className="text-sm text-muted-foreground">经度</label>
            <Input ... />
          </div>
        </div>
      </div>
      
      {/* 点2坐标 - 类似结构 */}
    </div>
  </Card>
</TabsContent>

替换为:

{/* 距离计算 */}
<TabsContent value="distance" className="space-y-4">
  {/* 点1选择 */}
  <Card className="p-6">
    <h3 className="mb-4">1坐标</h3>
    
    {/* 切换按钮 */}
    <div className="flex gap-2 mb-4">
      <Button
        variant={mapPickMode === 'distance-p1' ? 'default' : 'outline'}
        onClick={() => setMapPickMode(mapPickMode === 'distance-p1' ? null : 'distance-p1')}
        className="flex-1"
      >
        <MousePointer2 className="w-4 h-4 mr-2" />
        {mapPickMode === 'distance-p1' ? '正在使用地图选点' : '地图选点'}
      </Button>
      <Button
        variant={mapPickMode === null || mapPickMode === 'distance-p2' ? 'default' : 'outline'}
        onClick={() => setMapPickMode(null)}
        className="flex-1"
      >
        <Edit3 className="w-4 h-4 mr-2" />
        手动输入坐标
      </Button>
    </div>

    {mapPickMode === 'distance-p1' ? (
      <MapPointPicker
        points={[distPoint1]}
        mode="single"
        onPointsChange={(points) => setDistPoint1(points[0] || distPoint1)}
        height="300px"
        title="在地图上选择点1位置"
      />
    ) : (
      <div className="grid grid-cols-2 gap-3">
        <div>
          <label className="text-sm text-muted-foreground">纬度</label>
          <Input
            type="number"
            step="0.000001"
            value={distPoint1.lat}
            onChange={(e) => setDistPoint1({ ...distPoint1, lat: parseFloat(e.target.value) || 0 })}
          />
        </div>
        <div>
          <label className="text-sm text-muted-foreground">经度</label>
          <Input
            type="number"
            step="0.000001"
            value={distPoint1.lng}
            onChange={(e) => setDistPoint1({ ...distPoint1, lng: parseFloat(e.target.value) || 0 })}
          />
        </div>
      </div>
    )}
  </Card>

  {/* 点2选择 - 类似结构,使用 mapPickMode === 'distance-p2' 和 distPoint2 */}
  <Card className="p-6">
    <h3 className="mb-4">2坐标</h3>
    
    <div className="flex gap-2 mb-4">
      <Button
        variant={mapPickMode === 'distance-p2' ? 'default' : 'outline'}
        onClick={() => setMapPickMode(mapPickMode === 'distance-p2' ? null : 'distance-p2')}
        className="flex-1"
      >
        <MousePointer2 className="w-4 h-4 mr-2" />
        {mapPickMode === 'distance-p2' ? '正在使用地图选点' : '地图选点'}
      </Button>
      <Button
        variant={mapPickMode === null || mapPickMode === 'distance-p1' ? 'default' : 'outline'}
        onClick={() => setMapPickMode(null)}
        className="flex-1"
      >
        <Edit3 className="w-4 h-4 mr-2" />
        手动输入坐标
      </Button>
    </div>

    {mapPickMode === 'distance-p2' ? (
      <MapPointPicker
        points={[distPoint2]}
        mode="single"
        onPointsChange={(points) => setDistPoint2(points[0] || distPoint2)}
        height="300px"
        title="在地图上选择点2位置"
      />
    ) : (
      <div className="grid grid-cols-2 gap-3">
        <div>
          <label className="text-sm text-muted-foreground">纬度</label>
          <Input
            type="number"
            step="0.000001"
            value={distPoint2.lat}
            onChange={(e) => setDistPoint2({ ...distPoint2, lat: parseFloat(e.target.value) || 0 })}
          />
        </div>
        <div>
          <label className="text-sm text-muted-foreground">经度</label>
          <Input
            type="number"
            step="0.000001"
            value={distPoint2.lng}
            onChange={(e) => setDistPoint2({ ...distPoint2, lng: parseFloat(e.target.value) || 0 })}
          />
        </div>
      </div>
    )}
  </Card>

  <Button 
    onClick={handleCalculateDistance}
    className="w-full bg-orange-600 hover:bg-orange-700"
  >
    <MapIcon className="w-4 h-4 mr-2" />
    计算距离 (ST_Distance)
  </Button>
  
  {/* 结果显示部分保持不变 */}
</TabsContent>

📝 修改清单

需要修改的5个TabsContent

  • 面积计算 (area) - 多边形模式
  • 周长计算 (perimeter) - 多边形模式
  • 中心点计算 (centroid) - 多边形模式
  • 包围盒计算 (bbox) - 多边形模式
  • 距离计算 (distance) - 双点模式

🎯 关键要点

1. 模式切换按钮

<Button
  variant={mapPickMode === 'polygon' ? 'default' : 'outline'}
  onClick={() => setMapPickMode(mapPickMode === 'polygon' ? null : 'polygon')}
>
  地图选点
</Button>

2. 条件渲染

{mapPickMode === 'polygon' ? (
  <MapPointPicker ... />
) : (
  <Card>手动输入</Card>
)}

3. MapPointPicker属性

<MapPointPicker
  points={geomPoints}           // 坐标数组
  mode="polygon"                 // 或 "single"
  onPointsChange={setGeomPoints} // 更新回调
  height="400px"                 // 地图高度
  title="选择坐标点"             // 标题
/>

测试检查

完成集成后,测试以下功能:

面积计算

  • 切换到地图选点模式
  • 在地图上点击添加4个点
  • 验证多边形自动绘制
  • 点击标记删除点
  • 切换回手动输入模式
  • 验证坐标保留
  • 计算面积成功

周长计算

  • 使用相同测试流程

中心点计算

  • 使用相同测试流程

距离计算

  • 点1使用地图选择
  • 点2使用地图选择
  • 验证两点标记显示
  • 计算距离成功

包围盒计算

  • 使用相同测试流程

🐛 可能的问题

问题1: 地图未显示

原因: 高德地图SDK未加载 解决: 检查mapLoader.ts是否正常工作

问题2: 点击地图无反应

原因: 事件监听未生效 解决: 检查MapPointPicker组件的useEffect

问题3: 标记位置不准

原因: 经纬度顺序错误 解决: 高德地图使用[lng, lat]顺序

问题4: 多边形未绘制

原因: 点数不足3个 解决: 至少添加3个点才能形成多边形


🎉 完成后的效果

用户可以:

  1. 灵活选择输入方式

    • 地图直观选点
    • 手动精确输入
    • 随时切换
  2. 实时可视化反馈

    • 标记即时显示
    • 多边形自动绘制
    • 坐标实时更新
  3. 便捷操作

    • 点击添加
    • 点击删除
    • 一键清除
  4. 保留原有功能

    • 所有计算功能不变
    • 精度保持不变
    • 结果显示不变

📚 相关文件

  • /components/field/MapPointPicker.tsx - 地图选点组件
  • /components/field/FieldSpatialQuery.tsx - 空间数据管理主组件
  • /lib/spatialDataService.ts - 空间计算服务
  • /lib/mapLoader.ts - 地图SDK加载器

创建日期: 2025-10-18
版本: v4.0
状态: 组件已创建,需手动集成
维护团队: 智慧农业研发中心