marv_robotics.detail — MARV Robotics 19.07.0 documentation

# Source code for marv_robotics.detail

    # -*- coding: utf-8 -*-
    #
    # Copyright 2016 - 2018  Ternaris.
    # SPDX-License-Identifier: AGPL-3.0-only

    from __future__ import absolute_import, division, print_function

    import json
    import os

    import marv
    import marv_nodes
    from marv_detail import make_map_dict
    from marv.types import Section, Widget
    from .bag import bagmeta
    from .cam import ffmpeg, images
    from .gnss import gnss_plots
    from .trajectory import trajectory

    @marv.node(Widget)
    @marv.input('dataset', default=marv_nodes.dataset)
    @marv.input('bagmeta', default=bagmeta)
    def summary_keyval(dataset, bagmeta):
        """Keyval widget summarizing bag metadata.

        Useful for detail_summary_widgets.
        """
        dataset, bagmeta = yield marv.pull_all(dataset, bagmeta)
        yield marv.push({'keyval': {
            'items': [
                {'title': 'size', 'formatter': 'filesize', 'list': False,
                 'cell': {'uint64': sum(x.size for x in dataset.files)}},
                {'title': 'files', 'list': False,
                 'cell': {'uint64': len(dataset.files)}},
                {'title': 'start time', 'formatter': 'datetime', 'list': False,
                 'cell': {'timestamp': bagmeta.start_time}},
                {'title': 'end time', 'formatter': 'datetime', 'list': False,
                 'cell': {'timestamp': bagmeta.end_time}},
                {'title': 'duration', 'formatter': 'timedelta', 'list': False,
                 'cell': {'timedelta': bagmeta.duration}}
            ]
        }})

    @marv.node(Widget)
    @marv.input('dataset', default=marv_nodes.dataset)
    @marv.input('bagmeta', default=bagmeta)
    def bagmeta_table(bagmeta, dataset):
        """Table widget listing metadata for each bag of dataset.

        Useful for detail_summary_widgets.
        """
        dataset, bagmeta = yield marv.pull_all(dataset, bagmeta)
        columns = [
            {'title': 'Name', 'formatter': 'rellink'},
            {'title': 'Size', 'formatter': 'filesize'},
            {'title': 'Start time', 'formatter': 'datetime'},
            {'title': 'End time', 'formatter': 'datetime'},
            {'title': 'Duration', 'formatter': 'timedelta'},
            {'title': 'Message count', 'align': 'right'},
        ]
        rows = [{'id': idx, 'cells': [
            {'link': {'href': '{}'.format(idx),
                      'title': os.path.basename(f.path)}},
            {'uint64': f.size},
            {'timestamp': bag.start_time},
            {'timestamp': bag.end_time},
            {'timedelta': bag.duration},
            {'uint64': bag.msg_count}
        ]} for idx, (bag, f) in enumerate(zip(bagmeta.bags, dataset.files))]
        yield marv.push({'table': {'columns': columns, 'rows': rows}})

    @marv.node(Section)
    @marv.input('title', default='Position and Orientation')
    @marv.input('plots', default=gnss_plots)
    def gnss_section(plots, title):
        """Section displaying GNSS plots."""
        # tmps = []
        # tmp = yield marv.pull(plots)
        # while tmp:
        #     tmps.append(tmp)
        #     tmp = yield marv.pull(plots)
        # plots = tmps
        # TODO: no foreaching right now
        plots = [plots]

        widgets = []
        for plot in plots:
            plotfile = yield marv.pull(plot)
            if plotfile:
                widgets.append({'title': plot.title,
                                'image': {'src': plotfile.relpath}})
        assert len(set(x['title'] for x in widgets)) == len(widgets)
        if widgets:
            yield marv.push({'title': title, 'widgets': widgets})

    @marv.node(Widget)
    @marv.input('stream', foreach=images)  # images is a stream of streams of images
    def galleries(stream):
        """Galleries for all images streams.

        Used by marv_robotics.detail.images_section.
        """
        yield marv.set_header(title=stream.title)
        images = []
        while True:
            img = yield marv.pull(stream)
            if img is None:
                break
            images.append({'src': img.relpath})
        yield marv.push({'title': stream.title, 'gallery': {'images': images}})

    @marv.node(Section)
    @marv.input('title', default='Images')
    @marv.input('galleries', default=galleries)
    def images_section(galleries, title):
        """Section with galleries of images for each images stream."""
        tmp = []
        while True:
            msg = yield marv.pull(galleries)
            if msg is None:
                break
            tmp.append(msg)
        galleries = tmp
        galleries = sorted(galleries, key=lambda x: x.title)
        widgets = yield marv.pull_all(*galleries)
        if widgets:
            yield marv.push({'title': title, 'widgets': widgets})

    @marv.node(Section)
    @marv.input('title', default='Connections')
    @marv.input('bagmeta', default=bagmeta)
    @marv.input('dataset', default=marv_nodes.dataset)
    def connections_section(bagmeta, dataset, title):
        """Section displaying information about ROS connections."""
        dataset, bagmeta = yield marv.pull_all(dataset, bagmeta)
        if not bagmeta.topics:
            raise marv.Abort()
        columns = [
            {'title': 'Topic'},
            {'title': 'Type'},
            {'title': 'MD5'},
            {'title': 'Latching'},
            {'title': 'Message count', 'align': 'right'}
        ]
        rows = [{'id': idx, 'cells': [
            {'text': con.topic},
            {'text': con.datatype},
            {'text': con.md5sum},
            {'bool': con.latching},
            {'uint64': con.msg_count}
        ]} for idx, con in enumerate(bagmeta.connections)]
        widgets = [{'table': {'columns': columns, 'rows': rows}}]
        # TODO: Add text widget explaining what can be seen here: ROS bag
        # files store connections. There can be multiple connections for
        # one topic with the same or different message types and message
        # types with the same name might have different md5s. For
        # simplicity connections with the same topic, message type and md5
        # are treated as one, within one bag file as well as across bags
        # of one set. If one of such connections is latching, the
        # aggregated connection will be latching.
        yield marv.push({'title': title, 'widgets': widgets})

    @marv.node(Section)
    @marv.input('title', default='Trajectory')
    @marv.input('geojson', default=trajectory)
    @marv.input('minzoom', default=-30)
    @marv.input('maxzoom', default=40)
    @marv.input('tile_server_protocol', default='')
    def trajectory_section(geojson, title, minzoom, maxzoom, tile_server_protocol):
        """Section displaying trajectory on a map.

        Args:
            tile_server_protocol (str): Set to ``https:`` if you host marv
                behind http and prefer the tile requests to be secured.
        """
        geojson = yield marv.pull(geojson)
        if not geojson:
            raise marv.Abort()
        layers = [
            {'title': 'Background',
             'tiles': [
                 {'title': 'Roadmap',
                  'url': '%s//[abc].osm.ternaris.com/styles/osm-bright/rendered/{z}/{x}/{y}.png' % tile_server_protocol,
                  'attribution': '© <a href="http://openstreetmap.org/copyright">OpenStreetMap</a> contributors',
                  'retina': 3,
                  'zoom': {'min': 0, 'max': 20}},
                 {'title': 'Satellite',
                  'url': '%s//server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}.png' % tile_server_protocol,
                  'attribution': 'Sources: Esri, DigitalGlobe, GeoEye, Earthstar Geographics, CNES/Airbus DS, USDA, USGS, AeroGRID, IGN, and the GIS User Community',
                  'zoom': {'min': 0, 'max': 18}},
             ]},
            {'title': 'Trajectory',
             'color': (0., 1., 0., 1.),
             'geojson': geojson},
        ]
        dct = make_map_dict({
            'layers': layers,
            'zoom': {'min': minzoom, 'max': maxzoom},
        })
        jsonfile = yield marv.make_file('data.json')
        with open(jsonfile.path, 'w') as f:
            json.dump(dct, f, sort_keys=True)
        yield marv.push({'title': title,
                         'widgets': [{'map_partial': 'marv-partial:{}'.format(jsonfile.relpath)}]})

    @marv.node(Section)
    @marv.input('title', default='Videos')
    @marv.input('videos', default=ffmpeg)
    def video_section(videos, title):
        """Section displaying one video player per image stream."""
        tmps = []
        while True:
            tmp = yield marv.pull(videos)
            if tmp is None:
                break
            tmps.append(tmp)
        videos = sorted(tmps, key=lambda x: x.title)
        if not videos:
            raise marv.Abort()

        videofiles = yield marv.pull_all(*videos)
        widgets = [
            {
                'title': video.title,
                'video': {'src': videofile.relpath}
            }
            for video, videofile in zip(videos, videofiles)
            if videofile is not None
        ]
        assert len(set(x['title'] for x in widgets)) == len(widgets)
        if widgets:
            yield marv.push({'title': title, 'widgets': widgets})