pymammotion 0.4.0a2__py3-none-any.whl → 0.5.51__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pymammotion might be problematic. Click here for more details.

Files changed (133) hide show
  1. pymammotion/__init__.py +5 -4
  2. pymammotion/aliyun/client.py +235 -0
  3. pymammotion/aliyun/cloud_gateway.py +312 -64
  4. pymammotion/aliyun/model/aep_response.py +1 -2
  5. pymammotion/aliyun/model/dev_by_account_response.py +170 -23
  6. pymammotion/aliyun/model/login_by_oauth_response.py +2 -3
  7. pymammotion/aliyun/model/regions_response.py +3 -3
  8. pymammotion/aliyun/model/session_by_authcode_response.py +2 -2
  9. pymammotion/aliyun/model/thing_response.py +12 -0
  10. pymammotion/aliyun/regions.py +62 -0
  11. pymammotion/aliyun/tea/core.py +297 -0
  12. pymammotion/bluetooth/ble.py +7 -9
  13. pymammotion/bluetooth/ble_message.py +10 -14
  14. pymammotion/const.py +3 -0
  15. pymammotion/data/model/__init__.py +1 -2
  16. pymammotion/data/model/device.py +95 -27
  17. pymammotion/data/model/device_config.py +4 -4
  18. pymammotion/data/model/device_info.py +35 -0
  19. pymammotion/data/model/device_limits.py +10 -10
  20. pymammotion/data/model/enums.py +12 -2
  21. pymammotion/data/model/errors.py +12 -0
  22. pymammotion/data/model/events.py +14 -0
  23. pymammotion/data/model/generate_geojson.py +521 -0
  24. pymammotion/data/model/generate_route_information.py +2 -2
  25. pymammotion/data/model/hash_list.py +370 -57
  26. pymammotion/data/model/location.py +4 -4
  27. pymammotion/data/model/mowing_modes.py +17 -1
  28. pymammotion/data/model/raw_data.py +2 -10
  29. pymammotion/data/model/region_data.py +10 -11
  30. pymammotion/data/model/report_info.py +31 -5
  31. pymammotion/data/model/work.py +27 -0
  32. pymammotion/data/mower_state_manager.py +316 -0
  33. pymammotion/data/mqtt/event.py +73 -28
  34. pymammotion/data/mqtt/mammotion_properties.py +257 -0
  35. pymammotion/data/mqtt/properties.py +93 -78
  36. pymammotion/data/mqtt/status.py +18 -17
  37. pymammotion/event/event.py +27 -6
  38. pymammotion/homeassistant/__init__.py +3 -0
  39. pymammotion/homeassistant/mower_api.py +484 -0
  40. pymammotion/homeassistant/rtk_api.py +54 -0
  41. pymammotion/http/encryption.py +5 -6
  42. pymammotion/http/http.py +574 -28
  43. pymammotion/http/model/__init__.py +0 -0
  44. pymammotion/{aliyun/model/stream_subscription_response.py → http/model/camera_stream.py} +14 -2
  45. pymammotion/http/model/http.py +129 -4
  46. pymammotion/http/model/response_factory.py +61 -0
  47. pymammotion/http/model/rtk.py +16 -0
  48. pymammotion/mammotion/commands/abstract_message.py +7 -5
  49. pymammotion/mammotion/commands/mammotion_command.py +30 -1
  50. pymammotion/mammotion/commands/messages/basestation.py +43 -0
  51. pymammotion/mammotion/commands/messages/driver.py +61 -29
  52. pymammotion/mammotion/commands/messages/media.py +68 -15
  53. pymammotion/mammotion/commands/messages/navigation.py +61 -25
  54. pymammotion/mammotion/commands/messages/network.py +17 -23
  55. pymammotion/mammotion/commands/messages/ota.py +18 -18
  56. pymammotion/mammotion/commands/messages/system.py +32 -49
  57. pymammotion/mammotion/commands/messages/video.py +15 -16
  58. pymammotion/mammotion/devices/__init__.py +27 -3
  59. pymammotion/mammotion/devices/base.py +40 -131
  60. pymammotion/mammotion/devices/mammotion.py +436 -201
  61. pymammotion/mammotion/devices/mammotion_bluetooth.py +57 -47
  62. pymammotion/mammotion/devices/mammotion_cloud.py +134 -105
  63. pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
  64. pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
  65. pymammotion/mammotion/devices/managers/managers.py +81 -0
  66. pymammotion/mammotion/devices/mower_device.py +124 -0
  67. pymammotion/mammotion/devices/mower_manager.py +107 -0
  68. pymammotion/mammotion/devices/rtk_ble.py +89 -0
  69. pymammotion/mammotion/devices/rtk_cloud.py +113 -0
  70. pymammotion/mammotion/devices/rtk_device.py +50 -0
  71. pymammotion/mammotion/devices/rtk_manager.py +122 -0
  72. pymammotion/mqtt/__init__.py +2 -1
  73. pymammotion/mqtt/aliyun_mqtt.py +232 -0
  74. pymammotion/mqtt/linkkit/__init__.py +5 -0
  75. pymammotion/mqtt/linkkit/h2client.py +585 -0
  76. pymammotion/mqtt/linkkit/linkkit.py +3023 -0
  77. pymammotion/mqtt/mammotion_mqtt.py +176 -169
  78. pymammotion/mqtt/mqtt_models.py +66 -0
  79. pymammotion/proto/__init__.py +4839 -4
  80. pymammotion/proto/basestation.proto +8 -0
  81. pymammotion/proto/basestation_pb2.py +11 -9
  82. pymammotion/proto/basestation_pb2.pyi +16 -2
  83. pymammotion/proto/dev_net.proto +79 -55
  84. pymammotion/proto/dev_net_pb2.py +60 -56
  85. pymammotion/proto/dev_net_pb2.pyi +49 -6
  86. pymammotion/proto/luba_msg.proto +2 -1
  87. pymammotion/proto/luba_msg_pb2.py +6 -6
  88. pymammotion/proto/luba_msg_pb2.pyi +1 -0
  89. pymammotion/proto/luba_mul.proto +62 -1
  90. pymammotion/proto/luba_mul_pb2.py +38 -22
  91. pymammotion/proto/luba_mul_pb2.pyi +94 -7
  92. pymammotion/proto/mctrl_driver.proto +44 -4
  93. pymammotion/proto/mctrl_driver_pb2.py +26 -14
  94. pymammotion/proto/mctrl_driver_pb2.pyi +66 -11
  95. pymammotion/proto/mctrl_nav.proto +93 -52
  96. pymammotion/proto/mctrl_nav_pb2.py +75 -67
  97. pymammotion/proto/mctrl_nav_pb2.pyi +142 -56
  98. pymammotion/proto/mctrl_ota.proto +40 -2
  99. pymammotion/proto/mctrl_ota_pb2.py +23 -13
  100. pymammotion/proto/mctrl_ota_pb2.pyi +67 -4
  101. pymammotion/proto/mctrl_pept.proto +8 -3
  102. pymammotion/proto/mctrl_pept_pb2.py +8 -6
  103. pymammotion/proto/mctrl_pept_pb2.pyi +14 -6
  104. pymammotion/proto/mctrl_sys.proto +325 -86
  105. pymammotion/proto/mctrl_sys_pb2.py +162 -98
  106. pymammotion/proto/mctrl_sys_pb2.pyi +451 -25
  107. pymammotion/proto/message_pool.py +3 -0
  108. pymammotion/proto/py.typed +0 -0
  109. pymammotion/utility/constant/device_constant.py +29 -5
  110. pymammotion/utility/datatype_converter.py +13 -12
  111. pymammotion/utility/device_config.py +522 -130
  112. pymammotion/utility/device_type.py +218 -21
  113. pymammotion/utility/map.py +238 -51
  114. pymammotion/utility/mur_mur_hash.py +159 -0
  115. {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info}/METADATA +26 -31
  116. pymammotion-0.5.51.dist-info/RECORD +152 -0
  117. {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info}/WHEEL +1 -1
  118. pymammotion/aliyun/cloud_service.py +0 -65
  119. pymammotion/data/model/plan.py +0 -58
  120. pymammotion/data/state_manager.py +0 -129
  121. pymammotion/proto/basestation.py +0 -59
  122. pymammotion/proto/common.py +0 -12
  123. pymammotion/proto/dev_net.py +0 -381
  124. pymammotion/proto/luba_msg.py +0 -81
  125. pymammotion/proto/luba_mul.py +0 -76
  126. pymammotion/proto/mctrl_driver.py +0 -100
  127. pymammotion/proto/mctrl_nav.py +0 -664
  128. pymammotion/proto/mctrl_ota.py +0 -48
  129. pymammotion/proto/mctrl_pept.py +0 -41
  130. pymammotion/proto/mctrl_sys.py +0 -574
  131. pymammotion-0.4.0a2.dist-info/RECORD +0 -131
  132. /pymammotion/http/{_init_.py → __init__.py} +0 -0
  133. {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,521 @@
1
+ import json
2
+ import math
3
+ from typing import Any
4
+
5
+ from shapely.geometry import Point
6
+
7
+ from pymammotion import logger
8
+ from pymammotion.data.model.hash_list import (
9
+ AreaHashNameList,
10
+ CommDataCouple,
11
+ FrameList,
12
+ HashList,
13
+ MowPath,
14
+ MowPathPacket,
15
+ NavGetCommData,
16
+ )
17
+
18
+ # Constants
19
+ DEFAULT_X_OFFSET: int = 0
20
+ DEFAULT_Y_OFFSET: int = 0
21
+ GEOMETRY_TYPES: list[str] = ["Polygon", "Polygon", "LineString", "Point"]
22
+ MAP_OBJECT_TYPES: list[str] = ["area", "path", "obstacle", "dump"]
23
+
24
+ image_path = "/local/community/ha-mammotion-map/dist/assets/map/"
25
+
26
+ RTK_IMAGE = {
27
+ "iconImage": "map_icon_base_station_rtk.webp",
28
+ "iconSize": [30, 30],
29
+ "iconAnchor": [15, 30],
30
+ "iconUrl": f"{image_path}map_icon_base_station_rtk.webp",
31
+ }
32
+
33
+ DOCK_IMAGE = {
34
+ "iconImage": "icon_map_recharge.webp",
35
+ "iconSize": [30, 30],
36
+ "iconAnchor": [15, 15],
37
+ "iconUrl": f"{image_path}icon_map_recharge.webp",
38
+ }
39
+
40
+ #############################
41
+ # STYLE CONFIGURATION
42
+ #############################
43
+
44
+ DOCK_STYLE = {"color": "lightgray", "fill": "lightgray", "weight": 2, "opacity": 1.0, "fillOpacity": 0.7, "radius": 10}
45
+
46
+ RTK_STYLE = {"color": "purple", "fill": "purple", "weight": 2, "opacity": 1.0, "fillOpacity": 0.7, "radius": 7}
47
+
48
+ AREA_STYLE = {
49
+ "color": "green",
50
+ "fill": "darkgreen",
51
+ "weight": 3,
52
+ "opacity": 0.8,
53
+ "fillOpacity": 0.3,
54
+ "dashArray": "",
55
+ "lineCap": "round",
56
+ "lineJoin": "round",
57
+ }
58
+
59
+ OBSTACLE_STYLE = {
60
+ "color": "#FF4D00",
61
+ "fill": "darkorange",
62
+ "weight": 2,
63
+ "opacity": 0.9,
64
+ "fillOpacity": 0.4,
65
+ "dashArray": "",
66
+ "lineCap": "round",
67
+ "lineJoin": "round",
68
+ }
69
+
70
+ PATH_STYLE = {
71
+ "color": "#ffffff",
72
+ "weight": 8,
73
+ "opacity": 1.0,
74
+ "zIndex": -1,
75
+ "dashArray": "",
76
+ "lineCap": "round",
77
+ "lineJoin": "round",
78
+ "road_center_color": "#696969",
79
+ "road_center_dash": "8, 8",
80
+ }
81
+
82
+ POINT_STYLE = {"color": "blue", "fill": "lightblue", "weight": 2, "opacity": 1.0, "fillOpacity": 0.7, "radius": 5}
83
+
84
+ geojson_metadata = {"name": "Lawn Areas", "description": "Generated from Mammotion diagnostics data"}
85
+
86
+ # Map type IDs
87
+ TYPE_MOWING_ZONE: int = 0
88
+ TYPE_OBSTACLE: int = 1
89
+ TYPE_PATH: int = 2
90
+ TYPE_MOW_PATH: int = 3
91
+
92
+ # Coordinate conversion constants
93
+ METERS_PER_DEGREE_LON: int = 111320
94
+ METERS_PER_DEGREE_LAT: int = 111320
95
+
96
+ # Type aliases
97
+ Coordinate = tuple[float, float]
98
+ CoordinateList = list[list[float]]
99
+ GeoJSONFeature = dict[str, Any]
100
+ GeoJSONCollection = dict[str, Any]
101
+ LocationDict = dict[str, Coordinate]
102
+
103
+ # Path to mammotion integration diagnostics file
104
+ x_offset: int = DEFAULT_X_OFFSET
105
+ y_offset: int = DEFAULT_Y_OFFSET
106
+ geometry_types: list[str] = GEOMETRY_TYPES
107
+
108
+
109
+ def is_overlapping(p: Point, placed_points: list[Point], min_distance: float = 0.00005) -> bool:
110
+ """Check if point p is too close to any previously placed label."""
111
+ return any(p.distance(existing) < min_distance for existing in placed_points)
112
+
113
+
114
+ def apply_meter_offsets(lon: float, lat: float, lon_offset: float, lat_offset: float) -> list[float]:
115
+ """Apply meter-based offsets to coordinates (in degrees)"""
116
+ new_lon = lon + (lon_offset / (111320 * math.cos(math.radians(lat))))
117
+ new_lat = lat + (lat_offset / 111320)
118
+ return [new_lon, new_lat]
119
+
120
+
121
+ def generate_geojson(
122
+ hash_list: HashList, rtk_location: Point, dock_location: Point, dock_rotation: int
123
+ ) -> GeoJSONCollection:
124
+ """Generate GeoJSON from hash list data.
125
+
126
+ Args:
127
+ hash_list: HashList object containing map data
128
+ rtk_location: Tuple of (longitude, latitude) for rtk position
129
+ :param hash_list:
130
+ :param rtk_location:
131
+ :param dock_rotation:
132
+ :param dock_location:
133
+
134
+ """
135
+ area_names = _build_area_name_lookup(hash_list.area_name)
136
+
137
+ geo_json: GeoJSONCollection = {"type": "FeatureCollection", "name": "Lawn Areas", "features": []}
138
+ _add_rtk_and_dock(rtk_location, dock_location, dock_rotation, geo_json)
139
+ total_frames = _process_map_objects(hash_list, rtk_location, area_names, geo_json)
140
+
141
+ # _save_geojson(geo_json)
142
+ logger.debug("GeoJson complete. Total Frames processed:", total_frames)
143
+ return geo_json
144
+
145
+
146
+ def _add_rtk_and_dock(
147
+ rtk_location: Point, dock_location: Point, dock_rotation: int, geo_json: GeoJSONCollection
148
+ ) -> None:
149
+ geo_json["features"].append(
150
+ {
151
+ "type": "Feature",
152
+ "properties": {
153
+ "title": "RTK Base",
154
+ "Name": "RTK Base",
155
+ "description": "RTK Base Station location",
156
+ "type_name": "station",
157
+ **RTK_STYLE,
158
+ **RTK_IMAGE,
159
+ },
160
+ "geometry": {"type": "Point", "coordinates": [rtk_location.y, rtk_location.x]},
161
+ }
162
+ )
163
+
164
+ geo_json["features"].append(
165
+ {
166
+ "type": "Feature",
167
+ "properties": {
168
+ "title": "Dock",
169
+ "Name": "Dock",
170
+ "description": "Charging dock location",
171
+ "type_name": "station",
172
+ "rotation": dock_rotation,
173
+ **DOCK_STYLE,
174
+ **DOCK_IMAGE,
175
+ },
176
+ "geometry": {"type": "Point", "coordinates": [dock_location.y, dock_location.x]},
177
+ }
178
+ )
179
+
180
+
181
+ def _build_area_name_lookup(area_names: list[AreaHashNameList]) -> dict[int, str]:
182
+ """Build a hash lookup table for area names.
183
+
184
+ Args:
185
+ area_names: List of AreaHashNameList objects
186
+
187
+ Returns:
188
+ Dictionary mapping hash to area name
189
+
190
+ """
191
+ return {item.hash: item.name for item in area_names}
192
+
193
+
194
+ def _process_map_objects(
195
+ hash_list: HashList, rtk_location: Point, area_names: dict[int, str], geo_json: GeoJSONCollection
196
+ ) -> int:
197
+ """Process all map objects and add them to GeoJSON.
198
+
199
+ Args:
200
+ hash_list: HashList object containing map data
201
+ rtk_location: Tuple of (longitude, latitude) for rtk position
202
+ area_names: Dictionary mapping hash to area name
203
+ geo_json: GeoJSON collection to add features to
204
+
205
+ Returns:
206
+ Total number of frames processed
207
+
208
+ """
209
+ total_frames = 0
210
+
211
+ # Map type names to their corresponding dictionaries in HashList
212
+ type_mapping: dict[str, dict[int, FrameList]] = {
213
+ "area": hash_list.area,
214
+ "path": hash_list.path,
215
+ "obstacle": hash_list.obstacle,
216
+ "dump": hash_list.dump,
217
+ }
218
+
219
+ for type_name, map_objects in type_mapping.items():
220
+ for hash_key, frame_list in map_objects.items():
221
+ logger.debug(hash_key, type_name)
222
+
223
+ if not _validate_frame_list(frame_list, hash_key, area_names):
224
+ continue
225
+
226
+ local_coords = _collect_frame_coordinates(frame_list)
227
+ total_frames += len(frame_list.data)
228
+
229
+ lonlat_coords = _convert_to_lonlat_coords(local_coords, rtk_location)
230
+ length, area = map_object_stats(local_coords)
231
+
232
+ feature = _create_feature(hash_key, frame_list, type_name, lonlat_coords, length, area)
233
+ if feature:
234
+ geo_json["features"].append(feature)
235
+
236
+ for key, path_packet_list in hash_list.current_mow_path.items():
237
+ local_coords = _collect_mow_frame_coordinates(path_packet_list)
238
+ total_frames += len(path_packet_list.path_packets)
239
+
240
+ lonlat_coords = _convert_to_lonlat_coords(local_coords, rtk_location)
241
+ length, area = map_object_stats(local_coords)
242
+
243
+ feature = _create_mow_path_feature(key, path_packet_list, lonlat_coords, length, area)
244
+ if feature:
245
+ geo_json["features"].append(feature)
246
+
247
+ return total_frames
248
+
249
+
250
+ def _validate_frame_list(frame_list: FrameList, hash_key: int, area_names: dict[int, str]) -> bool:
251
+ """Validate that frame list has complete frame data.
252
+
253
+ Args:
254
+ frame_list: FrameList object to validate
255
+ hash_key: Hash key for the area
256
+ area_names: Dictionary mapping hash to area name
257
+
258
+ Returns:
259
+ True if valid, False otherwise
260
+
261
+ """
262
+ if len(frame_list.data) != frame_list.total_frame:
263
+ area_name = area_names.get(hash_key, "Unknown")
264
+ logger.debug(f"Error: full coord data not available for area: '{area_name}' - '{hash_key}'")
265
+ return False
266
+ return True
267
+
268
+
269
+ def _collect_frame_coordinates(frame_list: FrameList) -> list[CommDataCouple]:
270
+ """Collect coordinates from all frames in a FrameList.
271
+
272
+ Args:
273
+ frame_list: FrameList containing frame data
274
+
275
+ Returns:
276
+ List of coordinate dictionaries with 'x' and 'y' keys
277
+
278
+ """
279
+ local_coords: list[CommDataCouple] = []
280
+ for frame in frame_list.data:
281
+ if isinstance(frame, NavGetCommData):
282
+ local_coords.extend(frame.data_couple)
283
+ # TODO svg message needs different transform
284
+ # elif isinstance(frame, SvgMessage):
285
+ # local_coords.extend(frame.)
286
+ return local_coords
287
+
288
+
289
+ def _collect_mow_frame_coordinates(frame_list: MowPath) -> list[CommDataCouple]:
290
+ """Collect coordinates from all frames in a FrameList."""
291
+ local_coords: list[CommDataCouple] = []
292
+ for frame in frame_list.path_packets:
293
+ local_coords.extend(frame.data_couple)
294
+ return local_coords
295
+
296
+
297
+ def _convert_to_lonlat_coords(local_coords: list[CommDataCouple], rtk_location: Point) -> CoordinateList:
298
+ """Convert local x,y coordinates to lon,lat coordinates.
299
+
300
+ Args:
301
+ local_coords: List of coordinate dictionaries with 'x' and 'y' keys
302
+ rtk_location: Tuple of (longitude, latitude) for rtk position
303
+
304
+ Returns:
305
+ List of [longitude, latitude] coordinate pairs
306
+
307
+ """
308
+ lonlat_coords: CoordinateList = [
309
+ list(lon_lat_delta(rtk_location, xy.x + x_offset, xy.y + y_offset)) for xy in local_coords
310
+ ]
311
+ lonlat_coords.reverse() # GeoJSON polygons go clockwise
312
+ return lonlat_coords
313
+
314
+
315
+ def _create_feature(
316
+ hash_key: int, frame_list: FrameList, type_name: str, lonlat_coords: CoordinateList, length: float, area: float
317
+ ) -> GeoJSONFeature | None:
318
+ """Create a GeoJSON feature from frame list data.
319
+
320
+ Args:
321
+ hash_key: Hash identifier for the feature
322
+ frame_list: FrameList containing frame data
323
+ type_name: Type name of the map object
324
+ lonlat_coords: List of [longitude, latitude] coordinate pairs
325
+ length: Calculated length of the feature
326
+ area: Calculated area of the feature
327
+
328
+ Returns:
329
+ GeoJSON feature dictionary or None if invalid
330
+
331
+ """
332
+ first_frame = frame_list.data[0]
333
+ type_id = first_frame.type
334
+ object_name = ""
335
+ if isinstance(first_frame, NavGetCommData):
336
+ object_name = first_frame.name_time.name
337
+
338
+ properties = _create_feature_properties(hash_key, type_id, type_name, first_frame, length, area, object_name)
339
+ geometry = _create_feature_geometry(type_id, lonlat_coords, properties)
340
+
341
+ if geometry is None:
342
+ return None
343
+
344
+ return {"type": "Feature", "properties": properties, "geometry": geometry}
345
+
346
+
347
+ def _create_mow_path_feature(
348
+ key: int, path_packet_list: MowPath, lonlat_coords: CoordinateList, length: float, area: float
349
+ ) -> GeoJSONFeature | None:
350
+ first_frame = path_packet_list.path_packets[0]
351
+
352
+ properties = _create_feature_mow_path_properties(key, first_frame, length, path_packet_list.area)
353
+ geometry = _create_feature_geometry(3, lonlat_coords, properties)
354
+
355
+ if geometry is None:
356
+ return None
357
+
358
+ return {"type": "Feature", "properties": properties, "geometry": geometry}
359
+
360
+
361
+ def _create_feature_mow_path_properties(
362
+ hash_key: int, first_frame: MowPathPacket, length: float, area: float
363
+ ) -> dict[str, Any]:
364
+ """Create properties dictionary for GeoJSON feature."""
365
+ return {
366
+ "hash": hash_key,
367
+ "type_name": "mow_path",
368
+ "zone_hash": first_frame.zone_hash,
369
+ "length": length,
370
+ "area": area,
371
+ }
372
+
373
+
374
+ def _create_feature_properties(
375
+ hash_key: int, type_id: int, type_name: str, first_frame: Any, length: float, area: float, object_name: str = ""
376
+ ) -> dict[str, Any]:
377
+ """Create properties dictionary for GeoJSON feature.
378
+
379
+ Args:
380
+ hash_key: Hash identifier
381
+ object_name: Name of the object
382
+ type_id: Type ID of the feature
383
+ type_name: Type name of the feature
384
+ first_frame: First frame from the FrameList
385
+ length: Calculated length
386
+ area: Calculated area
387
+
388
+ Returns:
389
+ Properties dictionary
390
+
391
+ """
392
+ return {
393
+ "hash": hash_key,
394
+ "title": object_name,
395
+ "Name": object_name,
396
+ "description": "description <b>test</b>",
397
+ "type_id": type_id,
398
+ "type_name": type_name,
399
+ "parent_hash_a": first_frame.paternal_hash_a,
400
+ "parent_hash_b": first_frame.paternal_hash_b,
401
+ "length": length,
402
+ "area": area,
403
+ }
404
+
405
+
406
+ def _create_feature_geometry(
407
+ type_id: int, lonlat_coords: CoordinateList, properties: dict[str, Any]
408
+ ) -> dict[str, Any] | None:
409
+ """Create geometry dictionary for GeoJSON feature based on type.
410
+
411
+ Args:
412
+ type_id: Type ID determining geometry type
413
+ lonlat_coords: List of [longitude, latitude] coordinate pairs
414
+ properties: Properties dictionary to modify with style information
415
+
416
+ Returns:
417
+ Geometry dictionary or None if invalid type
418
+
419
+ """
420
+ if type_id == TYPE_MOWING_ZONE:
421
+ properties.update(AREA_STYLE)
422
+ return {"type": "Polygon", "coordinates": [lonlat_coords]}
423
+ elif type_id == TYPE_OBSTACLE:
424
+ properties.update(OBSTACLE_STYLE)
425
+ return {"type": "Polygon", "coordinates": [lonlat_coords]}
426
+ elif type_id == TYPE_PATH and len(lonlat_coords) > 1:
427
+ properties.update(PATH_STYLE)
428
+ return {"type": "LineString", "coordinates": lonlat_coords}
429
+ elif type_id == TYPE_MOW_PATH and len(lonlat_coords) > 1:
430
+ properties["color"] = "green"
431
+ return {"type": "LineString", "coordinates": lonlat_coords}
432
+ else:
433
+ return None # Point (ignore)
434
+
435
+
436
+ def _save_geojson(geoJSON: GeoJSONCollection) -> None:
437
+ """Save GeoJSON data to file.
438
+
439
+ Args:
440
+ geoJSON: GeoJSON collection to save
441
+
442
+ """
443
+ with open("areas.json", "w") as json_file:
444
+ json.dump(geoJSON, json_file, indent=2)
445
+
446
+
447
+ def lon_lat_delta(rtk: Point, x: float, y: float) -> Coordinate:
448
+ """Add delta (in meters) to lon/lat, return new lon/lat.
449
+
450
+ Args:
451
+ lon: Longitude in degrees
452
+ lat: Latitude in degrees
453
+ x: X offset in meters
454
+ y: Y offset in meters
455
+
456
+ Returns:
457
+ Tuple of (new_longitude, new_latitude)
458
+
459
+ """
460
+ new_lon = rtk.y + (x / (METERS_PER_DEGREE_LON * math.cos(math.radians(rtk.x))))
461
+ new_lat = rtk.x + (y / METERS_PER_DEGREE_LAT)
462
+ return new_lon, new_lat
463
+
464
+
465
+ def map_object_stats(coords: list[CommDataCouple]) -> Coordinate:
466
+ """Calculate length and area statistics for map object coordinates.
467
+
468
+ Args:
469
+ coords: List of coordinate dictionaries with 'x' and 'y' keys
470
+
471
+ Returns:
472
+ Tuple of (length, area) in meters and square meters
473
+
474
+ """
475
+ # Point Object
476
+ if len(coords) < 2:
477
+ return 0.0, 0.0
478
+
479
+ def distance(p1: CommDataCouple, p2: CommDataCouple) -> float:
480
+ """Calculate Euclidean distance between two points."""
481
+ return math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2)
482
+
483
+ length = sum(distance(coords[i], coords[i + 1]) for i in range(len(coords) - 1))
484
+
485
+ # Open line
486
+ if coords[0] != coords[-1]:
487
+ return length, 0.0
488
+
489
+ # Closed Polygon - Calculate area using shoelace formula
490
+ area = 0.5 * abs(sum(coords[i].x * coords[i + 1].y - coords[i + 1].x * coords[i].y for i in range(len(coords) - 1)))
491
+
492
+ return length, area
493
+
494
+
495
+ def is_point_in_polygon(x: float, y: float, poly: list[list[float]]) -> bool:
496
+ """Test if a point is inside a polygon using ray casting algorithm.
497
+
498
+ Args:
499
+ x: X coordinate of the point
500
+ y: Y coordinate of the point
501
+ poly: Polygon as list of [x, y] coordinate pairs
502
+
503
+ Returns:
504
+ True if point is inside polygon, False otherwise
505
+
506
+ """
507
+ return (
508
+ sum(
509
+ (y > poly[i][1]) != (y > poly[(i + 1) % len(poly)][1])
510
+ and (
511
+ x
512
+ < (poly[(i + 1) % len(poly)][0] - poly[i][0])
513
+ * (y - poly[i][1])
514
+ / (poly[(i + 1) % len(poly)][1] - poly[i][1])
515
+ + poly[i][0]
516
+ )
517
+ for i in range(len(poly))
518
+ )
519
+ % 2
520
+ == 1
521
+ )
@@ -1,5 +1,5 @@
1
+ from dataclasses import dataclass, field
1
2
  import logging
2
- from dataclasses import dataclass
3
3
 
4
4
  logger = logging.getLogger(__name__)
5
5
 
@@ -8,7 +8,7 @@ logger = logging.getLogger(__name__)
8
8
  class GenerateRouteInformation:
9
9
  """Creates a model for generating route information and mowing plan before starting a job."""
10
10
 
11
- one_hashs: list[int] = list
11
+ one_hashs: list[int] = field(default_factory=list)
12
12
  job_mode: int = 4 # taskMode
13
13
  job_version: int = 0
14
14
  job_id: int = 0