Created
October 7, 2024 10:14
-
-
Save DEADB33F/7390286f741ad7a84ccf60d92f7cbaf6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import sys | |
| import json | |
| import ctypes | |
| import math | |
| ################## | |
| # | |
| # Save script with a .pyw extension (enables drag-dropping on windows) | |
| # Extract integration diagnostics / log file from HA, Eg... | |
| # https://i.imgur.com/1G7cB6D.png | |
| # Drag-drop log onto python script | |
| # | |
| ################### | |
| offset_x,offset_y = -1.5, 3 # Offset in meters to apply to map coods | |
| scale = 100 # Scale for SVG image (eg 100:1) | |
| def load_json(file_path): | |
| try: | |
| with open(file_path, 'r') as json_file: | |
| data = json.load(json_file) | |
| except FileNotFoundError: | |
| print(f"File not found: {file_path}") | |
| except json.JSONDecodeError: | |
| print(f"Error decoding JSON in file: {file_path}") | |
| except Exception as e: | |
| print(f"An error occurred: {str(e)}") | |
| areas = data["data"]["map"]["area"] | |
| area_list = {} | |
| ### Generate raw JSON coords file for each area | |
| for area_hash in areas: | |
| total_frame = areas[area_hash]["total_frame"] | |
| frames = areas[area_hash]["data"] | |
| if len( frames ) != total_frame: | |
| print( f"Error: full coord data not available for area: '{area_hash}'") | |
| else: | |
| area_name = frames[0]["area_label"]["label"] | |
| coords = [] | |
| # Collate frames | |
| for frame in frames: | |
| coords.extend( frame["data_couple"] ) | |
| # Save JSON coords list | |
| with open(f"{area_name}.json", 'w') as json_file: | |
| json.dump(coords, json_file,indent=2) | |
| # Convert x,y dict to tuples | |
| area_list[area_name] = [(xy['x']+offset_x, xy['y']+offset_y) for xy in coords] | |
| ### Generate SVG map of all areas | |
| # Determine image height/width and viewbox dimensions | |
| xs, ys = zip(*[v for l in area_list.values() for v in l]) | |
| width, height = (max(xs)-min(xs))*1000/scale, (max(ys)-min(ys))*1000/scale | |
| viewbox = ( | |
| min(xs)-1, | |
| min(-y for y in ys)-1, | |
| max(xs)-min(xs)+2, | |
| max(-y for y in ys)-min(-y for y in ys)+2 | |
| ) | |
| xml_data = f"""<?xml version="1.0" encoding="UTF-8"?> | |
| <svg xmlns="http://www.w3.org/2000/svg" | |
| width="{width}mm" | |
| height="{height}mm" | |
| viewBox="%f %f %f %f" | |
| >""" % viewbox | |
| for coords in area_list.values(): | |
| svg_path = ' '.join(['%s%f %f' % (['M', 'L'][i>0], x, -y) for i, (x, y) in enumerate(coords)]) | |
| xml_data += f'\n <path style="fill:green;stroke:black;stroke-width:0.1" d="{svg_path}"/>' | |
| xml_data += "\n </svg>" | |
| # Save overview map (100:1 scale) | |
| with open(f"mowing_areas.svg", 'w') as svg_file: | |
| svg_file.write( xml_data ) | |
| ### Generate KML file | |
| # Get lon/lat of mower, dock & RTK | |
| locations = data["data"]["location"] | |
| mower = (locations["device"]["longitude"],locations["device"]["latitude"]) | |
| rtk = lon_lat_delta(*mower, locations["RTK"]["longitude"],locations["RTK"]["latitude"] ) | |
| dock = lon_lat_delta(*mower, locations["dock"]["longitude"],locations["dock"]["latitude"] ) | |
| kml_data = f"""<?xml version="1.0" encoding="UTF-8"?> | |
| <kml xmlns="http://www.opengis.net/kml/2.2"> | |
| <Document> | |
| <name>Mower Areas</name>""" | |
| for area_name, coords in area_list.items(): | |
| # Convert relative coords to lon/lat srings | |
| lonlat_str = "\n\t\t\t".join(f"{lon},{lat},0" for x, y in coords for lon, lat in [lon_lat_delta(*mower,x,y)]) | |
| kml_data += f""" | |
| <Placemark> | |
| <name>{area_name}</name> | |
| <Style> | |
| <LineStyle> | |
| <color>00000000</color> | |
| <width>2</width> | |
| </LineStyle> | |
| <PolyStyle> | |
| <color>7f00ff00</color> | |
| </PolyStyle> | |
| </Style> | |
| <Polygon> | |
| <outerBoundaryIs> | |
| <LinearRing> | |
| <coordinates> | |
| {lonlat_str} | |
| </coordinates> | |
| </LinearRing> | |
| </outerBoundaryIs> | |
| </Polygon> | |
| </Placemark>""" | |
| kml_data += """ | |
| <Placemark> | |
| <name>RTK</name> | |
| <Style> | |
| <IconStyle> | |
| <Icon> | |
| <href>http://maps.google.com/mapfiles/kml/shapes/placemark_circle.png</href> | |
| </Icon> | |
| </IconStyle> | |
| </Style> | |
| <Point> | |
| <coordinates> | |
| %f,%f,0 | |
| </coordinates> | |
| </Point> | |
| </Placemark> | |
| <Placemark> | |
| <name>Dock</name> | |
| <Style> | |
| <IconStyle> | |
| <Icon> | |
| <href>http://maps.google.com/mapfiles/kml/shapes/placemark_circle.png</href> | |
| </Icon> | |
| </IconStyle> | |
| </Style> | |
| <Point> | |
| <coordinates> | |
| %f,%f,0 | |
| </coordinates> | |
| </Point> | |
| </Placemark> | |
| <Placemark> | |
| <name>Mower</name> | |
| <Style> | |
| <IconStyle> | |
| <Icon> | |
| <href>http://maps.google.com/mapfiles/kml/shapes/placemark_circle.png</href> | |
| </Icon> | |
| </IconStyle> | |
| </Style> | |
| <Point> | |
| <coordinates> | |
| %f,%f,0 | |
| </coordinates> | |
| </Point> | |
| </Placemark> | |
| </Document> | |
| </kml>""" % (*rtk, *dock, *mower) | |
| # Save KML file | |
| with open(f"mowing_areas.kml", 'w') as kml_file: | |
| kml_file.write( kml_data ) | |
| ### Show Stats | |
| msg_text = f"{len(area_list)} Mowing areas processed\nArea stats:\n" | |
| for area_name, coords in area_list.items(): | |
| perim,area = polygon_stats(coords) | |
| msg_text += f"\n {area_name:>12} : {round(perim,2):>10}m, {round(area,2):>10} sq m" | |
| MessageBox = ctypes.windll.user32.MessageBoxW | |
| MessageBox(None, msg_text,'Success!', 0) | |
| # Add delta (in meters) to lon/lat, return new lon/lat | |
| def lon_lat_delta(lon, lat, x, y): | |
| new_lon = lon + (x / (111320 * math.cos(math.radians(lat)))) | |
| new_lat = lat + (y / 111320) | |
| return (new_lon, new_lat) | |
| # Return peremiter and area of polygon (credit: ChatGPT) | |
| def polygon_stats( polygon_coords ): | |
| n,v = len(polygon_coords),polygon_coords | |
| perimeter = sum(math.sqrt((v[(i + 1) % n][0] - v[i][0]) ** 2 + | |
| (v[(i + 1) % n][1] - v[i][1]) ** 2) | |
| for i in range(n)) | |
| area = abs(sum(v[i][0] * v[(i + 1) % n][1] - | |
| v[(i + 1) % n][0] * v[i][1] for i in range(n))) / 2.0 | |
| return perimeter, area | |
| # Test if a point is inside a polygon or not (credit: ChatGPT) | |
| def is_point_in_polygon( test_coords, polygon_coords ): | |
| x, y, v = *test_coords, polygon_coords | |
| return sum((y > v[i][1]) != (y > v[(i + 1) % len(v)][1]) and | |
| (x < (v[(i + 1) % len(v)][0] - v[i][0]) * | |
| (y - v[i][1]) / (v[(i + 1) % len(v)][1] - v[i][1]) + v[i][0]) | |
| for i in range(len(v))) % 2 == 1 | |
| if __name__ == "__main__": | |
| if len(sys.argv) > 1: | |
| file_path = sys.argv[1] | |
| load_json(file_path) | |
| else: | |
| MessageBox = ctypes.windll.user32.MessageBoxW | |
| MessageBox(None, 'No file to open','Error', 0) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment