mavkml.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. #!/usr/bin/python
  2. '''
  3. simple kml export for logfiles
  4. Thomas Gubler <thomasgubler@gmail.com>
  5. '''
  6. from __future__ import print_function
  7. from builtins import range
  8. from argparse import ArgumentParser
  9. import simplekml
  10. from pymavlink.mavextra import *
  11. from pymavlink import mavutil
  12. import time
  13. import re
  14. import os
  15. mainstate_field = 'STAT.MainState'
  16. position_field_types = [ # Order must be lon, lat, alt to match KML
  17. ['Lon', 'Lat', 'Alt'], # PX4
  18. ['Lng', 'Lat', 'Alt'] # APM > 3
  19. ]
  20. colors = [simplekml.Color.red, simplekml.Color.green, simplekml.Color.blue,
  21. simplekml.Color.violet, simplekml.Color.yellow, simplekml.Color.orange,
  22. simplekml.Color.burlywood, simplekml.Color.azure, simplekml.Color.lightblue,
  23. simplekml.Color.lawngreen, simplekml.Color.indianred, simplekml.Color.hotpink]
  24. kml = simplekml.Kml()
  25. kml_linestrings = []
  26. def add_to_linestring(position_data, kml_linestring):
  27. '''add a point to the kml file'''
  28. global kml
  29. # add altitude offset
  30. position_data[2] += float(args.aoff)
  31. kml_linestring.coords.addcoordinates([position_data])
  32. def save_kml(filename):
  33. '''saves the kml file'''
  34. global kml
  35. kml.save(filename)
  36. print("KML written to %s" % filename)
  37. def add_data(t, msg, msg_types, vars, fields, field_types, position_field_type):
  38. '''add some data'''
  39. mtype = msg.get_type()
  40. if mtype not in msg_types:
  41. return
  42. for i in range(0, len(fields)):
  43. if mtype not in field_types[i]:
  44. continue
  45. f = fields[i]
  46. v = mavutil.evaluate_expression(f, vars)
  47. if v is None:
  48. continue
  49. # Check if we have position or state information
  50. if f == mainstate_field:
  51. # Handle main state information
  52. # add_data.mainstate_current >= 0 : avoid starting a new linestring
  53. # when mainstate comes after the first position data in the log
  54. if (v != add_data.mainstate_current and
  55. add_data.mainstate_current >= 0):
  56. add_data.new_linestring = True
  57. add_data.mainstate_current = v
  58. else:
  59. # Handle position information
  60. # make sure lon, lat, alt is saved in the correct order in
  61. # position_data (the same as in position_field_types)
  62. add_data.position_data[i] = v
  63. # check if we have a full gps measurement
  64. gps_measurement_ready = True
  65. for v in add_data.position_data:
  66. if v is None:
  67. gps_measurement_ready = False
  68. if not gps_measurement_ready:
  69. return
  70. # if new line string is needed (because of state change): add previous
  71. # linestring to kml_linestrings list, add a new linestring to the kml
  72. # multigeometry and append to the new linestring
  73. # else: append to current linestring
  74. if add_data.new_linestring:
  75. if add_data.current_kml_linestring is not None:
  76. kml_linestrings.append(add_data.current_kml_linestring)
  77. name = "".join([args.source, ":", str(add_data.mainstate_current)])
  78. add_data.current_kml_linestring = \
  79. kml.newlinestring(name=name, altitudemode='absolute')
  80. # set rendering options
  81. if args.extrude:
  82. add_data.current_kml_linestring.extrude = 1
  83. add_data.current_kml_linestring.style.linestyle.color = \
  84. colors[max([add_data.mainstate_current, 0])]
  85. add_data.new_linestring = False
  86. add_to_linestring(add_data.position_data,
  87. add_data.current_kml_linestring)
  88. add_data.last_time = msg._timestamp
  89. else:
  90. if (msg._timestamp - add_data.last_time) > 0.1:
  91. add_to_linestring(add_data.position_data,
  92. add_data.current_kml_linestring)
  93. add_data.last_time = msg._timestamp
  94. # reset position_data
  95. add_data.position_data = [None for n in position_field_type]
  96. def process_file(filename, source):
  97. '''process one file'''
  98. print("Processing %s" % filename)
  99. mlog = mavutil.mavlink_connection(filename, notimestamps=args.notimestamps)
  100. position_field_type = sniff_field_spelling(mlog, source)
  101. # init fields and field_types lists
  102. fields = [args.source + "." + s for s in position_field_type]
  103. fields.append(mainstate_field)
  104. field_types = []
  105. msg_types = set()
  106. re_caps = re.compile('[A-Z_][A-Z0-9_]+')
  107. for f in fields:
  108. caps = set(re.findall(re_caps, f))
  109. msg_types = msg_types.union(caps)
  110. field_types.append(caps)
  111. add_data.new_linestring = True
  112. add_data.mainstate_current = -1
  113. add_data.current_kml_linestring = None
  114. add_data.position_data = [None for n in position_field_type]
  115. add_data.last_time = 0
  116. while True:
  117. msg = mlog.recv_match(args.condition)
  118. if msg is None:
  119. break
  120. tdays = (msg._timestamp - time.timezone) / (24 * 60 * 60)
  121. tdays += 719163 # pylab wants it since 0001-01-01
  122. add_data(tdays, msg, msg_types, mlog.messages, fields, field_types, position_field_type)
  123. def sniff_field_spelling(mlog, source):
  124. '''attempt to detect whether APM or PX4 attributes names are in use'''
  125. position_field_type_default = position_field_types[0] # Default to PX4 spelling
  126. msg = mlog.recv_match(source)
  127. mlog._rewind() # Unfortunately it's either call this or return a mutated object
  128. position_field_selection = [spelling for spelling in position_field_types if hasattr(msg, spelling[0])]
  129. return position_field_selection[0] if position_field_selection else position_field_type_default
  130. if __name__ == '__main__':
  131. parser = ArgumentParser(description=__doc__)
  132. parser.add_argument("--no-timestamps", dest="notimestamps",
  133. action='store_true', help="Log doesn't have timestamps")
  134. parser.add_argument("--condition", default=None,
  135. help="select packets by a condition [default: %(default)s]")
  136. parser.add_argument("--aoff", default=0.,
  137. help="Altitude offset for paths that go through the"
  138. "ground in google earth [default: %(default)s]")
  139. parser.add_argument("-o", "--output", dest="filename_out", default="mav.kml",
  140. help="Output filename [default: %(default)s] ")
  141. parser.add_argument("-s", "--source", default="GPOS",
  142. help="Select position data source"
  143. "(GPOS or GPS) [default: %(default)s]")
  144. parser.add_argument("-e", "--extrude", default=False,
  145. action='store_true',
  146. help="Extrude paths to ground [default: %(default)s]")
  147. parser.add_argument("logs", metavar="LOG", nargs="+")
  148. args = parser.parse_args()
  149. filenames = []
  150. for f in args.logs:
  151. if os.path.exists(f):
  152. filenames.append(f)
  153. if len(filenames) == 0:
  154. print("No files to process")
  155. sys.exit(1)
  156. for fi in range(0, len(filenames)):
  157. f = filenames[fi]
  158. process_file(f, args.source)
  159. save_kml(args.filename_out)