MongoDB06 - MongoDB 地理空间

发布于:2025-06-30 ⋅ 阅读:(17) ⋅ 点赞:(0)

MongoDB06 - MongoDB 地理空间

一:地理空间数据基础

1:地理数据表示方式

MongoDB 支持两种主要的地理空间数据表示方式: GeoJSON 格式 & 传统坐标对[经度,纬度]

  • 经度在前:GeoJSON 规定坐标顺序为 [经度, 纬度]
  • 有效范围:经度:-180 到 180 & 纬度:-90 到 90
1.1:GeoJSON 格式

GeoJSON支持如下的类型:

Point - 点

{ 
    type: "Point", 
    coordinates: [longitude, latitude] 
}

LineString - 线

{ 
    type: "LineString", 
    coordinates: [[lon1,lat1], [lon2,lat2], ...] 
}

Polygon - 多边形(闭合环)

{ type: "Polygon", coordinates: [
  [[lon1,lat1], [lon2,lat2], ..., [lon1,lat1]] // 外环
  // 可以有多个内环(洞)
]}

还有一些不太常用的:MultiPoint、MultiLineString、MultiPolygon、GeometryCollection

1.2:传统坐标对

简单数组格式:[longitude, latitude] -> 仅适用于 2d 索引,不支持复杂几何形状

{
    loc: [longitude, latitude]
}

2:地理空间索引

2.1:2dsphere 索引

地球球面几何计算,支持所有的 GeoJSON 类型,计算球面距离

支持 $nearSphere、$geoWithin、$geoIntersects 等操作

db.collection.createIndex(
    { <locationField>: "2dsphere" } // 创建索引
)
2.2:2d索引

平面几何计算,仅支持点数据(坐标对),计算平面距离(不考虑地球曲率)。·性能更高但精度较低

db.collection.createIndex(
    { <locationField>: "2d" }
)
2.3:混合索引

可以组合地理空间索引与其他字段:

db.places.createIndex(
    { location: "2dsphere", name: 1 }
)

二:地理空间查询和聚合

1:完全包含于几何图形

$geoWithin -> 查找完全包含在指定几何图形内的文档

  • 使用 GeoJSON:$geometry
  • 使用传统坐标:$box$polygon$center$centerSphere
