예제 #1
0
class RText(QObject):
    def __init__(self, text, x, y, size, color):
        self._pos = QPointF(x - 1.8, y + 1.8)
        super().__init__()
        self.text = QGraphicsTextItem(text)
        transform = QTransform.fromScale(0.3, -0.3)
        self.text.setTransformOriginPoint(self._pos)
        self.text.setTransform(transform)
        self.text.setPos(self._pos)
        # self.text.setRotation(-180)
        font = QFont("Times", 2)
        self.text.setFont(font)

        self._visible = 1

    @pyqtProperty(int)
    def visible(self):
        return self._visible

    @visible.setter
    def visible(self, value):
        if value > 0:
            self.text.show()
        else:
            self.text.hide()
        self._visible = value
예제 #2
0
class SpecBox(QGraphicsRectItem):
    """
    Represents the location of a 2D spectrum in a QGraphicsScene and allows for interaction, via a context menu and
    keyuboard shortcuts.
    """

    inactive_opacity = 0.21  # the opacity of rectangles that are not in focus

    def __init__(self, *args):
        rect = QRectF(*args)
        super().__init__(rect)
        self.setOpacity(SpecBox.inactive_opacity)
        self.setAcceptHoverEvents(True)
        self.setFlag(QGraphicsItem.ItemIsSelectable)
        self.setPen(green_pen)

        self.pinned = False
        self.label = None
        self._spec = None
        self._model = None
        self._contam_table = None
        self._info_window = None

        self._key_bindings = {Qt.Key_Up:   self.plot_column_sums,
                              Qt.Key_Down: self.plot_column_sums,
                              Qt.Key_Right: self.plot_row_sums,
                              Qt.Key_Left: self.plot_row_sums,
                              Qt.Key_S:    self.show_decontaminated,
                              Qt.Key_D:    self.show_decontaminated,
                              Qt.Key_V:    self.show_variance,
                              Qt.Key_C:    self.show_contamination,
                              Qt.Key_L:    self.show_contaminant_table,
                              Qt.Key_T:    self.show_contaminant_table,
                              Qt.Key_0:    self.show_zeroth_orders,
                              Qt.Key_Z:    self.show_zeroth_orders,
                              Qt.Key_R:    self.show_residual,
                              Qt.Key_O:    self.show_original,
                              Qt.Key_A:    self.show_all_layers,
                              Qt.Key_M:    self.show_model,
                              Qt.Key_I:    self.show_info,
                              Qt.Key_Home: self.open_analysis_tab,
                              Qt.Key_Space: self.open_all_spectra}

    @property
    def spec(self):
        return self._spec

    @spec.setter
    def spec(self, spec):
        self._spec = spec

    @property
    def model(self):
        return self._model

    @model.setter
    def model(self, model):
        self._model = model

    @property
    def view(self):
        return self.scene().views()[0]

    def hoverEnterEvent(self, event):
        self.setPen(red_pen)
        self.setOpacity(1.0)

    def hoverLeaveEvent(self, event):
        if not self.pinned:
            self.setPen(green_pen)
            self.setOpacity(SpecBox.inactive_opacity)

    def mousePressEvent(self, event: 'QGraphicsSceneMouseEvent'):
        keys = event.modifiers()
        event.button()

        if keys & Qt.CTRL:
            self.grabKeyboard()
        elif event.button() == Qt.LeftButton:
            self.handle_pinning(event)
            self.grabKeyboard()
        elif event.button() == Qt.RightButton:
            self.handle_right_click(event.screenPos())

    def handle_pinning(self, event):
        if self.pinned:  # it's already pinned; unpin it
            self.unpin()
        else:  # it's not pinned; pin it
            self.pin(event.scenePos())

    def pin(self, label_pos=None):
        """
        Changes the color of the bounding box and places a text label beside the object, containing the object's ID
        string. Refer to `self.unpin()`.
        """
        if label_pos is not None:
            self.label = QGraphicsTextItem(f"{self._spec.id}", parent=self)
            self.label.setTransform(flip_vertical, True)
            self.label.setPos(label_pos)
            self.label.setDefaultTextColor(QColor('red'))
        else:
            self.label = QGraphicsTextItem(f"{self._spec.id}")
            self.label.setTransform(flip_vertical, True)
            self.label.setDefaultTextColor(QColor('red'))
            self.scene().addItem(self.label)
            self.label.setPos(self.scenePos())

        self.setPen(red_pen)
        self.setOpacity(1.0)
        self.pinned = True

    def unpin(self):
        """
        Reverses the action performed by `self.pin()`. Returns the object to an unpinned state.
        """
        self.setPen(green_pen)
        if self.label is not None:
            self.scene().removeItem(self.label)
            self.label = None
        self.pinned = False

    def keyPressEvent(self, event):
        if event.key() in self._key_bindings:
            self._key_bindings[event.key()]()

    def handle_right_click(self, pos):
        """
        Handles right-click (context menu) events. This implementation turned out to be more robust than implementing
        the virtual function for handling context menu events.
        """
        menu = QMenu()

        def action(title, slot, caption=None, shortcut=None):
            act = QAction(title, menu)
            act.triggered.connect(slot)
            if caption is not None:
                act.setStatusTip(caption)

            if shortcut is not None:
                act.setShortcut(shortcut)
                act.setShortcutVisibleInContextMenu(True)
            return act

        menu.addSection(f'Object {self.spec.id}')

        menu.addAction(action('Show table of contaminants', self.show_contaminant_table, shortcut='T'))
        menu.addAction(action('Show Object Info', self.show_info, 'Show details about this object', 'I'))
        menu.addAction(action('Open Object tab', self.open_analysis_tab, shortcut=Qt.Key_Home))
        menu.addAction(action('Show in all detectors',  self.open_all_spectra, 'Show all spectra of object in new tabs',
                              Qt.Key_Space))

        menu.addSection('Plots')

        menu.addAction(action('Plot column sums', self.plot_column_sums, shortcut=Qt.Key_Up))
        menu.addAction(action('Plot row sums', self.plot_row_sums, shortcut=Qt.Key_Right))
        menu.addAction(action('Show all layers', self.show_all_layers, shortcut='A'))
        menu.addAction(action('Show decontaminated spectrum', self.show_decontaminated, shortcut='D'))
        menu.addAction(action('Show original spectrum', self.show_original, shortcut='O'))
        menu.addAction(action('Show contamination', self.show_contamination, shortcut='C'))
        menu.addAction(action('Show variance', self.show_variance, shortcut='V'))
        menu.addAction(action('Show zeroth-order positions', self.show_zeroth_orders, shortcut=Qt.Key_Z|Qt.Key_0))
        menu.addAction(action('Show residual', self.show_residual, shortcut='R'))
        menu.addAction(action('Show model spectrum', self.show_model, shortcut='M'))

        menu.exec(pos)

        self.view.ignore_clicks()

    def plot_column_sums(self):
        self.plot_pixel_sums(0, 'Column')

    def plot_row_sums(self):
        self.plot_pixel_sums(1, 'Row')

    def plot_pixel_sums(self, axis, label):

        plot = PlotWindow(f'{self.spec.id} {label} Sum')

        plt.sca(plot.axis)
        science = self.spec.science.sum(axis=axis)
        contamination = self.spec.contamination.sum(axis=axis)
        plt.plot(contamination, alpha=0.6, label='Contamination')
        plt.plot(science + contamination, alpha=0.6, label='Original')
        plt.plot(science, label='Decontaminated')
        plt.title(f'Object ID: {self.spec.id}')
        plt.xlabel(f'Pixel {label}')
        plt.ylabel(f'{label} Sum')
        plt.legend()
        plt.draw()
        plot.show()
        plot.adjustSize()
        plt.close()

    def show_variance(self):
        title = f'Variance of {self.spec.id}'
        self.show_spec_layer(title, self.spec.variance)

    def show_decontaminated(self):
        title = f'Decontaminated Spectrum of {self.spec.id}'
        self.show_spec_layer(title, self.spec.science)

    def show_contamination(self):
        title = f'Contamination of {self.spec.id}'
        self.show_spec_layer(title, self.spec.contamination)

    def show_zeroth_orders(self):
        title = f'Zeroth-order contamination regions of {self.spec.id}'
        data = (flag['ZERO'] & self.spec.mask) == flag['ZERO']
        self.show_spec_layer(title, data)

    def show_original(self):
        title = f'{self.spec.id} before decontamination'
        self.show_spec_layer(title, self.spec.contamination + self.spec.science)

    def show_residual(self):
        if self.model is not None:
            title = f"residual spectrum of {self.spec.id}"
            self.show_spec_layer(title, self.spec.science - self.model)

    def show_model(self):
        if self.model is not None:
            title = f"Model of {self.spec.id}"
            self.show_spec_layer(title, self.model)

    def show_spec_layer(self, title, data):
        plot = PlotWindow(title)

        plt.sca(plot.axis)
        plt.imshow(data, origin='lower')
        plt.subplots_adjust(top=0.975, bottom=0.025, left=0.025, right=0.975)
        plt.draw()
        plot.setWindowFlag(Qt.WindowStaysOnTopHint, False)
        plot.show()

        padding = 32

        display = QApplication.desktop()
        current_screen = display.screenNumber(self.view)
        geom = display.screenGeometry(current_screen)
        width = geom.width() - 2 * padding
        height = geom.height() - 2 * padding
        plot.setGeometry(geom.left() + padding, geom.top() + padding, width, height)
        plt.close()

    def show_all_layers(self):
        title = f'All Layers of {self.spec.id}'
        horizontal = self.rect().width() > self.rect().height()
        subplot_grid_shape = (7, 1) if horizontal else (1, 7)

        plot = PlotWindow(title, shape=subplot_grid_shape)

        plt.sca(plot.axis[0])
        plt.imshow(self.spec.contamination + self.spec.science, origin='lower')
        plt.title('Original')
        plt.draw()

        plt.sca(plot.axis[1])
        plt.imshow(self.spec.contamination, origin='lower')
        plt.title('Contamination')
        plt.draw()

        plt.sca(plot.axis[2])
        plt.imshow(self.spec.science, origin='lower')
        plt.title('Decontaminated')
        plt.draw()

        plt.sca(plot.axis[3])
        if self.model is not None:
            plt.imshow(self.model, origin='lower')
            plt.title('Model')
        else:
            plt.title('N/A')
        plt.draw()

        plt.sca(plot.axis[4])
        if self.model is not None:
            plt.imshow(self.spec.science - self.model, origin='lower')
            plt.title('Residual')
        else:
            plt.title('N/A')
        plt.draw()

        plt.sca(plot.axis[5])
        plt.imshow(self.spec.variance, origin='lower')
        plt.title('Variance')
        plt.draw()

        plt.sca(plot.axis[6])
        data = (flag['ZERO'] & self.spec.mask) == flag['ZERO']
        plt.imshow(data, origin='lower')
        plt.title('Zeroth Orders')
        plt.draw()

        if horizontal:
            plt.subplots_adjust(top=0.97, bottom=0.025, left=0.025, right=0.975, hspace=0, wspace=0)
        else:
            plt.subplots_adjust(top=0.9, bottom=0.03, left=0.025, right=0.975, hspace=0, wspace=0)

        plt.draw()
        plot.setWindowFlag(Qt.WindowStaysOnTopHint, False)
        plot.show()

        padding = 50

        display = QApplication.desktop()
        current_screen = display.screenNumber(self.view)
        geom = display.screenGeometry(current_screen)
        width = geom.width() - 2 * padding
        height = geom.height() - 2 * padding
        plot.setGeometry(geom.left() + padding, geom.top() + padding, width, height)
        plt.close()

    def show_contaminant_table(self):
        contents = self.spec.contaminants
        rows = len(contents)
        columns = 2

        self._contam_table = SpecTable(self.view, rows, columns)
        self._contam_table.setWindowTitle('Contaminants')
        self._contam_table.setWindowFlag(Qt.WindowStaysOnTopHint, True)
        self._contam_table.setWindowFlag(Qt.Window, True)
        self._contam_table.add_spectra(contents)
        self._contam_table.show()

    def open_analysis_tab(self):
        view_tab = self.view.view_tab
        inspector = view_tab.inspector
        inspector.new_object_tab(view_tab.current_dither, view_tab.current_detector, self.spec.id)

    def open_all_spectra(self):
        view_tab = self.view.view_tab
        inspector = view_tab.inspector

        # make a list of all open detectors (detectors currently being viewed in tabs)

        open_detectors = []

        for tab_index in range(inspector.tabs.count()):
            tab = inspector.tabs.widget(tab_index)
            if isinstance(tab, type(view_tab)):
                open_detectors.append((tab.current_dither, tab.current_detector))

        # open new tabs, where necessary

        for dither in inspector.get_object_dithers(self.spec.id):
            for detector in inspector.get_object_detectors(dither, self.spec.id):
                if (dither, detector) not in open_detectors:
                    inspector.new_view_tab(dither, detector)

        # pin the object in all tabs:

        for tab_index in range(inspector.tabs.count()):
            tab = inspector.tabs.widget(tab_index)
            if isinstance(tab, type(view_tab)):
                tab.select_spectrum_by_id(self.spec.id)

    def show_info(self):

        view_tab = self.view.view_tab
        inspector = view_tab.inspector

        if inspector.location_tables is not None:
            info = inspector.location_tables.get_info(self.spec.id)
            info_window = ObjectInfoWindow(info, inspector)
            info_window.show()
            self._info_window = info_window
        else:
            m = QMessageBox(0, 'No Object info available',
                            "Location tables containing the requested information must be loaded before showing info.",
                            QMessageBox.NoButton)
            m.exec()
