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