db.places.find({
  location: {
    $geoWithin: {
      // 返回在如下多边形中的文档
      $geometry: {
        type: "Polygon",
        coordinates: [[ [0,0], [3,6], [6,1], [0,0] ]
      }
    }
  }
})

2:与指定几何图形相交

$geoIntersects -> 查找与指定几何图形相交的文档

db.places.find({
  location: {
    $geoIntersects: {
      // 返回和这条线相交的文档
      $geometry: {
        type: "LineString",
        coordinates: [[0,0], [5,5]]
      }
    }
  }
})

3:找附近点并按距离排序

$near -> 找附近点并按距离排序

db.places.find({
  location: {
    $near: {
      // 找到举例给定中心点附近的点,最小距离100m,最大距离500m
      $geometry: {
        type: "Point",
        coordinates: [-73.9667, 40.78]
      },
      $maxDistance: 500,  // 米(2dsphere)
      $minDistance: 100
    }
  }
})

4:地理空间的聚合操作

使用$geoNear是基于距离的管道聚合,对于$geoNear有如下的说明:

  • 必须是管道的第一阶段
  • 自动按距离排序
  • 可返回计算的距离值
db.places.aggregate([
  {
    $geoNear: {
      // near:参考点
      // distanceField:存储距离的字段
      // maxDistance/minDistance:距离范围
      // spherical:是否使用球面计算
      // query:附加查询条件
      // $geoWithin 可在聚合中使用
      // 结合 $project 计算自定义地理空间数据
      near: { type: "Point", coordinates: [-73.9667, 40.78] },
      distanceField: "distance",
      maxDistance: 2000,
      spherical: true,
      query: { category: "Park" }
    }
  }
])

5:地理空间计算函数

$geoDistance:计算两点间距离

{
    $project: {
        distance: {
            $geoDistance: {
                // 起点是文档中的location字段
                start: "$location",
                // 终点是GeoJson的坐标点
                end: { type: "Point", coordinates: [-73.98, 40.77] },
                distanceMultiplier: 0.001 // 转换为公里
            }
        }
    }
}

三:实际应用示例

1:附近地点搜索

db.places.find({
    location: {
        $nearSphere: {
            // 在给定点1km之内的文档
            $geometry: {
                type: "Point",
                coordinates: [currentLng, currentLat]
            },
            $maxDistance: 1000 // 1公里内
        }
    },
    category: "restaurant"
}).limit(20)

2:地理围栏检查

// 检查点是否在配送区域内
db.deliveryZones.find({
    area: {
        $geoIntersects: {
            $geometry: {
                type: "Point",
                coordinates: [orderLng, orderLat]
            }
        }
    }
})

3:多点距离计算

db.stores.aggregate([
    {
        $geoNear: {
            // 指定参考点
            near: { type: "Point", coordinates: [userLng, userLat] },
            // 指定输出字段名,用于存储计算的距离值
            distanceField: "distance",
            // 使用球面
            spherical: true
        }
    },
    // 结果过滤,只要5km内的
    { $match: { distance: { $lte: 5000 } } }, // 5公里内
    // 按照距离正序排序
    { $sort: { distance: 1 } }
])

四:Spring Boot Data整合

1:依赖和配置

<dependencies>
    <!-- Spring Boot Starter Data MongoDB -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-mongodb</artifactId>
    </dependency>
    
    <!-- 其他必要依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- Lombok (可选) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>
# application.properties
spring:
	data:
		mongodb:
			host: localhost
			port: 27017
			database: geo_db

2:数据模型定义

创建地理空间实体

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
import org.springframework.data.mongodb.core.mapping.Document;
import lombok.Data;

@Data
@Document(collection = "places")
public class Place {
    @Id
    private String id;
    
    private String name;
    private String category;
    private GeoJsonPoint location;  // 使用GeoJsonPoint存储地理坐标
    
    // 构造方法、getter/setter等
    // Lombok的@Data注解会自动生成这些
}

地理空间索引配置

import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.index.GeoSpatialIndexType;
import org.springframework.data.mongodb.core.index.GeoSpatialIndexed;
import org.springframework.data.mongodb.core.index.IndexDefinition;
import org.springframework.data.mongodb.core.index.IndexOperations;
import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener;
import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent;

@Configuration
public class MongoConfig {
    
    // 添加监听器
    @Bean
    public BeforeConvertListener beforeConvertListener() {
        return new BeforeConvertListener();
    }
    
    // 定义监听器,继承地理事件监听器
    public static class BeforeConvertListener extends AbstractMongoEventListener<Place> {
        @Override
        public void onBeforeConvert(BeforeConvertEvent<Place> event) {
            // 说明要进行索引操作了
            IndexOperations indexOps = event.getCollection().getIndexOperations();
            
            // 定义索引
            // 确保location字段有2dsphere索引
            IndexDefinition indexDef = 
                new GeoSpatialIndexDefinition("location").typed(GeoSpatialIndexType.GEO_2DSPHERE);
            
            // 添加索引
            indexOps.ensureIndex(indexDef);
        }
    }
}

3:Repository 层

import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Point;
import org.springframework.data.mongodb.repository.Query;
import java.util.List;

public interface PlaceRepository extends MongoRepository<Place, String> {
    
    // 查找附近的点(按距离排序)
    List<Place> findByLocationNear(Point point, Distance distance);
    
    // 查找指定类别附近的点
    List<Place> findByCategoryAndLocationNear(String category, Point point, Distance distance);
    
    // 使用GeoJSON多边形查询
    @Query("{ 'location' : { $geoWithin : { $geometry : { type : 'Polygon', coordinates : ?0 } } }")
    List<Place> findWithinPolygon(List<List<Double[]>> polygonCoordinates);
    
    // 查找与指定线相交的地点
    @Query("{ 'location' : { $geoIntersects : { $geometry : { type : 'LineString', coordinates : ?0 } } }")
    List<Place> findIntersectingLine(List<Double[]> lineCoordinates);
    
    // 如果是聚合查询
    @Aggregation(pipeline = {
        "{ $geoNear: { " +
        "  near: { type: 'Point', coordinates: [ ?0, ?1 ] }, " +
        "  distanceField: 'distance', " +
        "  maxDistance: ?2, " +
        "  spherical: true " +
        "} }",
        "{ $match: { category: ?3 } }",
        "{ $sort: { distance: 1 } }",
        "{ $limit: ?4 }"
    })
    List<Place> findNearbyPlacesWithAggregation(
        double longitude, 
        double latitude, 
        double maxDistanceInMeters,
        String category,
        int limit);
}

4:服务Service层

import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Metrics;
import org.springframework.data.geo.Point;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class GeoService {
    
    private final PlaceRepository placeRepository;
    
    public GeoService(PlaceRepository placeRepository) {
        this.placeRepository = placeRepository;
    }
    
    /**
     * 查找附近的地点
     * @param longitude 经度
     * @param latitude 纬度
     * @param distance 距离(公里)
     * @return 附近的地点列表
     */
    public List<Place> findNearbyPlaces(double longitude, double latitude, double distance) {
        Point point = new Point(longitude, latitude);
        Distance radius = new Distance(distance, Metrics.KILOMETERS);
        return placeRepository.findByLocationNear(point, radius);
    }
    
    /**
     * 查找特定类别附近的地点
     */
    public List<Place> findNearbyPlacesByCategory(
        double longitude, double latitude, double distance, String category) {
        
        Point point = new Point(longitude, latitude);
        Distance radius = new Distance(distance, Metrics.KILOMETERS);
        return placeRepository.findByCategoryAndLocationNear(category, point, radius);
    }
    
    /**
     * 多边形区域查询
     */
    public List<Place> findWithinPolygon(List<List<Double[]>> polygonCoordinates) {
        return placeRepository.findWithinPolygon(polygonCoordinates);
    }
    
    /**
     * 与线相交的地点查询
     */
    public List<Place> findIntersectingLine(List<Double[]> lineCoordinates) {
        return placeRepository.findIntersectingLine(lineCoordinates);
    }
    
    // 使用 GeoJson 对象
    public List<Place> findWithinGeoJsonPolygon(GeoJsonPolygon polygon) {
    return mongoTemplate.find(
        Query.query(Criteria.where("location").within(polygon)), 
        Place.class
    );
}

5:控制Controller层

import org.springframework.web.bind.annotation.*;
import org.springframework.data.geo.Point;
import java.util.List;

@RestController
@RequestMapping("/api/places")
public class PlaceController {
    
    private final GeoService geoService;
    
    public PlaceController(GeoService geoService) {
        this.geoService = geoService;
    }
    
    @GetMapping("/nearby")
    public List<Place> getNearbyPlaces(
        @RequestParam double longitude,
        @RequestParam double latitude,
        @RequestParam(defaultValue = "5") double distance) {
        
        return geoService.findNearbyPlaces(longitude, latitude, distance);
    }
    
    @GetMapping("/nearby/{category}")
    public List<Place> getNearbyPlacesByCategory(
        @PathVariable String category,
        @RequestParam double longitude,
        @RequestParam double latitude,
        @RequestParam(defaultValue = "5") double distance) {
        
        return geoService.findNearbyPlacesByCategory(longitude, latitude, distance, category);
    }
    
    @PostMapping("/within-polygon")
    public List<Place> getPlacesWithinPolygon(@RequestBody List<List<Double[]>> polygonCoordinates) {
        return geoService.findWithinPolygon(polygonCoordinates);
    }
    
    @PostMapping("/intersecting-line")
    public List<Place> getPlacesIntersectingLine(@RequestBody List<Double[]> lineCoordinates) {
        return geoService.findIntersectingLine(lineCoordinates);
    }
}