Example #1
0
class OWSAMP(OWWidget):
    """
    The SAMP widget allows data to be received or requested through SAMP.
    SAMP is the Simple Application Messaging Protocol designed for the
    Virtual Observatory.
    """
    name = "SAMP"
    id = "orange.widgets.data.samp"
    description = """
    Creates a SAMP connection and requests or receives data."""
    long_description = """
    Creates a SAMP connection and requests or receives data."""
    icon = "icons/SAMP.svg"
    author = "Hugo Buddelmeijer"
    maintainer_email = "buddel(@at@)astro.rug.nl"
    priority = 10
    category = "Data"
    keywords = ["data", "file", "load", "read", "lazy"]
    outputs = [OutputSignal(
        "Data",
        LazyTable,
        doc = "Attribute-valued data set received over SAMP."
    )]

    # TODO: move this to LazyTable
    stop_pulling = False
    #stop_pulling = True

    region_of_interest = None
    """region_of_interest specifies what part of the data set is interesting
    according to widgets further in the scheme. See in_region_of_interest()
    of LazyRowInstance for information about its structure."""

    # catalog_of_interest = "100511" # still to complex
    #_catalog_of_interest = "892271" # based on KiDS DR2
    #_catalog_of_interest = "898581" # SA based on 892271
    #_catalog_of_interest = "898771" # SLW based on SA above
    _catalog_of_interest = "" # Can now be set through SAMP.


    """catalog_of_interest specifies the catalog that somehow has been
    set as interesting. Data is pulled from this catalog. For now this
    is hardcoded."""
    # TODO: Store the catalog_of_interest as a setting.
    # TODO: Use contexts etc. like other widgets do to handle multiple tables
    #   at the same time.
    # TODO: Allow the catalog_of_interest to be set over SAMP in some way.
    # TODO: Perhaps integrate region_of_interest with catalog_of_interest
    #   in some way?

    @property
    def catalog_of_interest(self):
        """
        The catalog of interest. Currently identified by its name, which
        currently is just a number.
        """
        return self._catalog_of_interest

    @catalog_of_interest.setter
    def catalog_of_interest(self, value):
        self._catalog_of_interest = value
        self.we_have_a_new_table()

    def we_have_a_new_table(self):
        # TODO: think of better name for this function.
        domain = self.pull_domain()
        self.old_region_of_interest = None
        #self.region_of_interest = None
        data = LazyTable.from_domain(domain=domain)
        data.widget_origin = self
        # Stop the pulling loop in our current data, if any.
        if isinstance(self.data, LazyTable):
            self.data.stop_pulling = True
        
        self.data = data
        self.send("Data", self.data)
        print("Orange Table send B")

    def pull_length(self):
        # TODO: implement
        #length = 0
        length = self.data.X.shape[0] if self.data is not None else 0
        #length = 1000000 if self.data is not None else 0        
        return length

    def __init__(self):
        super().__init__()

        self.data = None
        """The LazyTable that will be send."""

        # GUI: as simple as possible for now
        box = gui.widgetBox(self.controlArea, "SAMP Info")
        self.infoa = gui.widgetLabel(widget=box, label='SAMP status unknown.')

        box_input_catalog = gui.widgetBox(box, orientation=0)
        self.input_catalog_text = gui.widgetLabel(widget=box_input_catalog , label='Catalog')
        self.input_catalog = gui.lineEdit(widget=box_input_catalog , master=self, value='catalog_of_interest')

        #self.resize(100,50)
        self.button_disconnect = gui.button(self.controlArea, self, "&Disconnect", callback=self.disconnect_samp, default=False)
        self.button_connect = gui.button(self.controlArea, self, "&Connect", callback=self.connect_samp, default=False)
        self.button_disconnect.setHidden(True)
        #gui.button(self.controlArea, self, "&Pull Rows", callback=self.pull_rows, default=False)
        gui.button(self.controlArea, self, "&Set Table", callback=self.we_have_a_new_table, default=False)

        # Create a SAMP client and connect to HUB.
        # Do not make the client in __init__ because this does not allow
        # the client to disconnect and reconnect again.
        self.samp_client = None
        self.connect_samp()

        #self.pull_rows()

    def pull_region_of_interest(self):
        if self.region_of_interest is None:
            self.region_of_interest = {
               #'RA': (1., 359.),
               #'DEC': (-89., 89.),
            }

        if self.old_region_of_interest == self.region_of_interest:
            # We are already getting this data. With SAMP we don't have to
            # keep requesting.
            # TODO: make SAMP widget only keep sending data upon request
            pass
        else:
            self.old_region_of_interest = self.region_of_interest
            print("Pulling ROI",self.region_of_interest)
            self.pull_rows()

    counter_msg_tag = 123

    def pull_rows(self):
        """
        First experiment for data through SAMP. This is a prototype.
        """
        if self.data is None:
            self.we_have_a_new_table()

        extra_attributes = ['A', 'B', 'SLID', 'SID', 'HTM']

        # TODO: Proper SQL creation
        if self.region_of_interest is None:
            region_of_interest_in_sql = ""
        elif isinstance(self.region_of_interest, dict):
            region_of_interest_in_sql = " AND ".join(
                ''' "%s" BETWEEN %f AND %f ''' % (
                    name, values[0], values[1]
                ) for (name, values) in self.region_of_interest.items()
            ) if len(self.region_of_interest) else ""
        else:
            region_of_interest_in_sql = " AND ".join(
                to_sql(f)
                for f in self.region_of_interest.conditions
            ) if len(self.region_of_interest.conditions) else ""

        # TODO: Make it possible to specify attributes.
        #   However, this might require significant changes in the way the
        #   data is stored in the LazyTable.
        message = {
            'samp.mtype':'catalog.pull',
            'samp.params':{
                'startsc': str(self.catalog_of_interest),
                'query': urllib.parse.unquote_plus(region_of_interest_in_sql),
                #'attributes': [attribute for attribute in self.region_of_interest],
                #'attributes': [attribute for attribute in self.region_of_interest] + extra_attributes,
                'attributes': [attribute.name for attribute in self.data.domain],
            },
        }
        print("OWSAMP pull_rows", message['samp.params']['startsc'], message['samp.params']['query'])
        #        'query': urllib.unquote_plus('ROWNUM <= %i' % (row_index)),
        #        'query': urllib.unquote_plus('"R" < 300'),
        #attributes ['absMag_u', 'absMag_g', 'iC']

        # TODO: Abstract call and properly implement msg_tag.
        self.counter_msg_tag += 1
        self.samp_client.call_all("pull_nr_%i" % (self.counter_msg_tag), message)

    def pull_domain(self):
        """
        Requests information about the table over SAMP.
        """
        message = {
            'samp.mtype':'target.object.info',
            'samp.params':{
                'class': 'SourceCollection',
                'id': self.catalog_of_interest,
            },
        }

        # TODO: not use call_and_wait, just call
        id_awe = list(self.samp_client.get_subscribed_clients(mtype="target.object.info"))[0]
        reply = self.samp_client.call_and_wait(message=message, timeout="20", recipient_id = id_awe)

        columns = [a['value'] for a in reply['samp.result']['properties'] if a['name'][:10] == 'attribute|']

        # Create a Domain.
        # TODO: Y en metas, but we don't have this information!
        if False:
            attributes = [
                ContinuousVariable(name=column)
                for column in columns
            ]
            domain = Domain(attributes = attributes)
        
        # So we fake it. 'CLASS' in the column name means it is a
        # class.
        # TODO: Properly fix this. Allow more than two classes.
        #   dynamic class names.
        attributes = [
            ContinuousVariable(name=column)
            for column in columns if not 'CLASS' in column
        ]
        class_vars = [
            DiscreteVariable(name=column, values=['alpha', 'beta'])
            for column in columns if 'CLASS' in column
        ]
        domain = Domain(
            attributes=attributes,
            class_vars=class_vars,
        )

        print("Domain made")

        return domain





    def received_table_load_votable(self, private_key, sender_id, msg_id, mtype, parameters, extra):
        """
        Read the received VOTable and broadcast.
        """
        print("Call:", private_key, sender_id, msg_id, mtype, parameters, extra)

        # Retrieve and read the VOTable.
        url_table = parameters['url']

        # sys.stdout is redirected by canvas.__main__ via redirect_stdout()
        # in canvas.util.redirect to an
        # Orange.canvas.application.outputview.TextStream object. This
        # has a @queued_blocking flush(), which can result in an "Result not
        # yet ready" RuntimeError from the QueuedCallEvent class.
        # This exception is raised because astropy.io.votable.table uses
        # astropy.utils.xml.iterparser, which uses astropy.utils.data,
        # which uses the Spinner class from astropy.utils.console, which
        # finally uses stdout to output a progress indicator.
        # Orange has its own mechanisms for indicating progress, so it would
        # perhaps be better to try to use that.
        # For now, the Orange redirect of stdout is temporarily disabled
        # while the votable is being parsed.

        stdout_orange = sys.stdout
        sys.stdout = sys.__stdout__
        votable_tree = votable.parse(url_table)
        sys.stdout = stdout_orange

        print("VOTable Tree created")
        votable_table = votable_tree.get_first_table()
        #type(votable)
        #<class 'astropy.io.votable.tree.Table'>
        table = votable_table.to_table()
        #type(table)
        #<class 'astropy.table.table.Table'>
        print("AstroPy table made")



        # This does not allow classes.
        if False:
            # Convert the VOTable to a Domain.
            # TODO: Y en metas
            attributes = [
                ContinuousVariable(name=column)
                for column in table.columns
            ]
            domain = Domain(attributes = attributes)
            print("Domain made")

            # Convert the VOTable to a Table
            # Append the Table to LazyTable self.data.
            # (Re)send self.data).
            # TODO: Use from_domain() implicitly from __init__().
            # TODO: Include support to stop_pulling immediately.
            otable = Table.from_domain(
                #otable = LazyTable.from_domain(
                #otable = LazyTable(
                domain = domain,
                n_rows = len(table),
                #    stop_pulling = True,
            )
            otable.stop_pulling = True
            # TODO: set widget_origin?
            print("Orange Table initialized")
            for i, variable in enumerate(otable.domain.variables):
                otable.X[:,i] = table.columns[variable.name].data

        attributes = [
            ContinuousVariable(name=column)
            for column in table.columns if not 'CLASS' in column
        ]
        class_vars = [
            DiscreteVariable(name=column, values=['alpha', 'beta'])
            for column in table.columns if 'CLASS' in column
        ]
        domain = Domain(
            attributes=attributes,
            class_vars=class_vars,
        )

        otable = Table.from_domain(
            domain = domain,
            n_rows = len(table),
        )
        otable.stop_pulling = True
        print("Orange Table initialized")
        for i, variable in enumerate(otable.domain.attributes):
            otable.X[:,i] = table.columns[variable.name].data
        for i, variable in enumerate(otable.domain.class_vars):
            #otable.Y[:,i] = table.columns[variable.name].data
            #otable.Y[:] = table.columns[variable.name].data
            otable.Y[:] = table.columns[variable.name].data.round()

        print("Orange Table filled")
        if self.data is None:
            self.data = otable
        else:
            self.data.extend(otable)
            # TODO: Why doesn't this work?:
            #for row in otable:
            #    self.data.append(row)


        self.send("Data", self.data)
        print("Orange Table send A")

    # TODO: Implement the messages_received_cache in a nicer way.
    messages_received_cache = []
    def received_table_load_votable_call(self, private_key, sender_id, msg_id, mtype, parameters, extra):
        """
        Receive a VOTable and reply with success.

        TODO: Only reply with success if there was no problem.
        """
        if msg_id in self.messages_received_cache:
            print("Message repeated!", msg_id)
            #self.samp_client.reply(msg_id, {"samp.status": "samp.ok", "samp.result": {}})
            return

        self.messages_received_cache.append(msg_id)
        print("Call:", private_key, sender_id, msg_id, mtype, parameters, extra)
        self.samp_client.reply(msg_id, {"samp.status": "samp.ok", "samp.result": {}})
        print("Reply Send")
        self.received_table_load_votable(private_key, sender_id, msg_id, mtype, parameters, extra)
        print("Table loaded")

    def received_target_object_highlight_call(self, private_key, sender_id, msg_id, mtype, parameters, extra):
        if msg_id in self.messages_received_cache:
            print("Message repeated!", msg_id)
            #self.samp_client.reply(msg_id, {"samp.status": "samp.ok", "samp.result": {}})
            return

        self.messages_received_cache.append(msg_id)
        print("Call:", private_key, sender_id, msg_id, mtype, parameters, extra)
        self.samp_client.reply(msg_id, {"samp.status": "samp.ok", "samp.result": {}})
        print("Reply Send")
        self.catalog_of_interest = parameters['id']
        print("Table loaded")


    def disconnect_samp(self):
        """Disconnect from the SAMP HUB"""
        self.samp_client.disconnect()
        self.infoa.setText("SAMP disconnected.")
        self.button_disconnect.setHidden(True)
        self.button_connect.setHidden(False)

    def connect_samp(self):
        # Create a client. This has to be done on connect, because a
        # disconnected client cannot be reconnected.
        # TODO: Proper icon support.
        self.samp_client = SAMPIntegratedClient(
            metadata = {
                "samp.name":"Orange Client",
                "samp.description.text":"Orange SAMP connectivity",
                "OrangeClient.version":"0.01",
                "samp.icon.url":"http://www.astro.rug.nl/~buddel/tmp/samp.png",
           }
        )

        try:
            self.samp_client.connect()
            #self.samp_client.bind_receive_notification("table.load.votable", self.received_table_load_votable)
            self.samp_client.bind_receive_call("table.load.votable", self.received_table_load_votable_call)
            #self.SAMP_client.bind_receive_call("table.this.is.cool.table", self.table_this_is_cool_table)
            self.samp_client.bind_receive_call("target.object.highlight", self.received_target_object_highlight_call)

            self.infoa.setText("SAMP connected.")
            self.button_connect.setHidden(True)
            self.button_disconnect.setHidden(False)
            
        except Exception as e:
            self.infoa.setText("SAMP error: %s" % e)
    
        # For debuging.
        #self.received_table_load_votable(private_key='', sender_id='', msg_id= '', mtype='', parameters={'url':'/home/evisualization/voclass/testclassification.votable'}, extra='')


    def set_region_of_interest(self, region_of_interest):
        """
        A region of interest has been indicated, probably by the user.
        Give preference to data in this region when pulling more data.
        """
        self.region_of_interest = region_of_interest

    def onDeleteWidget(self):
        """Disconnect from the SAMP Hub on exit."""
        if isinstance(self.data, LazyTable):
            self.data.stop_pulling = True
        
        self.disconnect_samp()
        super().onDeleteWidget()
            handler = VSOHandler(client, start, end, timestamp, cadence,
                                 layers)

            return handler


