智慧水务管理系统 - 精河县供水工程综合管理平台

test_gis_spatial.py 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. """
  2. GIS空间查询单元测试
  3. 覆盖设备定位、区域分析和路径规划功能
  4. """
  5. import unittest
  6. from unittest.mock import Mock, patch
  7. from shapely.geometry import Point, Polygon, LineString
  8. from shapely.ops import unary_union
  9. import json
  10. from datetime import datetime
  11. from src.gis.models import DeviceLocation, GeoRegion, PathRoute
  12. from src.gis.services import SpatialQueryService, RoutePlanningService, GeoAnalysisService
  13. class TestDeviceLocation(unittest.TestCase):
  14. """设备位置模型测试"""
  15. def setUp(self):
  16. self.location_data = {
  17. "device_id": "sensor_001",
  18. "name": "温度传感器1",
  19. "location": {
  20. "type": "Point",
  21. "coordinates": [108.948024, 34.265773] # 西安坐标
  22. },
  23. "location_type": "outdoor",
  24. "installation_date": "2026-01-15",
  25. "last_updated": "2026-06-16T10:00:00Z"
  26. }
  27. def test_location_creation(self):
  28. """测试位置创建"""
  29. location = DeviceLocation(self.location_data)
  30. self.assertEqual(location.device_id, self.location_data["device_id"])
  31. self.assertEqual(location.name, self.location_data["name"])
  32. self.assertEqual(location.location_type, "outdoor")
  33. # 验证几何对象
  34. self.assertIsInstance(location.geometry, Point)
  35. self.assertEqual(location.geometry.x, 108.948024)
  36. self.assertEqual(location.geometry.y, 34.265773)
  37. def test_location_validation(self):
  38. """测试位置验证"""
  39. location = DeviceLocation(self.location_data)
  40. result = location.validate()
  41. self.assertTrue(result["valid"])
  42. # 测试无效位置(坐标格式错误)
  43. invalid_data = self.location_data.copy()
  44. invalid_data["location"]["coordinates"] = [108.948024] # 缺少Y坐标
  45. invalid_location = DeviceLocation(invalid_data)
  46. result = invalid_location.validate()
  47. self.assertFalse(result["valid"])
  48. def test_distance_calculation(self):
  49. """测试距离计算"""
  50. location1 = DeviceLocation(self.location_data)
  51. # 创建第二个位置
  52. location2_data = self.location_data.copy()
  53. location2_data["device_id"] = "sensor_002"
  54. location2_data["location"]["coordinates"] = [108.950024, 34.267773] # 附近位置
  55. location2 = DeviceLocation(location2_data)
  56. distance = location1.calculate_distance(location2)
  57. self.assertGreater(distance, 0)
  58. self.assertLess(distance, 1000) # 应该在1公里内
  59. class TestGeoRegion(unittest.TestCase):
  60. """地理区域模型测试"""
  61. def setUp(self):
  62. self.region_data = {
  63. "region_id": "area_001",
  64. "name": "测试区域",
  65. "boundary": {
  66. "type": "Polygon",
  67. "coordinates": [[[108.94, 34.26], [108.95, 34.26], [108.95, 34.27], [108.94, 34.27], [108.94, 34.26]]]
  68. },
  69. "properties": {
  70. "type": "residential",
  71. "population_density": "medium",
  72. "water_supply_network": "primary"
  73. }
  74. }
  75. def test_region_creation(self):
  76. """测试区域创建"""
  77. region = GeoRegion(self.region_data)
  78. self.assertEqual(region.region_id, self.region_data["region_id"])
  79. self.assertEqual(region.name, self.region_data["name"])
  80. self.assertEqual(region.properties["type"], "residential")
  81. # 验证几何对象
  82. self.assertIsInstance(region.geometry, Polygon)
  83. self.assertTrue(region.geometry.is_valid)
  84. def test_contains_point(self):
  85. """测试点包含测试"""
  86. region = GeoRegion(self.region_data)
  87. # 区域内的点
  88. inside_point = Point(108.945, 34.265)
  89. self.assertTrue(region.contains_point(inside_point))
  90. # 区域外的点
  91. outside_point = Point(109.0, 34.3)
  92. self.assertFalse(region.contains_point(outside_point))
  93. def test_region_intersection(self):
  94. """测试区域相交"""
  95. region1 = GeoRegion(self.region_data)
  96. # 创建第二个相交区域
  97. region2_data = self.region_data.copy()
  98. region2_data["region_id"] = "area_002"
  99. region2_data["boundary"]["coordinates"] = [[[108.945, 34.265], [108.955, 34.265], [108.955, 34.275], [108.945, 34.275], [108.945, 34.265]]]
  100. region2 = GeoRegion(region2_data)
  101. intersection = region1.intersection(region2)
  102. self.assertIsNotNone(intersection)
  103. self.assertIsInstance(intersection, Polygon)
  104. self.assertGreater(intersection.area, 0)
  105. def test_buffer_creation(self):
  106. """测试缓冲区创建"""
  107. region = GeoRegion(self.region_data)
  108. # 创建500米缓冲区
  109. buffered_region = region.create_buffer(500)
  110. self.assertIsInstance(buffered_region, Polygon)
  111. self.assertGreater(buffered_region.area, region.area)
  112. # 创建负缓冲区(收缩)
  113. shrunk_region = region.create_buffer(-200)
  114. self.assertIsInstance(shrunk_region, Polygon)
  115. self.assertLess(shrunk_region.area, region.area)
  116. class TestSpatialQueryService(unittest.TestCase):
  117. """空间查询服务测试"""
  118. def setUp(self):
  119. self.spatial_service = SpatialQueryService()
  120. @patch('src.gis.services.DeviceLocation')
  121. def test_find_devices_in_region(self, mock_device_location):
  122. """测试查找区域内的设备"""
  123. region_data = {
  124. "region_id": "area_001",
  125. "boundary": {
  126. "type": "Polygon",
  127. "coordinates": [[[108.94, 34.26], [108.95, 34.26], [108.95, 34.27], [108.94, 34.27], [108.94, 34.26]]]
  128. }
  129. }
  130. region = GeoRegion(region_data)
  131. # 模拟设备
  132. device1 = Mock()
  133. device1.geometry = Point(108.945, 34.265)
  134. device1.device_id = "sensor_001"
  135. device2 = Mock()
  136. device2.geometry = Point(109.0, 34.3) # 区域外
  137. device2.device_id = "sensor_002"
  138. mock_device_location.query.return_value = [device1, device2]
  139. devices = self.spatial_service.find_devices_in_region(region)
  140. self.assertEqual(len(devices), 1)
  141. self.assertEqual(devices[0].device_id, "sensor_001")
  142. @patch('src.gis.services.DeviceLocation')
  143. def test_find_devices_within_distance(self, mock_device_location):
  144. """测试查找指定距离内的设备"""
  145. center_point = Point(108.948024, 34.265773)
  146. max_distance = 1000 # 1公里
  147. # 模拟设备
  148. devices = []
  149. for i in range(5):
  150. device = Mock()
  151. # 创建不同距离的点
  152. distance = i * 200 # 0, 200, 400, 600, 800米
  153. device.geometry = Point(center_point.x + distance/100000, center_point.y + distance/100000)
  154. device.device_id = f"sensor_{i:03d}"
  155. devices.append(device)
  156. mock_device_location.query.return_value = devices
  157. nearby_devices = self.spatial_service.find_devices_within_distance(center_point, max_distance)
  158. # 应该找到距离小于1000米的设备(前4个)
  159. self.assertEqual(len(nearby_devices), 4)
  160. self.assertNotIn("sensor_004", [d.device_id for d in nearby_devices])
  161. @patch('src.gis.services.GeoRegion')
  162. def test_analyze_coverage(self, mock_geo_region):
  163. """测试覆盖范围分析"""
  164. # 模拟区域
  165. region = Mock()
  166. region.geometry = Polygon([(108.94, 34.26), (108.95, 34.26), (108.95, 34.27), (108.94, 34.27), (108.94, 34.26)])
  167. region.area = 1000000 # 1平方公里
  168. # 模拟设备
  169. devices = [
  170. Mock(geometry=Point(108.945, 34.265), coverage_radius=500),
  171. Mock(geometry=Point(108.948, 34.268), coverage_radius=500),
  172. Mock(geometry=Point(108.952, 34.262), coverage_radius=500)
  173. ]
  174. mock_geo_region.query.return_value = [region]
  175. coverage_analysis = self.spatial_service.analyze_coverage(devices, region)
  176. self.assertIn("total_coverage", coverage_analysis)
  177. self.assertIn("coverage_percentage", coverage_analysis)
  178. self.assertIn("coverage_gaps", coverage_analysis)
  179. self.assertGreaterEqual(coverage_analysis["coverage_percentage"], 0)
  180. self.assertLessEqual(coverage_analysis["coverage_percentage"], 100)
  181. def test_nearest_neighbor_search(self):
  182. """测试最近邻搜索"""
  183. target_point = Point(108.948024, 34.265773)
  184. # 创建候选点
  185. candidates = [
  186. Point(108.950024, 34.267773), # 约300米
  187. Point(108.955024, 34.270773), # 约600米
  188. Point(108.945024, 34.262773), # 约400米
  189. ]
  190. nearest = self.spatial_service.find_nearest_neighbor(target_point, candidates)
  191. self.assertIsInstance(nearest, Point)
  192. # 验证最近的点
  193. expected_nearest = candidates[0] # 最近的点
  194. self.assertEqual(nearest.x, expected_nearest.x)
  195. self.assertEqual(nearest.y, expected_nearest.y)
  196. class TestRoutePlanningService(unittest.TestCase):
  197. """路径规划服务测试"""
  198. def setUp(self):
  199. self.route_service = RoutePlanningService()
  200. @patch('src.gis.services.networkx')
  201. def test_shortest_path_planning(self, mock_networkx):
  202. """测试最短路径规划"""
  203. # 模拟路网
  204. mock_networkx.graph.return_value = {
  205. 'A': {'B': 1, 'C': 4},
  206. 'B': {'A': 1, 'C': 2, 'D': 5},
  207. 'C': {'A': 4, 'B': 2, 'D': 1},
  208. 'D': {'B': 5, 'C': 1}
  209. }
  210. mock_networkx.shortest_path.return_value = ['A', 'B', 'D']
  211. mock_networkx.shortest_path_length.return_value = 6
  212. start = 'A'
  213. end = 'D'
  214. route = self.route_service.plan_shortest_path(start, end)
  215. self.assertEqual(route.path, ['A', 'B', 'D'])
  216. self.assertEqual(route.distance, 6)
  217. self.assertEqual(route.duration, 6) # 假设速度相同
  218. @patch('src.gis.services.networkx')
  219. def test_avoid_obstacles(self, mock_networkx):
  220. """测试障碍物避让"""
  221. # 模拟包含障碍物的路网
  222. mock_networkx.graph.return_value = {
  223. 'A': {'B': 1, 'C': 4},
  224. 'B': {'A': 1, 'C': 2},
  225. 'C': {'A': 4, 'B': 2, 'D': 1},
  226. 'D': {'C': 1}
  227. }
  228. mock_networkx.shortest_path.return_value = ['A', 'B', 'C', 'D']
  229. mock_networkx.shortest_path_length.return_value = 4
  230. start = 'A'
  231. end = 'D'
  232. obstacles = ['C'] # 避开节点C
  233. route = self.route_service.plan_path_avoiding_obstacles(start, end, obstacles)
  234. self.assertNotIn('C', route.path)
  235. self.assertGreater(route.distance, 3) # 应该更长
  236. def test_multi_stop_routing(self):
  237. """测试多点路径规划"""
  238. stops = ['A', 'B', 'C', 'D']
  239. with patch.object(self.route_service, 'plan_shortest_path') as mock_plan:
  240. mock_plan.side_effect = [
  241. Mock(path=['A', 'B'], distance=2, duration=2),
  242. Mock(path=['B', 'C'], distance=3, duration=3),
  243. Mock(path=['C', 'D'], distance=1, duration=1)
  244. ]
  245. route = self.route_service.plan_multi_stop_route(stops)
  246. self.assertEqual(len(route.segments), 3)
  247. self.assertEqual(route.total_distance, 6)
  248. self.assertEqual(route.total_duration, 6)
  249. def test_routing_with_constraints(self):
  250. """测试约束条件路径规划"""
  251. start = Point(108.94, 34.26)
  252. end = Point(108.95, 34.27)
  253. max_distance = 2000
  254. max_duration = 1800 # 30分钟
  255. vehicle_type = "car"
  256. route = self.route_service.plan_constrained_route(
  257. start, end, max_distance, max_duration, vehicle_type
  258. )
  259. self.assertIsNotNone(route)
  260. self.assertLessEqual(route.distance, max_distance)
  261. self.assertLessEqual(route.duration, max_duration)
  262. class TestGeoAnalysisService(unittest.TestCase):
  263. """地理分析服务测试"""
  264. def setUp(self):
  265. self.analysis_service = GeoAnalysisService()
  266. def test_density_analysis(self):
  267. """测试密度分析"""
  268. points = [
  269. Point(108.94, 34.26),
  270. Point(108.942, 34.261),
  271. Point(108.944, 34.259),
  272. Point(108.941, 34.262),
  273. Point(108.943, 34.260)
  274. ]
  275. density_result = self.analysis_service.calculate_density(points, grid_size=0.001)
  276. self.assertIn("grid_density", density_result)
  277. self.assertIn("average_density", density_result)
  278. self.assertGreater(density_result["average_density"], 0)
  279. def test_hotspot_detection(self):
  280. """测试热点检测"""
  281. points = []
  282. for i in range(100):
  283. # 生成集中在某个区域的点
  284. x = 108.94 + (i % 10) * 0.001
  285. y = 34.26 + (i // 10) * 0.001
  286. points.append(Point(x, y))
  287. # 添加一些噪声点
  288. for i in range(20):
  289. x = 108.9 + i * 0.01
  290. y = 34.25 + i * 0.01
  291. points.append(Point(x, y))
  292. hotspots = self.analysis_service.detect_hotspots(points, threshold=10)
  293. self.assertGreater(len(hotspots), 0)
  294. self.assertTrue(all(hotspot.count >= 10 for hotspot in hotspots))
  295. def service_test_buffer_analysis(self):
  296. """测试缓冲区分析"""
  297. center_point = Point(108.948024, 34.265773)
  298. buffer_distances = [500, 1000, 1500] # 500m, 1km, 1.5km
  299. buffers = self.analysis_service.create_multiple_buffers(center_point, buffer_distances)
  300. self.assertEqual(len(buffers), 3)
  301. for i, buffer in enumerate(buffers):
  302. self.assertIsInstance(buffer, Polygon)
  303. self.assertEqual(buffer.area, (buffer_distances[i] ** 2) * 3.14159 / 1000000) # 大致面积
  304. def test_spatial_join(self):
  305. """测试空间连接"""
  306. # 创建点数据(设备)
  307. points = [
  308. Mock(geometry=Point(108.945, 34.265), device_id="sensor_001"),
  309. Mock(geometry=Point(108.948, 34.268), device_id="sensor_002"),
  310. Mock(geometry=Point(109.0, 34.3), device_id="sensor_003")
  311. ]
  312. # 创建多边形数据(区域)
  313. polygons = [
  314. Mock(geometry=Polygon([(108.94, 34.26), (108.95, 34.26), (108.95, 34.27), (108.94, 34.27), (108.94, 34.26)]), region_id="area_001")
  315. ]
  316. joined_data = self.analysis_service.spatial_join(points, polygons, "within")
  317. self.assertGreater(len(joined_data), 0)
  318. # 前两个点应该在区域内,第三个点不在
  319. self.assertEqual(len([p for p in joined_data if p["device_id"] == "sensor_001"]), 1)
  320. self.assertEqual(len([p for p in joined_data if p["device_id"] == "sensor_003"]), 0)
  321. class TestPathRoute(unittest.TestCase):
  322. """路径路线模型测试"""
  323. def setUp(self):
  324. self.route_data = {
  325. "route_id": "route_001",
  326. "name": "巡检路线",
  327. "start_point": {"coordinates": [108.94, 34.26]},
  328. "end_point": {"coordinates": [108.95, 34.27]},
  329. "waypoints": [
  330. {"coordinates": [108.941, 34.261]},
  331. {"coordinates": [108.945, 34.265]}
  332. ],
  333. "total_distance": 2500,
  334. "total_duration": 1800,
  335. "transport_mode": "walking"
  336. }
  337. def test_route_creation(self):
  338. """测试路线创建"""
  339. route = PathRoute(self.route_data)
  340. self.assertEqual(route.route_id, self.route_data["route_id"])
  341. self.assertEqual(route.total_distance, 2500)
  342. self.assertEqual(route.total_duration, 1800)
  343. self.assertEqual(route.transport_mode, "walking")
  344. # 验证几何对象
  345. self.assertIsInstance(route.geometry, LineString)
  346. self.assertEqual(len(route.waypoints), 2)
  347. def test_route_optimization(self):
  348. """测试路线优化"""
  349. route = PathRoute(self.route_data)
  350. # 添加更多路径点
  351. additional_waypoints = [
  352. {"coordinates": [108.942, 34.262]},
  353. {"coordinates": [108.944, 34.264]}
  354. ]
  355. optimized_route = route.optimize_waypoints(additional_waypoints)
  356. self.assertIsInstance(optimized_route, PathRoute)
  357. # 优化后的距离应该更短
  358. self.assertLess(optimized_route.total_distance, route.total_distance)
  359. def test_route_export(self):
  360. """测试路线导出"""
  361. route = PathRoute(self.route_data)
  362. # 导出为GeoJSON
  363. geojson = route.export_to_geojson()
  364. self.assertIn("type", geojson)
  365. self.assertEqual(geojson["type"], "LineString")
  366. self.assertIn("coordinates", geojson)
  367. # 导出为GPX
  368. gpx = route.export_to_gpx()
  369. self.assertIn("<trkseg>", gpx)
  370. self.assertIn("<trkpt", gpx)
  371. # 导出为KML
  372. kml = route.export_to_kml()
  373. self.assertIn("<LineString>", kml)
  374. self.assertIn("<coordinates>", kml)
  375. if __name__ == '__main__':
  376. unittest.main()