1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4# Copyright 2017, Data61
5# Commonwealth Scientific and Industrial Research Organisation (CSIRO)
6# ABN 41 687 119 230.
7#
8# This software may be distributed and modified according to the terms of
9# the BSD 2-Clause license. Note that NO WARRANTY is provided.
10# See "LICENSE_BSD2.txt" for details.
11#
12# @TAG(DATA61_BSD)
13
14import math, six
15
16from PyQt5 import QtGui, QtWidgets, QtCore
17
18from Interface.Property import PropertyInterface
19from Instance_Widget import InstanceWidget
20from Connection_Property_Widget import ConnectionPropertyWidget
21
22class ConnectionWidget(QtWidgets.QGraphicsItem, PropertyInterface):
23    """
24    ConnectionWidget - a View represetation for the camkes.ast.Connection objects.
25    This widget is a one time use. Instead of attempting to sync this connection with the model, just make a new object.
26    """
27
28    @property
29    def hidden(self):
30        return self._hidden
31
32    # Hidden will only work if a source and destination widget exist
33    # It will only be set false, if both source and destination is false
34    @hidden.setter
35    def hidden(self, value):
36        assert isinstance(value, bool)
37        self._hidden = value or self.source_instance_widget.hidden or \
38                       self.dest_instance_widget.hidden
39        if self._hidden:
40            self.setZValue(2)
41        else:
42            self.setZValue(4)
43
44    @property
45    def name(self):
46        return self._connection_name
47
48    @name.setter
49    def name(self, value):
50        assert isinstance(value, six.string_types)
51        self._connection_name = value
52        self.setToolTip("%s : %s" % (value, self.connection_type))
53
54    @property
55    def connection_type(self):
56        return self._connection_type
57
58    @connection_type.setter
59    def connection_type(self, value):
60        assert isinstance(value, six.string_types)
61        self._connection_type = value
62        self.setToolTip("%s : %s" % (self.name, value) )
63
64    @property
65    def path(self):
66        # Lazy Instantiation
67        if self._path is None:
68            self._path = QtGui.QPainterPath()
69        return self._path
70
71    def clear_path(self):
72        self._path = None
73
74    # Source information
75    @property
76    def source_instance_widget(self):
77        return self._source_instance_widget
78
79    @source_instance_widget.setter
80    def source_instance_widget(self, value):
81        assert isinstance(value, InstanceWidget)
82        self._source_instance_widget = value
83
84    @property
85    def source_connection_type(self):
86        return self._source_connection_type
87
88    @source_connection_type.setter
89    def source_connection_type(self, value):
90        assert isinstance(value, six.string_types)
91        self._source_connection_type = value
92
93    @property
94    def source_interface_name(self):
95        return self._source_interface_name
96
97    @source_interface_name.setter
98    def source_interface_name(self, value):
99        assert isinstance(value, six.string_types)
100        self._source_interface_name = value
101
102    # Destination Information
103    @property
104    def dest_instance_widget(self):
105        return self._dest_instance_widget
106
107    @dest_instance_widget.setter
108    def dest_instance_widget(self, value):
109        assert isinstance(value, InstanceWidget)
110        self._dest_instance_widget = value
111
112    @property
113    def dest_connection_type(self):
114        return self._dest_connection_type
115
116    @dest_connection_type.setter
117    def dest_connection_type(self, value):
118        assert isinstance(value, six.string_types)
119        self._dest_connection_type = value
120
121    @property
122    def dest_interface_name(self):
123        return self._dest_interface_name
124
125    @dest_interface_name.setter
126    def dest_interface_name(self, value):
127        assert isinstance(value, six.string_types)
128        self._dest_interface_name = value
129
130    @property
131    def source_pos(self):
132        return self._source_pos
133
134    @property
135    def source_angle(self):
136        return self._source_angle
137
138    def set_source_pos_angle(self, pos, angle):
139        assert isinstance(pos, QtCore.QPointF)
140        self._source_pos = pos
141        self._source_angle = angle
142
143        self.update_path()
144
145    @property
146    def dest_pos(self):
147        return self._dest_pos
148
149    @property
150    def dest_angle(self):
151        return self._dest_angle
152
153    def set_dest_pos_angle(self, pos, angle):
154        assert isinstance(pos, QtCore.QPointF)
155        self._dest_pos = pos
156        self._dest_angle = angle
157
158        self.update_path()
159
160    @PropertyInterface.property_widget.getter
161    def property_widget(self):
162        newWidget = ConnectionPropertyWidget(self)
163        return newWidget
164
165    # --- INITIALISATION ---
166
167    def __init__(self, name, con_type, source, source_type, source_inf_name, dest, dest_type, dest_inf_name):
168        super(ConnectionWidget, self).__init__()
169
170        self._connection_name = None
171        self._connection_name = name
172        self._connection_type = None
173        self.connection_type = con_type
174
175        self.setToolTip("%s : %s" % (name, con_type) )
176
177        # Get points from attributes of the edge
178        self._source_pos = None
179        self._dest_pos = None
180        self._source_angle = None
181        self._dest_angle = None
182
183        self._path = None
184        self._hidden = False
185
186        assert isinstance(source, InstanceWidget)
187        self._source_instance_widget = source
188
189        self._source_connection_type = None
190        self.source_connection_type = source_type
191
192        self._source_interface_name = None
193        self.source_interface_name = source_inf_name
194
195        assert isinstance(dest, InstanceWidget)
196        self._dest_instance_widget = dest
197
198        self._dest_connection_type = None
199        self.dest_connection_type = dest_type
200
201        self._dest_interface_name = None
202        self.dest_interface_name = dest_inf_name
203
204        self.source_instance_widget.add_connection(self)
205        self.dest_instance_widget.add_connection(self)
206
207    # --- UI FUNCTION ---
208
209    def paint(self, q_painter, style_option, widget=None):
210        """
211        Deals with the drawing of the connection when asked to paint.
212        Realies on self.path to be set
213        :param q_painter: QPainter to paint on
214        :param style_option: styling options - unused atm
215        :param widget: unused atm
216        """
217
218        # assert isinstance(style_option, QtWidgets.QStyleOptionGraphicsItem)
219        # assert isinstance(widget, QtWidgets.QWidget)
220
221        assert isinstance(q_painter, QtGui.QPainter)
222
223        q_painter.setRenderHint(QtGui.QPainter.Antialiasing)
224
225        if self.source_pos is not None and self.dest_pos is not None:
226            pen = QtGui.QPen(QtGui.QColor(66, 66, 66))
227            pen.setWidth(2)
228
229            pen_color = pen.color()
230            if self.hidden:
231                pen_color.setAlphaF(0.2)
232            else:
233                pen_color.setAlphaF(1)
234            pen.setColor(pen_color)
235
236            q_painter.setPen(pen)
237            q_painter.drawPath(self.path)
238            # q_painter.drawLine(self.source_pos, self.dest_pos)
239
240    def boundingRect(self):
241        """
242        Calculates bounding rect from current self.path with 2.5 margin
243        :return: QRectF
244        """
245
246        rect = self.path.boundingRect()
247        assert isinstance(rect, QtCore.QRectF)
248        rect.adjust(-2.5, -2.5, 2.5, 2.5)
249
250        return rect
251
252    def shape(self):
253        """
254        Returns the self.path with width = 5
255        :return: QPainterPath
256        """
257
258        stroker = QtGui.QPainterPathStroker()
259        stroker.setWidth(5)
260        return stroker.createStroke(self.path)
261
262    # -- Connector drawing --
263
264    def update_path(self):
265        """
266        Recreates path based on the current (which maybe newly set) source and destination points.
267        Relies on: self.source_pos and self.dest_pos to be set.
268        """
269
270        # If there is a source and destination point to work with
271        if self.source_pos and self.dest_pos:
272            self.clear_path()
273
274            # Start at source point
275            self.path.moveTo(self.source_pos)
276
277            # For a beizer curve, there are three points that make a curve (for a cubic beizer curve).
278            # The middle point is called the control point (since it controls the curvature).
279            # There are two curves, one on either side. In the middle, a icon will be drawn if there is space.
280
281            source_control_point = self.get_control_point(self.source_pos, self.dest_pos, self.source_angle)
282
283            destination_control_point = self.get_control_point(self.dest_pos, self.source_pos, self.dest_angle)
284
285            # Find final points for both source and destionation
286            if source_control_point != destination_control_point:
287                # It doesn't make sense to compare points as < or >, because its a bit ambigious.
288                # If we consider * to be source points, and X to be destination point, the following
289                # will not occur due to the get_control_point algorithm.
290                # * - - - X - - * - - X
291                # It would normally look like: * - - - * - - X - - - X
292
293                # Find vector from source to dest
294                s_to_d = destination_control_point - source_control_point
295
296                # Get length, and then find source and destination final points.
297                # if less than 30 - no enough for icon.
298                # otherwise keep 30 pixel space for icon when finding final points
299                length = math.sqrt(s_to_d.dotProduct(s_to_d, s_to_d))
300                if length < 30:
301                    middle_vector = self.change_vector_length(s_to_d, length / 2)
302
303                    source_final_point = source_control_point + middle_vector
304                    destination_final_point = destination_control_point - middle_vector
305                else:
306                    middle_vector = self.change_vector_length(s_to_d, length / 2 - 15)
307
308                    source_final_point = source_control_point + middle_vector
309                    destination_final_point = destination_control_point - middle_vector
310
311            else:
312                # Make final points the same as control points.
313                source_final_point = source_control_point
314                destination_final_point = destination_control_point
315
316            # Draw source cubic beizer curve
317            self.path.quadTo(source_control_point, source_final_point)
318
319            # Draw icon (if possible)
320            self.draw_connector_type(source_final_point, destination_final_point)
321
322            # Draw dest cubic curve
323            self.path.moveTo(destination_final_point)
324            self.path.quadTo(destination_control_point, self.dest_pos)
325
326            # Let graphicitem know that geometry has changed.
327            self.prepareGeometryChange()
328
329    def draw_connector_type(self, source_point, dest_point):
330        """
331        Draws the icon between two points.
332        Expected to be subclassed for different connector types.
333        :param source_point: QPointF - the starting point
334        :param dest_point: QPointF - the ending point
335        :return:
336        """
337
338        assert isinstance(source_point, QtCore.QPointF)
339        assert isinstance(dest_point, QtCore.QPointF)
340
341        self.path.moveTo(source_point)
342        self.path.lineTo(dest_point)
343
344    # --- EVENTS ---
345
346    def mousePressEvent(self, mouse_event):
347        """
348        Deals with mouse pressed events. Does nothing atm.
349        :param mouse_event: QGraphicsSceneMouseEvent
350        """
351        assert isinstance(mouse_event, QtWidgets.QGraphicsSceneMouseEvent)
352        super(ConnectionWidget, self).mousePressEvent(mouse_event)
353
354    # --- HELPER FUNCTIONS ---
355
356    @staticmethod
357    def change_vector_length(old_point, new_length):
358        """
359        Helper function to change the length of a vector
360        :param old_point: The current vector
361        :param new_length: The new length of the vector
362        :return: QPointF - new vector with the new length
363        """
364
365        assert isinstance(old_point, QtCore.QPointF)
366        old_length = math.sqrt(old_point.x() * old_point.x() + old_point.y() * old_point.y())
367
368        if old_length == 0:
369            # It doesn't make sense to extend or shorten a zero vector
370            return old_point
371
372        new_point = QtCore.QPointF((old_point.x() * new_length) / old_length, (old_point.y() * new_length) / old_length)
373        return new_point
374
375    @staticmethod
376    def get_control_point(source_pos, dest_pos, angle):
377        """
378        Get the middle point of the cubic beizer curve.
379        To get the source control point - pass in source point as source_pos and destination point as dest_pos
380        To get the dest control porint - pass in destination point as source_pos and source point as source_pos
381        :param source_pos: QPointF - source point
382        :param dest_pos: QPointF - destination point
383        :param angle: int - the distance that the final point has to be, from the straight line
384        :return: QPointF - the middle control point
385        """
386
387        assert isinstance(source_pos, QtCore.QPointF)
388        assert isinstance(dest_pos, QtCore.QPointF)
389
390        # Get vector from start to end
391        s_to_d = dest_pos - source_pos
392        assert isinstance(s_to_d, QtCore.QPointF)
393
394        normal_length = 10
395
396        # If the s_to_d length is less than 20, then set normal_length to half s_to_d length
397        # This will mean that source and destination control points are the same
398        if (math.sqrt(s_to_d.dotProduct(s_to_d, s_to_d)) / 2) < normal_length:
399            normal_length = math.sqrt(s_to_d.dotProduct(s_to_d, s_to_d)) / 2
400
401        s_to_d = ConnectionWidget.change_vector_length(s_to_d, normal_length)
402        perpend_vector = QtCore.QPointF(-s_to_d.y(), s_to_d.x())
403        perpend_vector = ConnectionWidget.change_vector_length(perpend_vector, angle)
404
405        source_control_point = source_pos + s_to_d + perpend_vector
406
407        return source_control_point
408
409    def delete(self):
410        """
411        Before removing the connection, make sure to call this function.
412        :return:
413        """
414
415        # TODO: Delete connection from source & destination
416        self.source_instance_widget.remove_connection(self)
417        self.dest_instance_widget.remove_connection(self)
418
419class DataportWidget(ConnectionWidget):
420    """
421    Subclass of ConnectionWidget, which deals with the drawing of the Dataport icon
422    for the connection type.
423    """
424
425    # Draws a rectangle
426    def draw_connector_type(self, source_point, dest_point):
427        assert isinstance(source_point, QtCore.QPointF)
428        assert isinstance(dest_point, QtCore.QPointF)
429
430        if source_point == dest_point:
431            return
432
433        s_to_d = dest_point - source_point
434        assert isinstance(s_to_d, QtCore.QPointF)
435
436        # Convert s_to_d to perpendicular vector
437        old_x = s_to_d.x()
438        s_to_d.setX(-s_to_d.y())
439        s_to_d.setY(old_x)
440
441        new_vector = self.change_vector_length(s_to_d, 10)
442        top_left = source_point + new_vector
443        top_right = dest_point + new_vector
444        bottom_left = source_point - new_vector
445        bottom_right = dest_point - new_vector
446
447        self.path.moveTo(top_left)
448        self.path.lineTo(top_right)
449        self.path.lineTo(bottom_right)
450        self.path.lineTo(bottom_left)
451        self.path.lineTo(top_left)
452
453class ProcedureWidget(ConnectionWidget):
454    """
455    Subclass of ConnectionWidget, which deals with the drawing of the Procedure icon
456    for the connection type.
457    """
458
459    # Draws a circle
460    def draw_connector_type(self, source_point, dest_point):
461        assert isinstance(source_point, QtCore.QPointF)
462        assert isinstance(dest_point, QtCore.QPointF)
463
464        if source_point == dest_point:
465            return
466
467        s_to_d = dest_point - source_point
468        assert isinstance(s_to_d, QtCore.QPointF)
469        s_to_d = self.change_vector_length(s_to_d, 5)
470
471        start_point = source_point + s_to_d
472        end_point = dest_point - s_to_d
473
474        self.path.moveTo(source_point)
475        self.path.lineTo(start_point)
476
477        centre_point = self.change_vector_length(s_to_d, 14) + start_point
478
479        new_vector = QtCore.QPointF(-s_to_d.y(), s_to_d.x())
480        new_vector = self.change_vector_length(new_vector, 14)
481        arc_start_point = centre_point + new_vector
482
483        self.path.moveTo(arc_start_point)
484
485        top_left = centre_point - QtCore.QPointF(14, 14)
486        bottom_right = centre_point + QtCore.QPointF(14, 14)
487        rect = QtCore.QRectF(top_left, bottom_right)
488
489        straight_point = QtCore.QPointF(1, 0)
490
491        start_straight_dot_product = new_vector.dotProduct(new_vector, straight_point)
492        perpend_length = math.sqrt(new_vector.x() * new_vector.x() + new_vector.y() * new_vector.y())
493        straight_length = math.sqrt(straight_point.x() * straight_point.x() + straight_point.y() * straight_point.y())
494
495        start_angle = math.degrees(math.acos(start_straight_dot_product / (perpend_length * straight_length)))
496
497        if new_vector.y() > 0:
498            start_angle = 360 - start_angle
499
500        self.path.arcTo(rect, start_angle, -180)
501
502        top_left = centre_point - QtCore.QPointF(6, 6)
503        bottom_right = centre_point + QtCore.QPointF(6, 6)
504        rect = QtCore.QRectF(top_left, bottom_right)
505
506        self.path.addEllipse(rect)
507
508        self.path.moveTo(end_point)
509        self.path.lineTo(dest_point)
510
511class EventWidget(ConnectionWidget):
512    """
513    Subclass of ConnectionWidget, which deals with the drawing of the Event icon
514    for the connection type.
515    """
516
517    # Draws a triangle
518    def draw_connector_type(self, source_point, dest_point):
519        assert isinstance(source_point, QtCore.QPointF)
520        assert isinstance(dest_point, QtCore.QPointF)
521
522        if source_point == dest_point:
523            return
524
525        s_to_d = dest_point - source_point
526        assert isinstance(s_to_d, QtCore.QPointF)
527        s_to_d = self.change_vector_length(s_to_d, 5)
528
529        start_point = source_point + s_to_d
530        end_point = dest_point - s_to_d
531
532        new_vector = QtCore.QPointF(-s_to_d.y(), s_to_d.x())
533        new_vector = self.change_vector_length(new_vector, 10)
534
535        self.path.moveTo(source_point)
536        self.path.lineTo(start_point)
537
538        top_left = start_point + new_vector
539        bottom_left = start_point - new_vector
540
541        s_to_d = self.change_vector_length(s_to_d, 14)
542
543        first_end = start_point + s_to_d
544
545        self.path.moveTo(top_left)
546        self.path.lineTo(bottom_left)
547        self.path.lineTo(first_end)
548        self.path.lineTo(top_left)
549
550        s_to_d = self.change_vector_length(s_to_d, 6)
551        top_left = top_left + s_to_d
552        bottom_left = bottom_left + s_to_d
553
554        self.path.moveTo(top_left)
555        self.path.lineTo(end_point)
556        self.path.lineTo(bottom_left)
557
558        self.path.moveTo(end_point)
559
560        self.path.lineTo(dest_point)
561