예제 #3
0
class Barometer(QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        self.uas = None
        svgRenderer = QSvgRenderer('res/barometer.svg')

        bkgnd = QGraphicsSvgItem()
        bkgnd.setSharedRenderer(svgRenderer)
        bkgnd.setCacheMode(QGraphicsItem.NoCache)
        bkgnd.setElementId('background')

        scene = QGraphicsScene()
        scene.addItem(bkgnd)
        scene.setSceneRect(bkgnd.boundingRect())

        self.needle = QGraphicsSvgItem()
        self.needle.setSharedRenderer(svgRenderer)
        self.needle.setCacheMode(QGraphicsItem.NoCache)
        self.needle.setElementId('needle')
        self.needle.setParentItem(bkgnd)
        self.needle.setPos(
            bkgnd.boundingRect().width() / 2 -
            self.needle.boundingRect().width() / 2,
            bkgnd.boundingRect().height() / 2 -
            self.needle.boundingRect().height() / 2)
        self.needle.setTransformOriginPoint(
            self.needle.boundingRect().width() / 2,
            self.needle.boundingRect().height() / 2)
        # textElement = svgRenderer.boundsOnElement('needle-text')
        self.digitalBaro = QGraphicsTextItem()
        self.digitalBaro.setDefaultTextColor(QColor(255, 255, 255))
        self.digitalBaro.document().setDefaultTextOption(
            QTextOption(Qt.AlignCenter))
        self.digitalBaro.setFont(QFont('monospace', 13, 60))
        self.digitalBaro.setParentItem(bkgnd)

        txm = QTransform()
        txm.translate(
            bkgnd.boundingRect().center().x(),
            bkgnd.boundingRect().height() -
            1.5 * self.digitalBaro.document().size().height())
        self.digitalBaro.setTransform(txm, False)

        view = QGraphicsView(scene)
        view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

        layout = QVBoxLayout()
        layout.addWidget(view)
        self.setLayout(layout)
        self.setBarometer(1000)

    def setBarometer(self, hbar):
        deg = ((hbar - 950) * 3 + 210) % 360
        self.needle.setRotation(deg)
        self.digitalBaro.setPlainText('{:.1f}'.format(hbar))
        self.digitalBaro.adjustSize()
        self.digitalBaro.setX(0 - self.digitalBaro.textWidth() / 2)

    def updateAirPressure(self, sourceUAS, timestamp, absPressure,
                          diffPressure, temperature):
        unused(sourceUAS, timestamp, diffPressure, temperature)
        self.setBarometer(absPressure)

    def setActiveUAS(self, uas):
        uas.updateAirPressureSignal.connect(self.updateAirPressure)
        self.uas = uas
예제 #4
0
class DetectorBox(QGraphicsRectItem):
    """
    Represents a single detector box in the MultiDetectorSelector widget.
    """
    length = 32
    disabled_brush = QBrush(QColor(210, 210, 210, 200))
    enabled_brush = QBrush(QColor(120, 123, 135, 255))
    hovered_brush = QBrush(QColor(156, 186, 252, 255))
    selected_brush = QBrush(QColor(80, 110, 206, 255))
    invisible_pen = QPen(QColor(255, 255, 255, 0))
    red_pen = QPen(QColor('red'))

    def __init__(self, row, column, enabled, *args):

        self._detector_id = box_id(row, column)

        self._enabled = enabled

        self._selected = False
        self._rect = QRectF(*args)
        self._rect.setHeight(self.length)
        self._rect.setWidth(self.length)

        super().__init__(self._rect)

        self.setPen(self.invisible_pen)

        self.setAcceptHoverEvents(True)

        if enabled:
            self.setBrush(self.enabled_brush)
        else:
            self.setBrush(self.disabled_brush)

        # states: enabled, disabled, hovered, selected
        self._label = QGraphicsTextItem(str(self.detector_id), self)

        self._label.setTransform(flip_vertical, True)

        self._label.setFont(QFont('Arial', 14))

        self._label.setDefaultTextColor(QColor('white'))

        self._detector_selector = None

    @property
    def detector_id(self):
        return self._detector_id

    @property
    def size(self):
        return self._rect.size()

    @property
    def selected(self):
        return self._selected

    def set_parent_selector(self, parent):
        self._detector_selector = parent

    def hoverEnterEvent(self, event):
        if self._enabled:
            self.setBrush(self.hovered_brush)

    def hoverLeaveEvent(self, event):
        if self._enabled:
            if self._selected:
                self.setBrush(self.selected_brush)
            else:
                self.setBrush(self.enabled_brush)
        else:
            self.setBrush(self.disabled_brush)

    def mousePressEvent(self, event):
        if self._enabled:
            if event.button() == Qt.LeftButton:
                if not self._selected:
                    self._selected = True
                    self.setBrush(self.selected_brush)
                    self.setPen(self.red_pen)
                else:
                    self._selected = False
                    self.setBrush(self.enabled_brush)
                    self.setPen(self.invisible_pen)

            self._detector_selector.selection_changed()

    def place_label(self, rect):
        # FIXMe: placement should be done differently, so that flipping the text leaves it in the same place.
        x_center = 0.5 * (rect.left() + rect.right()
                          ) - 0.5 * self._label.boundingRect().width()
        y_center = 0.5 * (rect.bottom() + rect.top(
        )) - 0.5 * self._label.boundingRect().height() + self.length
        self._label.setPos(x_center, y_center)
예제 #5
0
class LinkItem(QGraphicsObject):
    """
    A Link item in the canvas that connects two :class:`.NodeItem`\s in the
    canvas.

    The link curve connects two `Anchor` items (see :func:`setSourceItem`
    and :func:`setSinkItem`). Once the anchors are set the curve
    automatically adjusts its end points whenever the anchors move.

    An optional source/sink text item can be displayed above the curve's
    central point (:func:`setSourceName`, :func:`setSinkName`)

    """

    #: Z value of the item
    Z_VALUE = 0

    def __init__(self, *args):
        self.__boundingRect = None
        QGraphicsObject.__init__(self, *args)
        self.setFlag(QGraphicsItem.ItemHasNoContents, True)
        self.setAcceptedMouseButtons(Qt.RightButton | Qt.LeftButton)
        self.setAcceptHoverEvents(True)

        self.setZValue(self.Z_VALUE)

        self.sourceItem = None
        self.sourceAnchor = None
        self.sinkItem = None
        self.sinkAnchor = None

        self.curveItem = LinkCurveItem(self)

        self.sourceIndicator = LinkAnchorIndicator(self)
        self.sinkIndicator = LinkAnchorIndicator(self)
        self.sourceIndicator.hide()
        self.sinkIndicator.hide()

        self.linkTextItem = QGraphicsTextItem(self)

        self.__sourceName = ""
        self.__sinkName = ""

        self.__dynamic = False
        self.__dynamicEnabled = False

        self.hover = False

        self.prepareGeometryChange()
        self.__boundingRect = None

    def setSourceItem(self, item, anchor=None):
        """
        Set the source `item` (:class:`.NodeItem`). Use `anchor`
        (:class:`.AnchorPoint`) as the curve start point (if ``None`` a new
        output anchor will be created using ``item.newOutputAnchor()``).

        Setting item to ``None`` and a valid anchor is a valid operation
        (for instance while mouse dragging one end of the link).

        """
        if item is not None and anchor is not None:
            if anchor not in item.outputAnchors():
                raise ValueError("Anchor must be belong to the item")

        if self.sourceItem != item:
            if self.sourceAnchor:
                # Remove a previous source item and the corresponding anchor
                self.sourceAnchor.scenePositionChanged.disconnect(
                    self._sourcePosChanged)

                if self.sourceItem is not None:
                    self.sourceItem.removeOutputAnchor(self.sourceAnchor)

                self.sourceItem = self.sourceAnchor = None

            self.sourceItem = item

            if item is not None and anchor is None:
                # Create a new output anchor for the item if none is provided.
                anchor = item.newOutputAnchor()

            # Update the visibility of the start point indicator.
            self.sourceIndicator.setVisible(bool(item))

        if anchor != self.sourceAnchor:
            if self.sourceAnchor is not None:
                self.sourceAnchor.scenePositionChanged.disconnect(
                    self._sourcePosChanged)

            self.sourceAnchor = anchor

            if self.sourceAnchor is not None:
                self.sourceAnchor.scenePositionChanged.connect(
                    self._sourcePosChanged)

        self.__updateCurve()

    def setSinkItem(self, item, anchor=None):
        """
        Set the sink `item` (:class:`.NodeItem`). Use `anchor`
        (:class:`.AnchorPoint`) as the curve end point (if ``None`` a new
        input anchor will be created using ``item.newInputAnchor()``).

        Setting item to ``None`` and a valid anchor is a valid operation
        (for instance while mouse dragging one and of the link).

        """
        if item is not None and anchor is not None:
            if anchor not in item.inputAnchors():
                raise ValueError("Anchor must be belong to the item")

        if self.sinkItem != item:
            if self.sinkAnchor:
                # Remove a previous source item and the corresponding anchor
                self.sinkAnchor.scenePositionChanged.disconnect(
                    self._sinkPosChanged)

                if self.sinkItem is not None:
                    self.sinkItem.removeInputAnchor(self.sinkAnchor)

                self.sinkItem = self.sinkAnchor = None

            self.sinkItem = item

            if item is not None and anchor is None:
                # Create a new input anchor for the item if none is provided.
                anchor = item.newInputAnchor()

            # Update the visibility of the end point indicator.
            self.sinkIndicator.setVisible(bool(item))

        if self.sinkAnchor != anchor:
            if self.sinkAnchor is not None:
                self.sinkAnchor.scenePositionChanged.disconnect(
                    self._sinkPosChanged)

            self.sinkAnchor = anchor

            if self.sinkAnchor is not None:
                self.sinkAnchor.scenePositionChanged.connect(
                    self._sinkPosChanged)

        self.__updateCurve()

    def setFont(self, font):
        """
        Set the font for the channel names text item.
        """
        if font != self.font():
            self.linkTextItem.setFont(font)
            self.__updateText()

    def font(self):
        """
        Return the font for the channel names text.
        """
        return self.linkTextItem.font()

    def setChannelNamesVisible(self, visible):
        """
        Set the visibility of the channel name text.
        """
        self.linkTextItem.setVisible(visible)

    def setSourceName(self, name):
        """
        Set the name of the source (used in channel name text).
        """
        if self.__sourceName != name:
            self.__sourceName = name
            self.__updateText()

    def sourceName(self):
        """
        Return the source name.
        """
        return self.__sourceName

    def setSinkName(self, name):
        """
        Set the name of the sink (used in channel name text).
        """
        if self.__sinkName != name:
            self.__sinkName = name
            self.__updateText()

    def sinkName(self):
        """
        Return the sink name.
        """
        return self.__sinkName

    def _sinkPosChanged(self, *arg):
        self.__updateCurve()

    def _sourcePosChanged(self, *arg):
        self.__updateCurve()

    def __updateCurve(self):
        self.prepareGeometryChange()
        self.__boundingRect = None
        if self.sourceAnchor and self.sinkAnchor:
            source_pos = self.sourceAnchor.anchorScenePos()
            sink_pos = self.sinkAnchor.anchorScenePos()
            source_pos = self.curveItem.mapFromScene(source_pos)
            sink_pos = self.curveItem.mapFromScene(sink_pos)

            # Adaptive offset for the curve control points to avoid a
            # cusp when the two points have the same y coordinate
            # and are close together
            delta = source_pos - sink_pos
            dist = math.sqrt(delta.x()**2 + delta.y()**2)
            cp_offset = min(dist / 2.0, 60.0)

            # TODO: make the curve tangent orthogonal to the anchors path.
            path = QPainterPath()
            path.moveTo(source_pos)
            path.cubicTo(source_pos + QPointF(cp_offset, 0),
                         sink_pos - QPointF(cp_offset, 0), sink_pos)

            self.curveItem.setPath(path)
            self.sourceIndicator.setPos(source_pos)
            self.sinkIndicator.setPos(sink_pos)
            self.__updateText()
        else:
            self.setHoverState(False)
            self.curveItem.setPath(QPainterPath())

    def __updateText(self):
        self.prepareGeometryChange()
        self.__boundingRect = None

        if self.__sourceName or self.__sinkName:
            if self.__sourceName != self.__sinkName:
                text = u"{0} \u2192 {1}".format(self.__sourceName,
                                                self.__sinkName)
            else:
                # If the names are the same show only one.
                # Is this right? If the sink has two input channels of the
                # same type having the name on the link help elucidate
                # the scheme.
                text = self.__sourceName
        else:
            text = ""

        self.linkTextItem.setPlainText(text)

        path = self.curveItem.path()
        if not path.isEmpty():
            center = path.pointAtPercent(0.5)
            angle = path.angleAtPercent(0.5)

            brect = self.linkTextItem.boundingRect()

            transform = QTransform()
            transform.translate(center.x(), center.y())
            transform.rotate(-angle)

            # Center and move above the curve path.
            transform.translate(-brect.width() / 2, -brect.height())

            self.linkTextItem.setTransform(transform)

    def removeLink(self):
        self.setSinkItem(None)
        self.setSourceItem(None)
        self.__updateCurve()

    def setHoverState(self, state):
        if self.hover != state:
            self.prepareGeometryChange()
            self.__boundingRect = None
            self.hover = state
            self.sinkIndicator.setHoverState(state)
            self.sourceIndicator.setHoverState(state)
            self.curveItem.setHoverState(state)

    def hoverEnterEvent(self, event):
        # Hover enter event happens when the mouse enters any child object
        # but we only want to show the 'hovered' shadow when the mouse
        # is over the 'curveItem', so we install self as an event filter
        # on the LinkCurveItem and listen to its hover events.
        self.curveItem.installSceneEventFilter(self)
        return QGraphicsObject.hoverEnterEvent(self, event)

    def hoverLeaveEvent(self, event):
        # Remove the event filter to prevent unnecessary work in
        # scene event filter when not needed
        self.curveItem.removeSceneEventFilter(self)
        return QGraphicsObject.hoverLeaveEvent(self, event)

    def sceneEventFilter(self, obj, event):
        if obj is self.curveItem:
            if event.type() == QEvent.GraphicsSceneHoverEnter:
                self.setHoverState(True)
            elif event.type() == QEvent.GraphicsSceneHoverLeave:
                self.setHoverState(False)

        return QGraphicsObject.sceneEventFilter(self, obj, event)

    def boundingRect(self):
        if self.__boundingRect is None:
            self.__boundingRect = self.childrenBoundingRect()
        return self.__boundingRect

    def shape(self):
        return self.curveItem.shape()

    def setEnabled(self, enabled):
        """
        Reimplemented from :class:`QGraphicsObject`

        Set link enabled state. When disabled the link is rendered with a
        dashed line.

        """
        # This getter/setter pair override a property from the base class.
        # They should be renamed to e.g. setLinkEnabled/linkEnabled
        self.curveItem.setLinkEnabled(enabled)

    def isEnabled(self):
        return self.curveItem.isLinkEnabled()

    def setDynamicEnabled(self, enabled):
        """
        Set the link's dynamic enabled state.

        If the link is `dynamic` it will be rendered in red/green color
        respectively depending on the state of the dynamic enabled state.

        """
        if self.__dynamicEnabled != enabled:
            self.__dynamicEnabled = enabled
            if self.__dynamic:
                self.__updatePen()

    def isDynamicEnabled(self):
        """
        Is the link dynamic enabled.
        """
        return self.__dynamicEnabled

    def setDynamic(self, dynamic):
        """
        Mark the link as dynamic (i.e. it responds to
        :func:`setDynamicEnabled`).

        """
        if self.__dynamic != dynamic:
            self.__dynamic = dynamic
            self.__updatePen()

    def isDynamic(self):
        """
        Is the link dynamic.
        """
        return self.__dynamic

    def __updatePen(self):
        self.prepareGeometryChange()
        self.__boundingRect = None
        if self.__dynamic:
            if self.__dynamicEnabled:
                color = QColor(0, 150, 0, 150)
            else:
                color = QColor(150, 0, 0, 150)

            normal = QPen(QBrush(color), 2.0)
            hover = QPen(QBrush(color.darker(120)), 2.1)
        else:
            normal = QPen(QBrush(QColor("#9CACB4")), 2.0)
            hover = QPen(QBrush(QColor("#7D7D7D")), 2.1)

        self.curveItem.setCurvePenSet(normal, hover)