Beispiel #1
0
    def post(self, *args, **kwargs):
        """ POST request """

        data = tornado.escape.json_decode(self.request.body)

        log_id = data['log']
        if not validate_log_id(log_id):
            raise tornado.web.HTTPError(400, 'Invalid Parameter')

        error_ids = data['labels']
        if not validate_error_ids(error_ids):
            raise tornado.web.HTTPError(400, 'Invalid Parameter')

        error_id_str = ""
        for error_ix, error_id in enumerate(error_ids):
            error_id_str += str(error_id)
            if error_ix < len(error_ids) - 1:
                error_id_str += ","

        con = sqlite3.connect(get_db_filename(),
                              detect_types=sqlite3.PARSE_DECLTYPES)
        cur = con.cursor()

        cur.execute('UPDATE Logs SET ErrorLabels = ? WHERE Id = ?',
                    (error_id_str, log_id))

        con.commit()
        cur.close()
        con.close()

        self.write('OK')
    def get(self):
        log_id = self.get_argument('log')
        if not validate_log_id(log_id):
            raise tornado.web.HTTPError(400, 'Invalid Parameter')
        log_file_name = get_log_filename(log_id)
        download_type = self.get_argument('type', default='0')
        if not os.path.exists(log_file_name):
            raise tornado.web.HTTPError(404, 'Log not found')

        if download_type == '1':  # download the parameters
            ulog = load_ulog_file(log_file_name)
            param_keys = sorted(ulog.initial_parameters.keys())

            self.set_header("Content-Type", "text/plain")
            self.set_header('Content-Disposition',
                            'inline; filename=params.txt')

            delimiter = ', '
            for param_key in param_keys:
                self.write(param_key)
                self.write(delimiter)
                self.write(str(ulog.initial_parameters[param_key]))
                self.write('\n')

        elif download_type == '2':  # download the kml file
            kml_path = get_kml_filepath()
            kml_file_name = os.path.join(kml_path,
                                         log_id.replace('/', '.') + '.kml')

            # check if chached file exists
            if not os.path.exists(kml_file_name):
                print('need to create kml file', kml_file_name)

                def kml_colors(flight_mode):
                    """ flight mode colors for KML file """
                    if not flight_mode in flight_modes_table: flight_mode = 0

                    color_str = flight_modes_table[flight_mode][1][
                        1:]  # color in form 'ff00aa'

                    # increase brightness to match colors with template
                    rgb = [
                        int(color_str[2 * x:2 * x + 2], 16) for x in range(3)
                    ]
                    for i in range(3):
                        rgb[i] += 40
                        if rgb[i] > 255: rgb[i] = 255

                    color_str = "".join(map(lambda x: format(x, '02x'), rgb))

                    return 'ff' + color_str[4:6] + color_str[2:4] + color_str[
                        0:2]  # KML uses aabbggrr

                style = {'line_width': 2}
                # create in random temporary file, then move it (to avoid races)
                try:
                    temp_file_name = kml_file_name + '.' + str(uuid.uuid4())
                    convert_ulog2kml(log_file_name,
                                     temp_file_name,
                                     'vehicle_global_position',
                                     kml_colors,
                                     style=style)
                    shutil.move(temp_file_name, kml_file_name)
                except:
                    print('Error creating KML file',
                          sys.exc_info()[0],
                          sys.exc_info()[1])
                    raise CustomHTTPError(400, 'No Position Data in log')

            # send the whole KML file
            self.set_header("Content-Type",
                            "application/vnd.google-earth.kml+xml")
            self.set_header('Content-Disposition',
                            'attachment; filename=track.kml')
            with open(kml_file_name, 'rb') as kml_file:
                while True:
                    data = kml_file.read(4096)
                    if not data:
                        break
                    self.write(data)
                self.finish()

        elif download_type == '3':  # download the non-default parameters
            ulog = load_ulog_file(log_file_name)
            param_keys = sorted(ulog.initial_parameters.keys())

            self.set_header("Content-Type", "text/plain")
            self.set_header('Content-Disposition',
                            'inline; filename=params.txt')

            default_params = get_default_parameters()

            delimiter = ', '
            for param_key in param_keys:
                try:
                    param_value = str(ulog.initial_parameters[param_key])
                    is_default = False

                    if param_key in default_params:
                        default_param = default_params[param_key]
                        if default_param['type'] == 'FLOAT':
                            is_default = abs(
                                float(default_param['default']) -
                                float(param_value)) < 0.00001
                        else:
                            is_default = int(
                                default_param['default']) == int(param_value)

                    if not is_default:
                        self.write(param_key)
                        self.write(delimiter)
                        self.write(param_value)
                        self.write('\n')
                except:
                    pass

        else:  # download the log file
            self.set_header('Content-Type', 'application/octet-stream')
            self.set_header("Content-Description", "File Transfer")
            self.set_header(
                'Content-Disposition', 'attachment; filename={}'.format(
                    os.path.basename(log_file_name)))
            with open(log_file_name, 'rb') as log_file:
                while True:
                    data = log_file.read(4096)
                    if not data:
                        break
                    self.write(data)
                self.finish()
