1# -*- coding: utf-8 -*-
2#----------------------------------------------------------------------------
3# Name:         oglmisc.py
4# Purpose:      Miscellaneous OGL support functions
5#
6# Author:       Pierre Hjälm (from C++ original by Julian Smart)
7#
8# Created:      2004-05-08
9# Copyright:    (c) 2004 Pierre Hjälm - 1998 Julian Smart
10# Licence:      wxWindows license
11# Tags:         phoenix-port, unittest, py3-port, documented
12#----------------------------------------------------------------------------
13"""
14Miscellaneous support functions for OGL.
15
16params marked with '???' need review!
17"""
18import math
19
20import wx
21
22# Control point types
23# Rectangle and most other shapes
24CONTROL_POINT_VERTICAL = 1
25CONTROL_POINT_HORIZONTAL = 2
26CONTROL_POINT_DIAGONAL = 3
27
28# Line
29CONTROL_POINT_ENDPOINT_TO = 4
30CONTROL_POINT_ENDPOINT_FROM = 5
31CONTROL_POINT_LINE = 6
32
33# Types of formatting: can be combined in a bit list
34FORMAT_NONE = 0             # Left justification
35FORMAT_CENTRE_HORIZ = 1     # Centre horizontally
36FORMAT_CENTRE_VERT = 2      # Centre vertically
37FORMAT_SIZE_TO_CONTENTS = 4 # Resize shape to contents
38
39# Attachment modes
40ATTACHMENT_MODE_NONE, ATTACHMENT_MODE_EDGE, ATTACHMENT_MODE_BRANCHING = 0, 1, 2
41
42# Shadow mode
43SHADOW_NONE, SHADOW_LEFT, SHADOW_RIGHT = 0, 1, 2
44
45OP_CLICK_LEFT, OP_CLICK_RIGHT, OP_DRAG_LEFT, OP_DRAG_RIGHT = 1, 2, 4, 8
46OP_ALL = OP_CLICK_LEFT | OP_CLICK_RIGHT | OP_DRAG_LEFT | OP_DRAG_RIGHT
47
48# Sub-modes for branching attachment mode
49BRANCHING_ATTACHMENT_NORMAL = 1
50BRANCHING_ATTACHMENT_BLOB = 2
51
52# logical function to use when drawing rubberband boxes, etc.
53OGLRBLF = wx.INVERT
54
55CONTROL_POINT_SIZE = 6
56
57# Types of arrowhead
58# (i) Built-in
59ARROW_HOLLOW_CIRCLE   = 1
60ARROW_FILLED_CIRCLE   = 2
61ARROW_ARROW           = 3
62ARROW_SINGLE_OBLIQUE  = 4
63ARROW_DOUBLE_OBLIQUE  = 5
64# (ii) Custom
65ARROW_METAFILE        = 20
66
67# Position of arrow on line
68ARROW_POSITION_START  = 0
69ARROW_POSITION_END    = 1
70ARROW_POSITION_MIDDLE = 2
71
72# Line alignment flags
73# Vertical by default
74LINE_ALIGNMENT_HORIZ              = 1
75LINE_ALIGNMENT_VERT               = 0
76LINE_ALIGNMENT_TO_NEXT_HANDLE     = 2
77LINE_ALIGNMENT_NONE               = 0
78
79# was defined in canvas and in composit
80KEY_SHIFT = 1
81KEY_CTRL = 2
82
83
84
85def FormatText(dc, text, width, height, formatMode):
86    """
87    Format a text
88
89    :param `dc`: the :class:`wx.MemoryDC`
90    :param `text`: the text to format
91    :param `width`: the width of the box???
92    :param `height`: the height of the box??? it is not used in the code!
93    :param `formatMode`: one of the format modes, can be combined in a bit list
94
95      ======================================== ==================================
96      Format mode name                         Description
97      ======================================== ==================================
98      `FORMAT_NONE`                            Left justification
99      `FORMAT_CENTRE_HORIZ`                    Centre horizontally
100      `FORMAT_CENTRE_VERT`                     Centre vertically
101      `FORMAT_SIZE_TO_CONTENTS`                Resize shape to contents
102      ======================================== ==================================
103
104    :returns: a list of strings fitting in the box
105
106    """
107    i = 0
108    word = ""
109    word_list = []
110    end_word = False
111    new_line = False
112    while i < len(text):
113        if text[i] == "%":
114            i += 1
115            if i == len(text):
116                word += "%"
117            else:
118                if text[i] == "n":
119                    new_line = True
120                    end_word = True
121                    i += 1
122                else:
123                    word += "%" + text[i]
124                    i += 1
125        elif text[i] in ["\012","\015"]:
126            new_line = True
127            end_word = True
128            i += 1
129        elif text[i] == " ":
130            end_word = True
131            i += 1
132        else:
133            word += text[i]
134            i += 1
135
136        if i == len(text):
137            end_word = True
138
139        if end_word:
140            word_list.append(word)
141            word = ""
142            end_word = False
143        if new_line:
144            word_list.append(None)
145            new_line = False
146
147    # Now, make a list of strings which can fit in the box
148    string_list = []
149    buffer = ""
150    for s in word_list:
151        oldBuffer = buffer
152        if s is None:
153            # FORCE NEW LINE
154            if len(buffer) > 0:
155                string_list.append(buffer)
156            buffer = ""
157        else:
158            if len(buffer):
159                buffer += " "
160            buffer += s
161            x, y = dc.GetTextExtent(buffer)
162
163            # Don't fit within the bounding box if we're fitting
164            # shape to contents
165            if (x > width) and not (formatMode & FORMAT_SIZE_TO_CONTENTS):
166                # Deal with first word being wider than box
167                if len(oldBuffer):
168                    string_list.append(oldBuffer)
169                buffer = s
170    if len(buffer):
171        string_list.append(buffer)
172
173    return string_list
174
175
176def GetCentredTextExtent(dc, text_list, xpos=0, ypos=0, width=0, height=0):
177    """
178    Get the centred text extend
179
180    :param `dc`: the :class:`wx.MemoryDC`
181    :param `text_list`: a list of text lines
182    :param `xpos`: unused
183    :param `ypos`: unused
184    :param `width`: unused
185    :param `height`: unused
186
187    :returns: maximum width and the height
188
189    """
190    if not text_list:
191        return 0, 0
192
193    max_width = 0
194    for line in text_list:
195        current_width, char_height = dc.GetTextExtent(line.GetText())
196        if current_width > max_width:
197            max_width = current_width
198
199    return max_width, len(text_list) * char_height
200
201
202def CentreText(dc, text_list, xpos, ypos, width, height, formatMode):
203    """
204    Centre a text
205
206    :param `dc`: the :class:`wx.MemoryDC`
207    :param `text_list`: a list of texts
208    :param `xpos`: the x position
209    :param `ypos`: the y position
210    :param `width`: the width of the box???
211    :param `height`: the height of the box???
212    :param `formatMode`: one of the format modes, can be combined in a bit list
213
214      ======================================== ==================================
215      Format mode name                         Description
216      ======================================== ==================================
217      `FORMAT_NONE`                            Left justification
218      `FORMAT_CENTRE_HORIZ`                    Centre horizontally
219      `FORMAT_CENTRE_VERT`                     Centre vertically
220      `FORMAT_SIZE_TO_CONTENTS`                Resize shape to contents
221      ======================================== ==================================
222
223    """
224    if not text_list:
225        return
226
227    # First, get maximum dimensions of box enclosing text
228    char_height = 0
229    max_width = 0
230    current_width = 0
231
232    # Store text extents for speed
233    widths = []
234    for line in text_list:
235        current_width, char_height = dc.GetTextExtent(line.GetText())
236        widths.append(current_width)
237        if current_width > max_width:
238            max_width = current_width
239
240    max_height = len(text_list) * char_height
241
242    if formatMode & FORMAT_CENTRE_VERT:
243        if max_height < height:
244            yoffset = ypos - height / 2.0 + (height - max_height) / 2.0
245        else:
246            yoffset = ypos - height / 2.0
247        yOffset = ypos
248    else:
249        yoffset = 0.0
250        yOffset = 0.0
251
252    if formatMode & FORMAT_CENTRE_HORIZ:
253        xoffset = xpos - width / 2.0
254        xOffset = xpos
255    else:
256        xoffset = 0.0
257        xOffset = 0.0
258
259    for i, line in enumerate(text_list):
260        if formatMode & FORMAT_CENTRE_HORIZ and widths[i] < width:
261            x = (width - widths[i]) / 2.0 + xoffset
262        else:
263            x = xoffset
264        y = i * char_height + yoffset
265
266        line.SetX(x - xOffset)
267        line.SetY(y - yOffset)
268
269
270def DrawFormattedText(dc, text_list, xpos, ypos, width, height, formatMode):
271    """
272    Draw formated text
273
274    :param `dc`: the :class:`wx.MemoryDC`
275    :param `text_list`: a list of texts
276    :param `xpos`: the x position
277    :param `ypos`: the y position
278    :param `width`: the width of the box???
279    :param `height`: the height of the box???
280    :param `formatMode`: one of the format modes, can be combined in a bit list
281
282      ======================================== ==================================
283      Format mode name                         Description
284      ======================================== ==================================
285      `FORMAT_NONE`                            Left justification
286      `FORMAT_CENTRE_HORIZ`                    Centre horizontally
287      `FORMAT_CENTRE_VERT`                     Centre vertically
288      `FORMAT_SIZE_TO_CONTENTS`                Resize shape to contents
289      ======================================== ==================================
290
291    """
292    if formatMode & FORMAT_CENTRE_HORIZ:
293        xoffset = xpos
294    else:
295        xoffset = xpos - width / 2.0
296
297    if formatMode & FORMAT_CENTRE_VERT:
298        yoffset = ypos
299    else:
300        yoffset = ypos - height / 2.0
301
302    # +1 to allow for rounding errors
303    dc.SetClippingRegion(xpos - width / 2.0, ypos - height / 2.0, width + 1, height + 1)
304
305    for line in text_list:
306        dc.DrawText(line.GetText(), xoffset + line.GetX(), yoffset + line.GetY())
307
308    dc.DestroyClippingRegion()
309
310
311def RoughlyEqual(val1, val2, tol=0.00001):
312    """
313    Check if values are roughtly equal
314
315    :param `val1`: the first value to check
316    :param `val2`: the second value to check
317    :param `tol`: the tolerance, defaults to 0.00001
318
319    :returns: True or False
320
321    """
322    return val1 < (val2 + tol) and val1 > (val2 - tol) and \
323           val2 < (val1 + tol) and val2 > (val1 - tol)
324
325
326def FindEndForBox(width, height, x1, y1, x2, y2):
327    """
328    Find the end for a box
329
330    :param `width`: the width of the box
331    :param `height`: the height of the box
332    :param `x1`: x1 position
333    :param `y1`: y1 position
334    :param `x2`: x2 position
335    :param `y2`: y2 position
336
337    :returns: the end position
338
339    """
340    xvec = [x1 - width / 2.0, x1 - width / 2.0, x1 + width / 2.0, x1 + width / 2.0, x1 - width / 2.0]
341    yvec = [y1 - height / 2.0, y1 + height / 2.0, y1 + height / 2.0, y1 - height / 2.0, y1 - height / 2.0]
342
343    return FindEndForPolyline(xvec, yvec, x2, y2, x1, y1)
344
345
346def CheckLineIntersection(x1, y1, x2, y2, x3, y3, x4, y4):
347    """
348    Check for line intersection
349
350    :param `x1`: x1 position
351    :param `y1`: y1 position
352    :param `x2`: x2 position
353    :param `y2`: y2 position
354    :param `x3`: x3 position
355    :param `y3`: y3 position
356    :param `x4`: x4 position
357    :param `y4`: y4 position
358
359    :returns: a lenght ratio and a k line???
360
361    """
362    denominator_term = (y4 - y3) * (x2 - x1) - (y2 - y1) * (x4 - x3)
363    numerator_term = (x3 - x1) * (y4 - y3) + (x4 - x3) * (y1 - y3)
364
365    length_ratio = 1.0
366    k_line = 1.0
367
368    # Check for parallel lines
369    if denominator_term < 0.005 and denominator_term > -0.005:
370        line_constant = -1.0
371    else:
372        line_constant = float(numerator_term) / denominator_term
373
374    # Check for intersection
375    if line_constant < 1.0 and line_constant > 0.0:
376        # Now must check that other line hits
377        if (y4 - y3) < 0.005 and (y4 - y3) > -0.005:
378            k_line = (x1 - x3 + line_constant * (x2 - x1)) / (x4 - x3)
379        else:
380            k_line = (y1 - y3 + line_constant * (y2 - y1)) / (y4 - y3)
381        if k_line >= 0 and k_line < 1:
382            length_ratio = line_constant
383        else:
384            k_line = 1
385
386    return length_ratio, k_line
387
388
389def FindEndForPolyline(xvec, yvec, x1, y1, x2, y2):
390    """
391    Find the end for a polyline
392
393    :param `xvec`: x vector ???
394    :param `yvec`: y vector ???
395    :param `x1`: x1 position
396    :param `y1`: y1 position
397    :param `x2`: x2 position
398    :param `y2`: y2 position
399
400    :returns: the end position
401
402    """
403    lastx = xvec[0]
404    lasty = yvec[0]
405
406    min_ratio = 1.0
407
408    for i in range(1, len(xvec)):
409        line_ratio, other_ratio = CheckLineIntersection(x1, y1, x2, y2, lastx, lasty, xvec[i], yvec[i])
410        lastx = xvec[i]
411        lasty = yvec[i]
412
413        if line_ratio < min_ratio:
414            min_ratio = line_ratio
415
416    # Do last (implicit) line if last and first doubles are not identical
417    if not (xvec[0] == lastx and yvec[0] == lasty):
418        line_ratio, other_ratio = CheckLineIntersection(x1, y1, x2, y2, lastx, lasty, xvec[0], yvec[0])
419        if line_ratio < min_ratio:
420            min_ratio = line_ratio
421
422    return x1 + (x2 - x1) * min_ratio, y1 + (y2 - y1) * min_ratio
423
424
425def PolylineHitTest(xvec, yvec, x1, y1, x2, y2):
426    """
427    Hittest for a polyline
428
429    :param `xvec`: x vector ???
430    :param `yvec`: y vector ???
431    :param `x1`: x1 position
432    :param `y1`: y1 position
433    :param `x2`: x2 position
434    :param `y2`: y2 position
435
436    :returns: True or False
437
438    """
439    isAHit = False
440    lastx = xvec[0]
441    lasty = yvec[0]
442
443    min_ratio = 1.0
444
445    for i in range(1, len(xvec)):
446        line_ratio, other_ratio = CheckLineIntersection(x1, y1, x2, y2, lastx, lasty, xvec[i], yvec[i])
447        if line_ratio != 1.0:
448            isAHit = True
449        lastx = xvec[i]
450        lasty = yvec[i]
451
452        if line_ratio < min_ratio:
453            min_ratio = line_ratio
454
455    # Do last (implicit) line if last and first doubles are not identical
456    if not (xvec[0] == lastx and yvec[0] == lasty):
457        line_ratio, other_ratio = CheckLineIntersection(x1, y1, x2, y2, lastx, lasty, xvec[0], yvec[0])
458        if line_ratio != 1.0:
459            isAHit = True
460
461    return isAHit
462
463
464def GraphicsStraightenLine(point1, point2):
465    """
466    Straighten a line in graphics
467
468    :param `point1`: a point list???
469    :param `point2`: a point list???
470
471    """
472    dx = point2[0] - point1[0]
473    dy = point2[1] - point1[1]
474
475    if dx == 0:
476        return
477    elif abs(float(dy) / dx) > 1:
478        point2[0] = point1[0]
479    else:
480        point2[1] = point1[1]
481
482
483def GetPointOnLine(x1, y1, x2, y2, length):
484    """
485    Get point on a line
486
487    :param `x1`: x1 position
488    :param `y1`: y1 position
489    :param `x2`: x2 position
490    :param `y2`: y2 position
491    :param `length`: length ???
492
493    :returns: point on line
494
495    """
496    l = math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1))
497    if l < 0.01:
498        l = 0.01
499
500    i_bar = (x2 - x1) / l
501    j_bar = (y2 - y1) / l
502
503    return -length * i_bar + x2, -length * j_bar + y2
504
505
506def GetArrowPoints(x1, y1, x2, y2, length, width):
507    """
508    Get point on arrow
509
510    :param `x1`: x1 position
511    :param `y1`: y1 position
512    :param `x2`: x2 position
513    :param `y2`: y2 position
514    :param `length`: length ???
515    :param `width`: width ???
516
517    :returns: point on line
518
519    """
520    l = math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1))
521
522    if l < 0.01:
523        l = 0.01
524
525    i_bar = (x2 - x1) / l
526    j_bar = (y2 - y1) / l
527
528    x3 = -length * i_bar + x2
529    y3 = -length * j_bar + y2
530
531    return x2, y2, width * -j_bar + x3, width * i_bar + y3, -width * -j_bar + x3, -width * i_bar + y3
532
533
534def DrawArcToEllipse(x1, y1, width1, height1, x2, y2, x3, y3):
535    """
536    Draw arc to ellipse
537
538    :param `x1`: x1 position
539    :param `y1`: y1 position
540    :param `width1`: width
541    :param `height1`: height
542    :param `x2`: x2 position
543    :param `y2`: y2 position
544    :param `x3`: x3 position
545    :param `y3`: y3 position
546
547    :returns: ellipse points ???
548
549    """
550    a1 = width1 / 2.0
551    b1 = height1 / 2.0
552
553    # Check that x2 != x3
554    if abs(x2 - x3) < 0.05:
555        x4 = x2
556        if y3 > y2:
557            y4 = y1 - math.sqrt((b1 * b1 - (((x2 - x1) * (x2 - x1)) * (b1 * b1) / (a1 * a1))))
558        else:
559            y4 = y1 + math.sqrt((b1 * b1 - (((x2 - x1) * (x2 - x1)) * (b1 * b1) / (a1 * a1))))
560        return x4, y4
561
562    # Calculate the x and y coordinates of the point where arc intersects ellipse
563    A = (1 / (a1 * a1))
564    B = ((y3 - y2) * (y3 - y2)) / ((x3 - x2) * (x3 - x2) * b1 * b1)
565    C = (2 * (y3 - y2) * (y2 - y1)) / ((x3 - x2) * b1 * b1)
566    D = ((y2 - y1) * (y2 - y1)) / (b1 * b1)
567    E = (A + B)
568    F = (C - (2 * A * x1) - (2 * B * x2))
569    G = ((A * x1 * x1) + (B * x2 * x2) - (C * x2) + D - 1)
570    H = (float(y3 - y2) / (x3 - x2))
571    K = ((F * F) - (4 * E * G))
572
573    if K >= 0:
574        # In this case the line intersects the ellipse, so calculate intersection
575        if x2 >= x1:
576            ellipse1_x = ((F * -1) + math.sqrt(K)) / (2 * E)
577            ellipse1_y = ((H * (ellipse1_x - x2)) + y2)
578        else:
579            ellipse1_x = (((F * -1) - math.sqrt(K)) / (2 * E))
580            ellipse1_y = ((H * (ellipse1_x - x2)) + y2)
581    else:
582        # in this case, arc does not intersect ellipse, so just draw arc
583        ellipse1_x = x3
584        ellipse1_y = y3
585
586    return ellipse1_x, ellipse1_y
587
588
589def FindEndForCircle(radius, x1, y1, x2, y2):
590    """
591    Find end for a circle
592
593    :param `radius`: radius
594    :param `x1`: x1 position
595    :param `y1`: y1 position
596    :param `x2`: x2 position
597    :param `y2`: y2 position
598
599    :returns: end position
600
601    """
602    H = math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1))
603
604    if H == 0:
605        return x1, y1
606    else:
607        return radius * (x2 - x1) / H + x1, radius * (y2 - y1) / H + y1
608