From 47337363d26f298d321270f88550f01972ffb5e2 Mon Sep 17 00:00:00 2001 From: Peter Barker Date: Mon, 17 Mar 2025 19:42:00 +1100 Subject: [PATCH 1/3] lib/kmlread: provide alternate interface for reading KML --- MAVProxy/modules/lib/kmlread.py | 131 ++++++++++++++++++++++++++++++-- 1 file changed, 123 insertions(+), 8 deletions(-) diff --git a/MAVProxy/modules/lib/kmlread.py b/MAVProxy/modules/lib/kmlread.py index 275ed80b3a..8924b79d94 100644 --- a/MAVProxy/modules/lib/kmlread.py +++ b/MAVProxy/modules/lib/kmlread.py @@ -9,14 +9,28 @@ from io import BytesIO as SIO from zipfile import ZipFile import pathlib +import re namespaces = {'kml': 'http://www.opengis.net/kml/2.2'} +class Style(): + def __init__(self, id): + self.id = id + self.line_colour = None + + +class StyleMap(): + def __init__(self, id, otherstyle): + self.id = id + self.otherstyle = otherstyle + + class Polygon(): - def __init__(self, name, latlon): + def __init__(self, name, latlon, line_colour=None): self.name = name self.vertexes = latlon + self.line_colour = line_colour class Point(): @@ -27,6 +41,15 @@ def __init__(self, name, latlon): def readkmz(filename): '''reads in a kmz file and returns xml nodes''' + xpath = xpath = ".//kml:Placemark" + + tree = etree_for_filepath(filename) + + return tree.findall(xpath, namespaces) + + +def etree_for_filepath(filename): + '''reads in a kmz file and returns lxml.etree.ElementTree''' # Strip quotation marks if neccessary filename.strip('"') # Open the zip file (as applicable) @@ -48,10 +71,7 @@ def readkmz(filename): raise Exception(f"load expects a .kml or .kmz file, got ({suffix}) from ({filename})") parser = etree.XMLParser(encoding='utf-8', recover=True) - tree = etree.parse(SIO(fstring), parser) - xpath = xpath = ".//kml:Placemark" - - return tree.findall(xpath, namespaces) + return etree.parse(SIO(fstring), parser) def find_tag(node, tagname): @@ -92,13 +112,108 @@ def readObject(innode): for c in coordinates.text.split(): s = c.split(',') latlon.append((float(s[1]), float(s[0]))) + return Polygon(name.text, latlon) return ('Unknown', None, None) +class KMLRead(): + def __init__(self, filepath): + self.filepath = filepath + + def placemark_nodes(self): + return self.tree.findall(".//kml:Placemark", namespaces) + + def readObject(self, innode): + '''reads in a node and returns as a tuple: (type, name, points[])''' + # get name + name = find_tag(innode, 'name') + if name is None: + return None + point = find_tag(innode, 'Point') + if point is not None: + coordinates = find_tag(point, 'coordinates') + if coordinates is None: + return None + s = coordinates.text.split(',') + return Point(name.text, (float(s[1]), float(s[0]))) + + coordinates = find_tag_recursive(innode, 'coordinates') + if coordinates is not None: + # a Polygon + latlon = [] + for c in coordinates.text.split(): + s = c.split(',') + latlon.append((float(s[1]), float(s[0]))) + + styleURL = find_tag_recursive(innode, 'styleUrl') + line_colour = None + if styleURL is not None: + styleurl_name = styleURL.text.lstrip('#') + if styleurl_name in self.stylemap: + stylemap = self.stylemap[styleurl_name] + otherstyle = stylemap.otherstyle + if otherstyle in self.style: + style = self.style[otherstyle] + line_colour = style.line_colour + + return Polygon(name.text, latlon, line_colour=line_colour) + + return ('Unknown', None, None) + + def parse(self): + self.tree = etree_for_filepath(self.filepath) + + # extract styles: + self.style = {} + for s in self.tree.findall(".//kml:Style", namespaces): + _id = s.get("id") + style = Style(_id) + self.style[_id] = style + line_style = find_tag_recursive(s, 'LineStyle') + if line_style is None: + continue + colour = find_tag_recursive(line_style, 'color') + g = re.match("(?P..)(?P..)(?P..)(?P..)", colour.text) + if g is None: + continue + style.line_colour = ( + int(g.group("r"), 16), + int(g.group("g"), 16), + int(g.group("b"), 16) + ) + + # extract stylemaps: + self.stylemap = {} + for s in self.tree.findall(".//kml:StyleMap", namespaces): + _id = s.get("id") + styleurl_obj = None + for pair in s.getchildren(): + if pair is None: + continue + if find_tag_recursive(pair, "key").text == "normal": + styleurl_obj = find_tag_recursive(pair, "styleUrl") + if styleurl_obj is not None: + break + break + + if styleurl_obj is None: + continue + stylemap = StyleMap(_id, styleurl_obj.text.lstrip('#')) + self.stylemap[_id] = stylemap + + if __name__ == '__main__': import sys - nodes = readkmz(sys.argv[1]) - for n in nodes: - print(readObject(n)) + + kml = KMLRead(sys.argv[1]) + kml.parse() + + for n in kml.placemark_nodes(): + obj = kml.readObject(n) + if obj is None: + continue + +# for n in readkmz(sys.argv[1]): +# print(readObject(n)) From b74678135933fd11d96a27e00e684935ea06f119 Mon Sep 17 00:00:00 2001 From: Peter Barker Date: Mon, 17 Mar 2025 19:43:22 +1100 Subject: [PATCH 2/3] mavproxy_kmlread: use alternate interface to get colours for KML --- MAVProxy/modules/mavproxy_kmlread.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/MAVProxy/modules/mavproxy_kmlread.py b/MAVProxy/modules/mavproxy_kmlread.py index 9dda23ec4f..fd621ada09 100644 --- a/MAVProxy/modules/mavproxy_kmlread.py +++ b/MAVProxy/modules/mavproxy_kmlread.py @@ -319,19 +319,20 @@ def clearkml(self): self.curtextlayers = [] self.menu_needs_refreshing = True - def add_polygon(self, name, coords): + def add_polygon(self, name, coords, line_colour=None): '''add a polygon to the KML list. coords is a list of lat/lng tuples in degrees''' self.snap_points.extend(coords) # print("Adding " + name) - newcolour = (random.randint(0, 255), 0, random.randint(0, 255)) + if line_colour is None: + line_colour = (random.randint(0, 255), 0, random.randint(0, 255)) layer_name = f"{name}-{self.counter}" curpoly = mp_slipmap.SlipPolygon( layer_name, coords, layer=2, linewidth=2, - colour=newcolour, + colour=line_colour, ) self.add_map_object(curpoly) self.allayers.append(curpoly) @@ -341,7 +342,9 @@ def add_polygon(self, name, coords): def loadkml(self, filename): '''Load a kml from file and put it on the map''' # Open the zip file - nodes = kmlread.readkmz(filename) + kml = kmlread.KMLRead(filename) + kml.parse() + nodes = kml.placemark_nodes() self.snap_points = [] @@ -351,7 +354,7 @@ def loadkml(self, filename): return for n in nodes: try: - point = kmlread.readObject(n) + point = kml.readObject(n) except Exception: continue if point is None: @@ -359,7 +362,7 @@ def loadkml(self, filename): # and place any polygons on the map if isinstance(point, kmlread.Polygon): - self.add_polygon(point.name, point.vertexes) + self.add_polygon(point.name, point.vertexes, point.line_colour) # and points - barrell image and text if isinstance(point, kmlread.Point): From 4583b18d9712221bc6fc15bf2121c533ce819dba Mon Sep 17 00:00:00 2001 From: Peter Barker Date: Fri, 16 May 2025 23:24:20 +1000 Subject: [PATCH 3/3] kmlread.py: correct for id coming from cascadingstyle not style --- MAVProxy/modules/lib/kmlread.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/MAVProxy/modules/lib/kmlread.py b/MAVProxy/modules/lib/kmlread.py index 8924b79d94..953360da9f 100644 --- a/MAVProxy/modules/lib/kmlread.py +++ b/MAVProxy/modules/lib/kmlread.py @@ -11,7 +11,10 @@ import pathlib import re -namespaces = {'kml': 'http://www.opengis.net/kml/2.2'} +namespaces = { + 'kml': 'http://www.opengis.net/kml/2.2', + 'gx': 'http://www.google.com/kml/ext/2.2', +} class Style(): @@ -167,8 +170,9 @@ def parse(self): # extract styles: self.style = {} - for s in self.tree.findall(".//kml:Style", namespaces): - _id = s.get("id") + for s in self.tree.findall(".//gx:CascadingStyle", namespaces): + idname = f"{{{namespaces['kml']}}}id" + _id = s.get(idname) style = Style(_id) self.style[_id] = style line_style = find_tag_recursive(s, 'LineStyle')