Beispiel #3
0
    def get(self, *args, **kwargs):
        """ GET request callback """

        # load the log file
        log_id = self.get_argument('log')
        if not validate_log_id(log_id):
            raise tornado.web.HTTPError(400, 'Invalid Parameter')
        log_file_name = get_log_filename(log_id)
        ulog = load_ulog_file(log_file_name)

        # extract the necessary information from the log

        try:
            # required topics: none of these are optional
            gps_pos = ulog.get_dataset('vehicle_gps_position').data
            vehicle_global_position = ulog.get_dataset(
                'vehicle_global_position').data
            attitude = ulog.get_dataset('vehicle_attitude').data
        except (KeyError, IndexError, ValueError) as error:
            raise CustomHTTPError(
                400, 'The log does not contain all required topics<br />'
                '(vehicle_gps_position, vehicle_global_position, '
                'vehicle_attitude)') from error

        # manual control setpoint is optional
        manual_control_setpoint = None
        try:
            manual_control_setpoint = ulog.get_dataset(
                'manual_control_setpoint').data
        except (KeyError, IndexError, ValueError) as error:
            pass

        # Get the takeoff location. We use the first position with a valid fix,
        # and assume that the vehicle is not in the air already at that point
        takeoff_index = 0
        gps_indices = np.nonzero(gps_pos['fix_type'] > 2)
        if len(gps_indices[0]) > 0:
            takeoff_index = gps_indices[0][0]
        takeoff_altitude = '{:.3f}' \
            .format(gps_pos['alt'][takeoff_index] * 1.e-3)
        takeoff_latitude = '{:.10f}'.format(gps_pos['lat'][takeoff_index] *
                                            1.e-7)
        takeoff_longitude = '{:.10f}'.format(gps_pos['lon'][takeoff_index] *
                                             1.e-7)

        # calculate UTC time offset (assume there's no drift over the entire log)
        utc_offset = int(gps_pos['time_utc_usec'][takeoff_index]) - \
                int(gps_pos['timestamp'][takeoff_index])

        # flight modes
        flight_mode_changes = get_flight_mode_changes(ulog)
        flight_modes_str = '[ '
        for t, mode in flight_mode_changes:
            t += utc_offset
            utctimestamp = datetime.datetime.utcfromtimestamp(
                t / 1.e6).replace(tzinfo=datetime.timezone.utc)
            if mode in flight_modes_table:
                mode_name, color = flight_modes_table[mode]
            else:
                mode_name = ''
                color = '#ffffff'
            flight_modes_str += '["{:}", "{:}"], ' \
                .format(utctimestamp.isoformat(), mode_name)
        flight_modes_str += ' ]'

        # manual control setpoints (stick input)
        manual_control_setpoints_str = '[ '
        if manual_control_setpoint:
            for i in range(len(manual_control_setpoint['timestamp'])):
                manual_x = manual_control_setpoint['x'][i]
                manual_y = manual_control_setpoint['y'][i]
                manual_z = manual_control_setpoint['z'][i]
                manual_r = manual_control_setpoint['r'][i]
                t = manual_control_setpoint['timestamp'][i] + utc_offset
                utctimestamp = datetime.datetime.utcfromtimestamp(
                    t / 1.e6).replace(tzinfo=datetime.timezone.utc)
                manual_control_setpoints_str += '["{:}", {:.3f}, {:.3f}, {:.3f}, {:.3f}], ' \
                    .format(utctimestamp.isoformat(), manual_x, manual_y, manual_z, manual_r)
        manual_control_setpoints_str += ' ]'

        # position
        # Note: alt_ellipsoid from gps_pos would be the better match for
        # altitude, but it's not always available. And since we add an offset
        # (to match the takeoff location with the ground altitude) it does not
        # matter as much.
        position_data = '[ '
        # TODO: use vehicle_global_position? If so, then:
        # - altitude requires an offset (to match the GPS data)
        # - it's worse for some logs where the estimation is bad -> acro flights
        #   (-> add both: user-selectable between GPS & estimated trajectory?)
        for i in range(len(gps_pos['timestamp'])):
            lon = gps_pos['lon'][i] * 1.e-7
            lat = gps_pos['lat'][i] * 1.e-7
            alt = gps_pos['alt'][i] * 1.e-3
            t = gps_pos['timestamp'][i] + utc_offset
            utctimestamp = datetime.datetime.utcfromtimestamp(
                t / 1.e6).replace(tzinfo=datetime.timezone.utc)
            if i == 0:
                start_timestamp = utctimestamp
            end_timestamp = utctimestamp
            position_data += '["{:}", {:.10f}, {:.10f}, {:.3f}], ' \
                .format(utctimestamp.isoformat(), lon, lat, alt)
        position_data += ' ]'

        start_timestamp_str = '"{:}"'.format(start_timestamp.isoformat())
        boot_timestamp = datetime.datetime.utcfromtimestamp(
            utc_offset / 1.e6).replace(tzinfo=datetime.timezone.utc)
        boot_timestamp_str = '"{:}"'.format(boot_timestamp.isoformat())
        end_timestamp_str = '"{:}"'.format(end_timestamp.isoformat())

        # orientation as quaternion
        attitude_data = '[ '
        for i in range(len(attitude['timestamp'])):
            att_qw = attitude['q[0]'][i]
            att_qx = attitude['q[1]'][i]
            att_qy = attitude['q[2]'][i]
            att_qz = attitude['q[3]'][i]
            t = attitude['timestamp'][i] + utc_offset
            utctimestamp = datetime.datetime.utcfromtimestamp(
                t / 1.e6).replace(tzinfo=datetime.timezone.utc)
            # Cesium uses (x, y, z, w)
            attitude_data += '["{:}", {:.6f}, {:.6f}, {:.6f}, {:.6f}], ' \
                .format(utctimestamp.isoformat(), att_qx, att_qy, att_qz, att_qw)
        attitude_data += ' ]'

        # handle different vehicle types
        # the model_scale_factor should scale the different models to make them
        # equal in size (in proportion)
        mav_type = ulog.initial_parameters.get('MAV_TYPE', None)
        if mav_type == 1:  # fixed wing
            model_scale_factor = 0.06
            model_uri = 'plot_app/static/cesium/SampleData/models/CesiumAir/Cesium_Air.glb'
        elif mav_type == 2:  # quad
            model_scale_factor = 1
            model_uri = 'plot_app/static/cesium/models/iris/iris.glb'
        elif mav_type == 22:  # delta-quad
            # TODO: use the delta-quad model
            model_scale_factor = 0.06
            model_uri = 'plot_app/static/cesium/SampleData/models/CesiumAir/Cesium_Air.glb'
        else:  # TODO: handle more types
            model_scale_factor = 1
            model_uri = 'plot_app/static/cesium/models/iris/iris.glb'

        template = get_jinja_env().get_template(THREED_TEMPLATE)
        self.write(
            template.render(
                flight_modes=flight_modes_str,
                manual_control_setpoints=manual_control_setpoints_str,
                takeoff_altitude=takeoff_altitude,
                takeoff_longitude=takeoff_longitude,
                takeoff_latitude=takeoff_latitude,
                position_data=position_data,
                start_timestamp=start_timestamp_str,
                boot_timestamp=boot_timestamp_str,
                end_timestamp=end_timestamp_str,
                attitude_data=attitude_data,
                model_scale_factor=model_scale_factor,
                model_uri=model_uri,
                log_id=log_id,
                bing_api_key=get_bing_maps_api_key(),
                cesium_api_key=get_cesium_api_key()))
    def get(self, *args, **kwargs):
        """ GET request callback """
        log_id = self.get_argument('log')
        if not validate_log_id(log_id):
            raise tornado.web.HTTPError(400, 'Invalid Parameter')
        log_file_name = get_log_filename(log_id)
        download_type = self.get_argument('type', default='0')
        if not os.path.exists(log_file_name):
            raise tornado.web.HTTPError(404, 'Log not found')


        def get_original_filename(default_value, new_file_suffix):
            """
            get the uploaded file name & exchange the file extension
            """
            try:
                con = sqlite3.connect(get_db_filename(), detect_types=sqlite3.PARSE_DECLTYPES)
                cur = con.cursor()
                cur.execute('select OriginalFilename '
                            'from Logs where Id = ?', [log_id])
                db_tuple = cur.fetchone()
                if db_tuple is not None:
                    original_file_name = escape(db_tuple[0])
                    if original_file_name[-4:].lower() == '.ulg':
                        original_file_name = original_file_name[:-4]
                    return original_file_name + new_file_suffix
                cur.close()
                con.close()
            except:
                print("DB access failed:", sys.exc_info()[0], sys.exc_info()[1])
            return default_value


        if download_type == '1': # download the parameters
            ulog = load_ulog_file(log_file_name)
            param_keys = sorted(ulog.initial_parameters.keys())

            self.set_header("Content-Type", "text/plain")
            self.set_header('Content-Disposition', 'inline; filename=params.txt')

            delimiter = ', '
            for param_key in param_keys:
                self.write(param_key)
                self.write(delimiter)
                self.write(str(ulog.initial_parameters[param_key]))
                self.write('\n')

        elif download_type == '2': # download the kml file
            kml_path = get_kml_filepath()
            kml_file_name = os.path.join(kml_path, log_id.replace('/', '.')+'.kml')

            # check if chached file exists
            if not os.path.exists(kml_file_name):
                print('need to create kml file', kml_file_name)

                def kml_colors(flight_mode):
                    """ flight mode colors for KML file """
                    if not flight_mode in flight_modes_table: flight_mode = 0

                    color_str = flight_modes_table[flight_mode][1][1:] # color in form 'ff00aa'

                    # increase brightness to match colors with template
                    rgb = [int(color_str[2*x:2*x+2], 16) for x in range(3)]
                    for i in range(3):
                        rgb[i] += 40
                        if rgb[i] > 255: rgb[i] = 255

                    color_str = "".join(map(lambda x: format(x, '02x'), rgb))

                    return 'ff'+color_str[4:6]+color_str[2:4]+color_str[0:2] # KML uses aabbggrr

                style = {'line_width': 2}
                # create in random temporary file, then move it (to avoid races)
                try:
                    temp_file_name = kml_file_name+'.'+str(uuid.uuid4())
                    convert_ulog2kml(log_file_name, temp_file_name,
                                     'vehicle_global_position', kml_colors,
                                     style=style,
                                     camera_trigger_topic_name='camera_capture')
                    shutil.move(temp_file_name, kml_file_name)
                except Exception as e:
                    print('Error creating KML file', sys.exc_info()[0], sys.exc_info()[1])
                    raise CustomHTTPError(400, 'No Position Data in log') from e


            kml_dl_file_name = get_original_filename('track.kml', '.kml')

            # send the whole KML file
            self.set_header("Content-Type", "application/vnd.google-earth.kml+xml")
            self.set_header('Content-Disposition', 'attachment; filename='+kml_dl_file_name)
            with open(kml_file_name, 'rb') as kml_file:
                while True:
                    data = kml_file.read(4096)
                    if not data:
                        break
                    self.write(data)
                self.finish()

        elif download_type == '3': # download the non-default parameters
            ulog = load_ulog_file(log_file_name)
            param_keys = sorted(ulog.initial_parameters.keys())

            self.set_header("Content-Type", "text/plain")
            self.set_header('Content-Disposition', 'inline; filename=params.txt')

            default_params = get_default_parameters()

            delimiter = ', '
            for param_key in param_keys:
                try:
                    param_value = str(ulog.initial_parameters[param_key])
                    is_default = False

                    if param_key in default_params:
                        default_param = default_params[param_key]
                        if default_param['type'] == 'FLOAT':
                            is_default = abs(float(default_param['default']) -
                                             float(param_value)) < 0.00001
                        else:
                            is_default = int(default_param['default']) == int(param_value)

                    if not is_default:
                        self.write(param_key)
                        self.write(delimiter)
                        self.write(param_value)
                        self.write('\n')
                except:
                    pass

        else: # download the log file
            self.set_header('Content-Type', 'application/octet-stream')
            self.set_header("Content-Description", "File Transfer")
            self.set_header('Content-Disposition', 'attachment; filename={}'.format(
                os.path.basename(log_file_name)))
            with open(log_file_name, 'rb') as log_file:
                while True:
                    data = log_file.read(4096)
                    if not data:
                        break
                    self.write(data)
                self.finish()