# The next cell connects to an active Hub (see http://docs.astropy.org/en/stable/vo/samp/example_table_image.html)

# In[ ]:

client = SAMPIntegratedClient()
client.connect()

r = Receiver(client)
client.bind_receive_call("jhv.vso.load", r.receive_call)
client.bind_receive_notification("jhv.vso.load", r.receive_notification)

# ### Downloading the data
# When a message is sent over SAMP, run the following to create a new Handler with the parameters given over SAMP.
#
# A Handler object named handler will be created which offers several utility methods to create a VSOQuery and download the data.
# * __handler.createQuerySingleResult()__
#   Creates a VSO-Query to only retrieve the currently show images in JHelioviewer for each active Layer
# * __handler.createQuery()__
#   Creates a VSO-Query for the complete timespan visible in JHelioviewer
# * __handler.showQuery()__
#   Can be used to check the query result before downloading the actual data
# * __handler.downloadData()__
#   Downloads the data found by the current query
# * __handler.showData(figsize)__
Example #3
0
    def receive_notification(self, private_key, sender_id, mtype, params,
                             extra):
        self.params = params
        self.sender = sender_id
        self.received = True

    def reset(self):
        self.received = False


# Instantiate the receiver
r = Receiver(client)

# Listen for any instructions to load a table
client.bind_receive_call("table.load.votable", r.receive_call)
client.bind_receive_notification("table.load.votable", r.receive_notification)

# We now run the loop to wait for the message in a try/finally block so that if
# the program is interrupted e.g. by control-C, the client terminates
# gracefully.

print("Starting websocket...")

websocket.enableTrace(True)
ws = websocket.create_connection("ws://{}:{}/ws".format(host, port))

print("Connecting to http://{}".format(host))

webbrowser.open("http://{}:{}".format(host, port))