Skip to content

Use colours specified in KML file by preference #1530

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 128 additions & 9 deletions MAVProxy/modules/lib/kmlread.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,31 @@
from io import BytesIO as SIO
from zipfile import ZipFile
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():
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():
Expand All @@ -27,6 +44,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)
Expand All @@ -48,10 +74,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):
Expand Down Expand Up @@ -92,13 +115,109 @@ 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(".//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')
if line_style is None:
continue
colour = find_tag_recursive(line_style, 'color')
g = re.match("(?P<alpha>..)(?P<r>..)(?P<g>..)(?P<b>..)", 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))
15 changes: 9 additions & 6 deletions MAVProxy/modules/mavproxy_kmlread.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 = []

Expand All @@ -351,15 +354,15 @@ 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:
continue

# 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):
Expand Down