Beispiel #5
0
    def get(self, *args, **kwargs):
        """ GET request callback """

        # load the log file
        log_id = self.get_argument('log')
        if not validate_log_id(log_id):
            raise tornado.web.HTTPError(400, 'Invalid Parameter')
        log_file_name = get_log_filename(log_id)
        ulog = load_ulog_file(log_file_name)

        # extract the necessary information from the log

        try:
            # required topics: none of these are optional
            gps_pos = ulog.get_dataset('vehicle_gps_position').data
            vehicle_global_position = ulog.get_dataset('vehicle_global_position').data
            attitude = ulog.get_dataset('vehicle_attitude').data
        except (KeyError, IndexError, ValueError) as error:
            raise CustomHTTPError(
                400,
                'The log does not contain all required topics<br />'
                '(vehicle_gps_position, vehicle_global_position, '
                'vehicle_attitude)')

        # These are optional data streams. Most of them are added
        # for charting/streaming 2D plots on the 3D viewer.
        manual_control_setpoint = None
        vehicle_local_position = None
        vehicle_local_position_setpoint = None
        vehicle_attitude_setpoint = None
        vehicle_rates_setpoint = None
        actuator_outputs = None
        sensor_combined = None
        actuator_controls_0 = None

        # Exception handling is done on each dataset separately to find
        # source of the missing stream.

        # Exception: manual_control_setpoint
        try:
            manual_control_setpoint = ulog.get_dataset('manual_control_setpoint').data
        except (KeyError, IndexError, ValueError) as error:
            print("Manual control setpoint not found")

        # Exception: vehicle_local_position
        try:
            vehicle_local_position = ulog.get_dataset('vehicle_local_position').data
        except (KeyError, IndexError, ValueError) as error:
            print("Vehicle local position not found")

        # Exception: vehicle_local_position_setpoint
        try:
            vehicle_local_position_setpoint = ulog.get_dataset('vehicle_local_position_setpoint').data
        except (KeyError, IndexError, ValueError) as error:
            print("Vehicle local position setpoint not found")   

        # Exception: vehicle_attitude_setpoint
        try:
            vehicle_attitude_setpoint = ulog.get_dataset('vehicle_attitude_setpoint').data
        except (KeyError, IndexError, ValueError) as error:
            print("Vehicle attitude setpoint not found")                       

        # Exception: vehicle_rates_setpoint
        try:
            vehicle_rates_setpoint = ulog.get_dataset('vehicle_rates_setpoint').data
        except (KeyError, IndexError, ValueError) as error:
            print("Vehicle rates setpoint not found")  

        # Exception: actuator_outputs
        try:
            actuator_outputs = ulog.get_dataset('actuator_outputs').data
        except (KeyError, IndexError, ValueError) as error:
            print("Actuator output not found")                  

        # Exception: sensor_combined
        try:
            sensor_combined = ulog.get_dataset('sensor_combined').data
        except (KeyError, IndexError, ValueError) as error:
            print("Sensor combined not found")      

        # Exception: actuator_controls_0
        try:
            actuator_controls_0 = ulog.get_dataset('actuator_controls_0').data
        except (KeyError, IndexError, ValueError) as error:
            print("Actuator Controls 0 not found")   

        # Get the takeoff location. We use the first position with a valid fix,
        # and assume that the vehicle is not in the air already at that point
        takeoff_index = 0
        gps_indices = np.nonzero(gps_pos['fix_type'] > 2)
        if len(gps_indices[0]) > 0:
            takeoff_index = gps_indices[0][0]
        takeoff_altitude = '{:.3f}' \
            .format(gps_pos['alt'][takeoff_index] * 1.e-3)
        takeoff_latitude = '{:.10f}'.format(gps_pos['lat'][takeoff_index] * 1.e-7)
        takeoff_longitude = '{:.10f}'.format(gps_pos['lon'][takeoff_index] * 1.e-7)


        # calculate UTC time offset (assume there's no drift over the entire log)
        utc_offset = int(gps_pos['time_utc_usec'][takeoff_index]) - \
                int(gps_pos['timestamp'][takeoff_index])

        # flight modes
        flight_mode_changes = get_flight_mode_changes(ulog)
        flight_modes_str = '[ '
        for t, mode in flight_mode_changes:
            t += utc_offset
            utctimestamp = datetime.datetime.utcfromtimestamp(t/1.e6).replace(
                tzinfo=datetime.timezone.utc)
            if mode in flight_modes_table:
                mode_name, color = flight_modes_table[mode]
            else:
                mode_name = ''
                color = '#ffffff'
            flight_modes_str += '["{:}", "{:}"], ' \
                .format(utctimestamp.isoformat(), mode_name)
        flight_modes_str += ' ]'

        # manual control setpoints (stick input)
        manual_control_setpoints_str = '[ '
        if manual_control_setpoint:
            for i in range(len(manual_control_setpoint['timestamp'])):
                manual_x = manual_control_setpoint['x'][i]
                manual_y = manual_control_setpoint['y'][i]
                manual_z = manual_control_setpoint['z'][i]
                manual_r = manual_control_setpoint['r'][i]
                t = manual_control_setpoint['timestamp'][i] + utc_offset
                utctimestamp = datetime.datetime.utcfromtimestamp(t/1.e6).replace(
                    tzinfo=datetime.timezone.utc)
                manual_control_setpoints_str += '["{:}", {:.3f}, {:.3f}, {:.3f}, {:.3f}], ' \
                    .format(utctimestamp.isoformat(), manual_x, manual_y, manual_z, manual_r)
        manual_control_setpoints_str += ' ]'


        # position
        # Note: alt_ellipsoid from gps_pos would be the better match for
        # altitude, but it's not always available. And since we add an offset
        # (to match the takeoff location with the ground altitude) it does not
        # matter as much.
        position_data = '[ '
        # TODO: use vehicle_global_position? If so, then:
        # - altitude requires an offset (to match the GPS data)
        # - it's worse for some logs where the estimation is bad -> acro flights
        #   (-> add both: user-selectable between GPS & estimated trajectory?)
        for i in range(len(gps_pos['timestamp'])):
            lon = gps_pos['lon'][i] * 1.e-7
            lat = gps_pos['lat'][i] * 1.e-7
            alt = gps_pos['alt'][i] * 1.e-3
            t = gps_pos['timestamp'][i] + utc_offset
            utctimestamp = datetime.datetime.utcfromtimestamp(t/1.e6).replace(
                tzinfo=datetime.timezone.utc)
            if i == 0:
                start_timestamp = utctimestamp
            end_timestamp = utctimestamp
            position_data += '["{:}", {:.10f}, {:.10f}, {:.3f}], ' \
                .format(utctimestamp.isoformat(), lon, lat, alt)
        position_data += ' ]'

        start_timestamp_str = '"{:}"'.format(start_timestamp.isoformat())
        boot_timestamp = datetime.datetime.utcfromtimestamp(utc_offset/1.e6).replace(
            tzinfo=datetime.timezone.utc)
        boot_timestamp_str = '"{:}"'.format(boot_timestamp.isoformat())
        end_timestamp_str = '"{:}"'.format(end_timestamp.isoformat())

        # orientation as quaternion
        attitude_data = '[ '
        for i in range(len(attitude['timestamp'])):
            att_qw = attitude['q[0]'][i]
            att_qx = attitude['q[1]'][i]
            att_qy = attitude['q[2]'][i]
            att_qz = attitude['q[3]'][i]
            rollSpeed = attitude['rollspeed'][i]
            pitchSpeed = attitude['pitchspeed'][i]
            yawSpeed = attitude['yawspeed'][i]
            t = attitude['timestamp'][i] + utc_offset
            utctimestamp = datetime.datetime.utcfromtimestamp(t/1.e6).replace(
                tzinfo=datetime.timezone.utc)
            # Cesium uses (x, y, z, w)
            attitude_data += '["{:}", {:.6f}, {:.6f}, {:.6f}, {:.6f}, {:.6f}, {:.6f}, {:.6f}], ' \
                .format(utctimestamp.isoformat(), att_qx, att_qy, att_qz, att_qw, rollSpeed, pitchSpeed, yawSpeed)
        attitude_data += ' ]'

        # Optional data stream serialization starts here.
        # The code checks None condition to decide whether
        # to serialize or not.

        # Attitude setpoint data
        vehicle_rates_setpoint_data = '[ '
        if vehicle_rates_setpoint is not None:
            for i in range(len(vehicle_rates_setpoint['timestamp'])):
                rollRateSP = vehicle_rates_setpoint['roll'][i]
                pitchRateSP = vehicle_rates_setpoint['pitch'][i]
                yawRateSp = vehicle_rates_setpoint['yaw'][i]
                t = vehicle_rates_setpoint['timestamp'][i] + utc_offset
                utctimestamp = datetime.datetime.utcfromtimestamp(t/1.e6).replace(
                    tzinfo=datetime.timezone.utc)
                vehicle_rates_setpoint_data += '["{:}", {:.6f}, {:.6f}, {:.6f}], ' \
                    .format(utctimestamp.isoformat(), rollRateSP, pitchRateSP, yawRateSp)
        vehicle_rates_setpoint_data += ' ]'        

        # Sensor combined data. Includes things like raw gyro, raw accelleration.
        sensor_combined_data = '[ '
        if sensor_combined is not None:
            for i in range(len(sensor_combined['timestamp'])):
                rawRoll = sensor_combined['gyro_rad[0]'][i]
                rawPitch = sensor_combined['gyro_rad[1]'][i]
                rawYaw = sensor_combined['gyro_rad[2]'][i]
                rawXAcc = sensor_combined['accelerometer_m_s2[0]'][i]
                rawYAcc = sensor_combined['accelerometer_m_s2[1]'][i]
                rawZAcc = sensor_combined['accelerometer_m_s2[2]'][i]            
                t = sensor_combined['timestamp'][i] + utc_offset
                utctimestamp = datetime.datetime.utcfromtimestamp(t/1.e6).replace(
                    tzinfo=datetime.timezone.utc)
                sensor_combined_data += '["{:}", {:.6f}, {:.6f}, {:.6f}, {:.6f}, {:.6f}, {:.6f}], ' \
                    .format(utctimestamp.isoformat(), rawRoll, rawPitch, rawYaw, rawXAcc, rawYAcc, rawZAcc)
        sensor_combined_data += ' ]'         

        # Attitude setpoint
        vehicle_attitude_setpoint_data = '[ '
        if vehicle_attitude_setpoint is not None:
            for i in range(len(vehicle_attitude_setpoint['timestamp'])):
                rollSP = vehicle_attitude_setpoint['roll_body'][i]
                pitchSP = vehicle_attitude_setpoint['pitch_body'][i]
                yawSP = vehicle_attitude_setpoint['yaw_body'][i]

                t = vehicle_attitude_setpoint['timestamp'][i] + utc_offset
                utctimestamp = datetime.datetime.utcfromtimestamp(t/1.e6).replace(
                    tzinfo=datetime.timezone.utc)
                vehicle_attitude_setpoint_data += '["{:}", {:.6f}, {:.6f}, {:.6f}], ' \
                    .format(utctimestamp.isoformat(), rollSP, pitchSP, yawSP)
        vehicle_attitude_setpoint_data += ' ]'    

        # Local Position
        vehicle_local_position_data = '[ '
        if vehicle_local_position is not None:
            for i in range(len(vehicle_local_position['timestamp'])):
                xPos = vehicle_local_position['x'][i]
                yPos = vehicle_local_position['y'][i]
                zPos = vehicle_local_position['z'][i]
                xVel = vehicle_local_position['vx'][i]
                yVel = vehicle_local_position['vy'][i]
                zVel = vehicle_local_position['vz'][i]            

                t = vehicle_local_position['timestamp'][i] + utc_offset
                utctimestamp = datetime.datetime.utcfromtimestamp(t/1.e6).replace(
                    tzinfo=datetime.timezone.utc)
                vehicle_local_position_data += '["{:}", {:.6f}, {:.6f}, {:.6f}, {:.6f}, {:.6f}, {:.6f}], ' \
                    .format(utctimestamp.isoformat(), xPos, yPos, zPos, xVel, yVel, zVel)
        vehicle_local_position_data += ' ]'   

        # Local Position Setpoint
        vehicle_local_position_setpoint_data = '[ '
        if vehicle_local_position_setpoint is not None:
            for i in range(len(vehicle_local_position_setpoint['timestamp'])):
                xPosSP = vehicle_local_position_setpoint['x'][i]
                yPosSP = vehicle_local_position_setpoint['y'][i]
                zPosSP = vehicle_local_position_setpoint['z'][i]
                xVelSP = vehicle_local_position_setpoint['vx'][i]
                yVelSP = vehicle_local_position_setpoint['vy'][i]
                zVelSP = vehicle_local_position_setpoint['vz'][i]

                t = vehicle_local_position_setpoint['timestamp'][i] + utc_offset
                utctimestamp = datetime.datetime.utcfromtimestamp(t/1.e6).replace(
                    tzinfo=datetime.timezone.utc)
                vehicle_local_position_setpoint_data += '["{:}", {:.6f}, {:.6f}, {:.6f}, {:.6f}, {:.6f}, {:.6f}], ' \
                    .format(utctimestamp.isoformat(), xPosSP, yPosSP, zPosSP, xVelSP, yVelSP, zVelSP)
        vehicle_local_position_setpoint_data += ' ]'       

        # Actuator Outputs. This can handle airframes up to 8 actuation outputs (i.e. motors).
        # Tons of formatting things ...

        actuator_outputs_data = '[ '
        if actuator_outputs is not None:
            num_actuator_outputs = 8
            max_outputs = np.amax(actuator_outputs['noutputs'])
            if max_outputs < num_actuator_outputs: num_actuator_outputs = max_outputs

            
            for i in range(len(actuator_outputs['timestamp'])):

                t = actuator_outputs['timestamp'][i] + utc_offset
                utctimestamp = datetime.datetime.utcfromtimestamp(t/1.e6).replace(
                    tzinfo=datetime.timezone.utc)            

                actuatorList = []
                actuatorList.append(utctimestamp.isoformat())
                actuatorDictionary={}
                formatStringLoop = ''
                for x in range(max_outputs):
                    actuatorDictionary["actuator_outputs_{0}".format(x)]=actuator_outputs['output['+str(x)+']'][i]
                    formatStringLoop += ', {:.6f}'
                    actuatorList.append(actuatorDictionary["actuator_outputs_{0}".format(x)])
                formatStringLoop += '], '   
                formatString = '["{:}"' + formatStringLoop

                actuator_outputs_data += formatString.format(*actuatorList)
        actuator_outputs_data += ' ]'          

        # Actuator controls
        actuator_controls_0_data = '[ '
        if actuator_controls_0 is not None:
            for i in range(len(actuator_controls_0['timestamp'])):
                cont0 = actuator_controls_0['control[0]'][i]
                cont1 = actuator_controls_0['control[1]'][i]
                cont2 = actuator_controls_0['control[2]'][i]
                cont3 = actuator_controls_0['control[3]'][i]

                t = actuator_controls_0['timestamp'][i] + utc_offset
                utctimestamp = datetime.datetime.utcfromtimestamp(t/1.e6).replace(
                    tzinfo=datetime.timezone.utc)
                actuator_controls_0_data += '["{:}", {:.6f}, {:.6f}, {:.6f}, {:.6f}], ' \
                    .format(utctimestamp.isoformat(), cont0, cont1, cont2, cont3)
        actuator_controls_0_data += ' ]'     

        # handle different vehicle types
        # the model_scale_factor should scale the different models to make them
        # equal in size (in proportion)
        mav_type = ulog.initial_parameters.get('MAV_TYPE', None)
        if mav_type == 1: # fixed wing
            model_scale_factor = 0.06
            model_uri = 'plot_app/static/cesium/SampleData/models/CesiumAir/Cesium_Air.glb'
        elif mav_type == 2: # quad
            model_scale_factor = 1
            model_uri = 'plot_app/static/cesium/models/iris/iris.glb'
        elif mav_type == 22: # delta-quad
            # TODO: use the delta-quad model
            model_scale_factor = 0.06
            model_uri = 'plot_app/static/cesium/SampleData/models/CesiumAir/Cesium_Air.glb'
        else: # TODO: handle more types
            model_scale_factor = 1
            model_uri = 'plot_app/static/cesium/models/iris/iris.glb'

        template = get_jinja_env().get_template(THREED_TEMPLATE)
        self.write(template.render(
            flight_modes=flight_modes_str,
            manual_control_setpoints=manual_control_setpoints_str,
            takeoff_altitude=takeoff_altitude,
            takeoff_longitude=takeoff_longitude,
            takeoff_latitude=takeoff_latitude,
            position_data=position_data,
            start_timestamp=start_timestamp_str,
            boot_timestamp=boot_timestamp_str,
            end_timestamp=end_timestamp_str,
            attitude_data=attitude_data,
            vehicle_attitude_setpoint_data = vehicle_attitude_setpoint_data,
            vehicle_local_position_data = vehicle_local_position_data,
            vehicle_local_position_setpoint_data = vehicle_local_position_setpoint_data,
            actuator_outputs_data = actuator_outputs_data,
            vehicle_rates_setpoint_data = vehicle_rates_setpoint_data,
            sensor_combined_data = sensor_combined_data,
            actuator_controls_0_data = actuator_controls_0_data,
            model_scale_factor=model_scale_factor,
            model_uri=model_uri,
            log_id=log_id,
            bing_api_key=get_bing_maps_api_key(),
            cesium_api_key=get_cesium_api_key()))