detail_lib.py

detail_lib.py is a file that is maintained with the detailing template files that are used when you auto Detail using templates .

Example showing the first lines of the detail_lib.py file:

import math
import model
from detailing_vars import *
from string import split
def feq(value1, value2, tol):
    return math.fabs( value1 - value2 ) < tol

def fne(value1, value2, tol):
    return math.fabs( value1 - value2 ) > tol

def flt(value1, value2, tol):
    return (value2 - value1) > tol

def fgt(value1, value2, tol):
    return (value1 - value2) > tol

def fle(value1, value2, tol):
    return (value1 - value2) < tol

def fge(value1, value2, tol):
    return (value2 - value1) < tol

def dist_squared (loc1, loc2):
    xdist = loc1.x - loc2.x
    ydist = loc1.y - loc2.y
    return xdist * xdist + ydist * ydist

The detail_lib.py file defines functions that are useful for defining the rules in templates. It also imports useful Python modules such as the model module, which means that importing the detail_lib.py file also imports the model module. Template developers can modify the file to import additional modules and to write their own functions.


Complete contents of the detail_lib.py file (6/21/2013)

import math
import model
from detailing_vars import *
from string import split
def feq(value1, value2, tol):
    return math.fabs( value1 - value2 ) < tol
def fne(value1, value2, tol):
    return math.fabs( value1 - value2 ) > tol
def flt(value1, value2, tol):
    return (value2 - value1) > tol
def fgt(value1, value2, tol):
    return (value1 - value2) > tol
def fle(value1, value2, tol):
    return (value1 - value2) < tol
def fge(value1, value2, tol):
    return (value2 - value1) < tol
def dist_squared (loc1, loc2):
    xdist = loc1.x - loc2.x
    ydist = loc1.y - loc2.y
    return xdist * xdist + ydist * ydist
def column_face_direction(job_north, column_rot):
    test_rot = job_north - column_rot
    PI_8 = 180 / 8.0
    pi_01_8 =  1.0 * PI_8
    pi_03_8 =  3.0 * PI_8
    pi_05_8 =  5.0 * PI_8
    pi_07_8 =  7.0 * PI_8
    pi_09_8 =  9.0 * PI_8
    pi_11_8 = 11.0 * PI_8
    pi_13_8 = 13.0 * PI_8
    pi_15_8 = 15.0 * PI_8
    if fgt( test_rot, pi_01_8, 0.001 ) and fle( test_rot, pi_03_8, 0.001 ):
        return "\"NORTHWEST\""
    elif fgt( test_rot, pi_03_8, 0.001 ) and fle( test_rot, pi_05_8, 0.001 ):
        return "\"NORTH\""
    elif fgt( test_rot, pi_05_8, 0.001 ) and fle( test_rot, pi_07_8, 0.001 ):
        return "\"NORTHEAST\""
    elif fgt( test_rot, pi_07_8, 0.001 ) and fle( test_rot, pi_09_8, 0.001 ):
        return "\"EAST\""
    elif fgt( test_rot, pi_09_8, 0.001 ) and fle( test_rot, pi_11_8, 0.001 ):
        return "\"SOUTHEAST\""
    elif fgt( test_rot, pi_11_8, 0.001 ) and fle( test_rot, pi_13_8, 0.001 ):
        return "\"SOUTH\""
    elif fgt( test_rot, pi_13_8, 0.001 ) and fle( test_rot, pi_15_8, 0.001 ):
        return "\"SOUTHWEST\""
    elif fgt( test_rot, -pi_15_8, 0.001 ) and fle( test_rot, -pi_13_8, 0.001 ):
        return "\"NORTHWEST\""
    elif fgt( test_rot, -pi_13_8, 0.001 ) and fle( test_rot, -pi_11_8, 0.001 ):
        return "\"NORTH\""
    elif fgt( test_rot, -pi_11_8, 0.001 ) and fle( test_rot, -pi_09_8, 0.001 ):
        return "\"NORTHEAST\""
    elif fgt( test_rot, -pi_09_8, 0.001 ) and fle( test_rot, -pi_07_8, 0.001 ):
        return "\"EAST\""
    elif fgt( test_rot, -pi_07_8, 0.001 ) and fle( test_rot, -pi_05_8, 0.001 ):
        return "\"SOUTHEAST\""
    elif fgt( test_rot, -pi_05_8, 0.001 ) and fle( test_rot, -pi_03_8, 0.001 ):
        return "\"SOUTH\""
    elif fgt( test_rot, -pi_03_8, 0.001 ) and fle( test_rot, -pi_01_8, 0.001 ):
        return "\"SOUTHWEST\""
    else:
        return "\"WEST\""
    
def column_end_cut_text(member_end):
    if member_end.end_cut == "Square Cut":
        return "SQ CUT"
    elif member_end.end_cut == "Bevel Cut":
        return "BEV CUT"
    elif member_end.end_cut == "Mill Cut":
        return "MILL CUT"
    else:
        return ""
        
def shop_callout_text(shop_bolt_types, member_qty):
    if len(shop_bolt_types) == 0:
        return ""
    # pbt = primary bolt type
    pbt = shop_bolt_types[0]
    if len(shop_bolt_types) > 1:
        ret_text = "SHOP BOLTS (Unless Noted):"
    else:
        ret_text = "SHOP BOLTS:"
    bdia = to_dim(pbt.Diameter)
    blen = to_dim(pbt.Length)
    # once bqty is initted, it will only change never .. or .. once
    bqty = pbt.Quantity
    if member_qty == "ONE":
        mqty = 1
    else:
        mqty = int(member_qty)
    fmt_option = get_drawing_setup_option("sum_bolt_format")
    if fmt_option == "TotalOnly":
        bqty *= mqty
    ret_text += "\n  %i-%sx%s %s" % (bqty, bdia, blen, pbt.Name)
    if pbt.Remarks != "":
        ret_text += " w/%s" % pbt.Remarks
    if fmt_option == "UnitAndTotal":
        bqty *= mqty
        ret_text += " [total=%i]" % bqty
    return ret_text
def field_callout_text(field_bolt_types, member_qty):
    if len(field_bolt_types) == 0:
        return ""
    ret_text = "FIELD BOLTS:"
    if member_qty == "ONE":
        mqty = 1
    else:
        mqty = int(member_qty)
    for bolt in field_bolt_types:
        bdia = to_dim(bolt.Diameter)
        blen = to_dim(bolt.Length)
        # once bqty is initted, it will only change never .. or .. once
        bqty = bolt.Quantity
        fmt_option = get_drawing_setup_option("sum_bolt_format")
        if fmt_option == "TotalOnly":
            bqty *= mqty
        ret_text += "\n  %i-%sx%s %s" % (bqty, bdia, blen, bolt.Name)
        if bolt.Remarks != "":
            ret_text += " w/%s" % bolt.Remarks
        if fmt_option == "UnitAndTotal":
            bqty *= mqty
            ret_text += " [total=%i]" % bqty
    return ret_text
def double_material_count(mem):
    if mem.main_mtrl().MaterialType in ["Angle", "Channel"]:
        if hasattr( mem, "is_double" ) and mem.is_double == "Yes":
            return 2
        else:
            return 1
    else:
        return 1
def show_toe_direction_ns_note(mem):
    if mem.type == "Horizontal Brc":
        if mem.main_mtrl().MaterialType in ["S Tee", "W Tee"]:
            return False
    if mem.main_mtrl().MaterialType in ["Angle", "Channel"]:
        if hasattr( mem, "is_double" ) and mem.is_double == "Yes":
            return False
        if hasattr( mem, "toeio" ) and mem.toeio == "In":
            return True
    return False
def show_toe_direction_fs_note(mem):
    if mem.type == "Horizontal Brc":
        if mem.main_mtrl().MaterialType in ["S Tee", "W Tee"]:
            return False
    if mem.main_mtrl().MaterialType in ["Angle", "Channel"]:
        if hasattr( mem, "is_double" ) and mem.is_double == "Yes":
            return False
        if hasattr( mem, "toeio" ) and mem.toeio == "Out":
            return True
    return False
def show_stem_direction_ns_note(mem):
    if mem.type == "Horizontal Brc":
        if mem.main_mtrl().MaterialType in ["S Tee", "W Tee"]:
            if hasattr( mem, "toeio" ) and mem.toeio == "In":
                return True
    return False
def show_stem_direction_fs_note(mem):
    if mem.type == "Horizontal Brc":
        if mem.main_mtrl().MaterialType in ["S Tee", "W Tee"]:
            if hasattr( mem, "toeio" ) and mem.toeio == "Out":
                return True
    return False
def show_long_leg_note(mem):
    if mem.main_mtrl().MaterialType != "Angle":
        return False
    if not hasattr( mem, "Depth" ):
        return False
    if not hasattr( mem, "FlangeWidth" ):
        return False
    if feq(mem.Depth, mem.FlangeWidth, 0.01):
        return False
    if mem.type == "Horizontal Brc" or mem.type == "Vertical Brace":
        if hasattr( mem, "LLV" ) and mem.LLV == "To gusset":
            return True
    return False
def show_short_leg_note(mem):
    if mem.main_mtrl().MaterialType != "Angle":
        return False
    if not hasattr( mem, "Depth" ):
        return False
    if not hasattr( mem, "FlangeWidth" ):
        return False
    if feq(mem.Depth, mem.FlangeWidth, 0.01):
        return False
    if mem.type == "Horizontal Brc" or mem.type == "Vertical Brace":
        if hasattr( mem, "LLV" ) and mem.LLV == "Outstanding":
            return True
    return False
def shop_note_text(mem):
    toe_ns = show_toe_direction_ns_note(mem)
    toe_fs = show_toe_direction_fs_note(mem)
    stem_ns = show_stem_direction_ns_note(mem)
    stem_fs = show_stem_direction_fs_note(mem)
    long_leg = show_long_leg_note(mem)
    short_leg = show_short_leg_note(mem)
    
    if toe_ns or toe_fs or stem_ns or stem_fs or long_leg or short_leg:
        result = "SHOP NOTE:"
        if toe_ns:
            result += "\n  TOE DIRECTION NEAR SIDE"
        if toe_fs:
            result += "\n  TOE DIRECTION FAR SIDE"
        if stem_ns:
            result += "\n  STEM NEAR SIDE"
        if stem_fs:
            result += "\n  STEM FAR SIDE"
        if long_leg:
            result += "\n  LONG LEG SHOWN"
        if short_leg:
            result += "\n  SHORT LEG SHOWN"
        return result
    else:
        return ""
#This is hardcoded here, because it was hardcoded in SDS2 in mtrl_pcmk.c.
#This should actually be pulled from the "Member and Material Piecemarking"
#in setup
def get_material_desc(material):
    material_type = material.MaterialType
    
    if material_type == "Channel":
        return "C"
    elif material_type == "Angle":
        return "L"
    elif material_type == "Pipe":
        return "PIPE"
    elif material_type == "W Tee":
        return "WT"
    elif material_type == "Tube":
        return "TS"
    elif material_type == "W flange":
        return "WF"
    elif material_type == "S Shape":
        return "S"
    elif material_type == "S Tee":
        return "ST"
    elif material_type == "Round plate":
        return "RND PL"
    elif material_type == "Round bar":
        return "RND BAR"
    elif material_type == "Flat bar":
        return "FL"
    elif material_type == "Beaded Flat":
        return "HB"
    elif material_type == "Flat pl layout" or material_type == "Plate":
        if is_bar_stock(material):
            return "FL"
        else:
            return "PL"
    elif material_type == "Bent pl layout" or material_type == "Bent plate":
        return "PL"
    return ""
def get_memb_hsym_note(mem,material):
    mtype = material.MaterialType
    if mtype in ["Plate", "Round plate", "Rolled plate", "Bent plate"]:
        return "IN PLATE"
    elif mtype in ["Flat pl layout", "Bent pl layout"]:
        return "IN PLATE"
    elif mtype in ["Round bar", "Square bar", "Flat bar"]:
        return "IN BAR"
    elif mtype in ["W Tee", "S Tee"]:
        return "IN TEE"
    if mtype == "Angle":
        return "IN ANGLE"
    elif mtype == "Tube":
        return "IN TUBE"
    if material.MainMaterial == "Yes":
        if mem.type == "Beam":
            return "IN BEAM"
        elif mem.type == "Column":
            return "IN COLUMN"
        elif mem.type in ["Horizontal brace", "Vertical brace"]:
            return "IN BRACE"
    return ""
def find_hole_in_group(member_number, mat_num, group_num):
    for hole in model.member(member_number).materials[int(mat_num)].hole:
        if hole.group == int(group_num):
            return hole
    return None
def is_memb_non_std_hole(memno, group):
    matno, grpno = group.split("-")
    hole = find_hole_in_group(memno,matno,grpno)
    if hole == None:
        return False
    gidx = int(grpno)
    sidx = int(model.member(memno).materials[int(matno)].sub_mtrl_idx)
    if hole.group == gidx:
        return is_nonstd_hole(sidx,gidx)
    return False
def get_clip_angle_callout(material, side):
    if side != "NS/FS" and side != "NS" and side != "FS":
        side = extract_ns_fs(side)
    mat_desc = get_material_desc(material)
    text = "1-%s %s %s" % (mat_desc, material.MinorMark, side)
    
    return text
def left_end(left,right,loc):
    mid = left + (right - left) / 2.0
    return flt(loc,mid,0.001)
def right_end(left,right,loc):
    mid = left + (right - left) / 2.0
    return fgt(loc,mid,0.001)
    
def extract_ns_fs(usage):
    return usage[-2:]
def under_reqd_limit(mem,matno):
    memno = mem.member_number
    mt = model.member(memno).materials[int(matno)]
    return pcmk_under_reqd_limit(mt.SubMaterialIndex)
def general_pcmk(memno,matno,side):
    mt = model.member(memno).materials[int(matno)]
    smi = mt.SubMaterialIndex
    desc = subm_pcmk_prefix(smi)
    # args to format_pcmk() -- gosl is meaningless for these
    a1 = float(0.0)
    a2 = float(0.0)
    a3 = float(0.0)
    a4 = float(0.0)
    a5 = float(0.0)
    a6 = float(0.0)
    note_info = get_noted_shop_bolts_on_material(model.member(memno),int(matno))
    note_str = ""
    for ni in note_info:
        this_note = "\n%i-%sx%s %s" % (ni.Quantity, to_dim(ni.Diameter), to_dim(ni.Length), ni.Name)
        note_str += this_note
    if side == "NS":
        if get_drawing_setup_option("ns_sub_mtrl") == "No":
            # near side not indicated
            side = ""
    else:
        if get_drawing_setup_option("fs_sub_mtrl") == "No":
            # far side not indicated
            side = ""
    fmt = format_pcmk(smi,a1,a2,a3,a4,a5,a6,side)
    if fmt == "":
        return fmt
    return "1-" + fmt + note_str
def unopposed_clip_angle_callout(memno,matno,ga1,ga2,gol1,gol2,gosl,side):
    mt = model.member(memno).materials[int(matno)]
    smi = mt.SubMaterialIndex
    desc = subm_pcmk_prefix(smi)
    # args to format_pcmk() -- gosl is meaningless for these
    # args to format_pcmk() -- ga1, ga2 gage is not needed for unopposed clip angles
    a1 = float(0.0)
    a2 = float(0.0)
    a3 = float(gol1)
    a4 = float(gol2)
    a5 = float(0.0)
    a6 = float(0.0)
#bolts not needed for unopposed clip angles
#    note_info = get_noted_shop_bolts_on_material(model.member(memno),int(matno))
    note_str = ""
#    for ni in note_info:
#    this_note = "\n%i-%sx%s %s" % (ni.Quantity, to_dim(ni.Diameter), to_dim(ni.Length), #ni.Name)
    this_note = ""
    note_str += this_note
    if side == "NS":
        if get_drawing_setup_option("ns_sub_mtrl") == "No":
            # near side not indicated
            side = ""
    else:
        if get_drawing_setup_option("fs_sub_mtrl") == "No":
            # far side not indicated
            side = ""
    fmt = format_pcmk(smi,a1,a2,a3,a4,a5,a6,side)
    if fmt == "":
        return fmt
    return "1-" + fmt + note_str
def pad_label(text):
    # prepend and append 'pad' to each line of text
    pad = "  "
    ret_text = ""
    for line in split(text,"\n"):
        if ret_text != "":
            ret_text += "\n"
        ret_text += pad + line + pad
    return ret_text
def opposing_clip_angles(memno,matno_ns,matno_fs,ga1_1,ga2_1,gol1_1,gol2_1,gosl1,ga1_2,ga2_2,gol1_2,gol2_2,gosl2):
    mt_ns = model.member(memno).materials[int(matno_ns)]
    mt_fs = model.member(memno).materials[int(matno_fs)]
    desc_ns = subm_pcmk_prefix(mt_ns.SubMaterialIndex)
    desc_fs = subm_pcmk_prefix(mt_fs.SubMaterialIndex)
    minor_ns = mt_ns.MinorMark
    minor_fs = mt_fs.MinorMark
    # args to format_pcmk()
    a1 = float(ga1_1)
    a2 = float(ga2_1)
    a3 = float(gol1_1)
    a4 = float(gol2_1)
    a5 = float(gosl1)
    a6 = float(gosl2)
    # for these, we want the distance..
    a5 = abs(a5 - a6)
    a6 = float(0.0)
    note_info = get_noted_shop_bolts_on_material(model.member(memno),int(matno_ns))
    note_str = ""
    for ni in note_info:
        this_note = "\n%i-%sx%s %s" % (ni.Quantity, to_dim(ni.Diameter), to_dim(ni.Length), ni.Name)
        note_str += this_note
    smi = mt_ns.SubMaterialIndex
    if desc_ns == desc_fs and minor_ns == minor_fs:
        if get_drawing_setup_option("ns_fs_sub_mtrl") == "Yes":
            # both sides indicated
            fmt = format_pcmk(smi,a1,a2,a3,a4,a5,a6,"NS/FS")
            if fmt == "":
                return fmt
            ret_text = "1-" + fmt + note_str
            return pad_label(ret_text)
        opt_ns = get_drawing_setup_option("ns_sub_mtrl")
        opt_fs = get_drawing_setup_option("fs_sub_mtrl")
        if opt_ns == "No" and opt_fs == "No":
            # neither side indicated
            fmt = format_pcmk(smi,a1,a2,a3,a4,a5,a6,"")
            if fmt == "":
                return fmt
            ret_text = "2-" + fmt + note_str
            return pad_label(ret_text)
    side = ""
    if get_drawing_setup_option("ns_sub_mtrl") == "Yes":
        # near side indicated
        side = "NS"
    fmt = format_pcmk(smi,a1,a2,a3,a4,a5,a6,side)
    if fmt == "":
        text_ns = fmt
    else:
        text_ns = "1-" + fmt
    smi = mt_fs.SubMaterialIndex
    a1 = float(ga1_2)
    a2 = float(ga2_2)
    a3 = float(gol1_2)
    a4 = float(gol2_2)
    side = ""
    if get_drawing_setup_option("fs_sub_mtrl") == "Yes":
        # far side indicated
        side = "FS"
    fmt = format_pcmk(smi,a1,a2,a3,a4,a5,a6,side)
    if fmt == "":
        text_fs = fmt
    else:
        text_fs = "1-" + fmt
    ns_list = split(text_ns,"\n")
    fs_list = split(text_fs,"\n")
    ii = 0
    for line_ns in ns_list:
        if ii == 0:
            text_ns = line_ns
        else:
            found = False
            for line_fs in fs_list:
                if line_ns == line_fs:
                    found = True
                    break
            if found == False:
                text_ns += "\n"
                text_ns += line_ns
        ii += 1
    # at this point, we need a multilabel
    text = "%s\n%s" % (text_ns, text_fs)
    ret_text = text + note_str
    return pad_label(ret_text)
def cmp_dia_slen_rot(d1,s1,r1,d2,s2,r2):
    if fne( float(d1), float(d2), 0.001 ):
        return False
    if fne( float(s1), float(s2), 0.001 ):
        return False
    ar1 = math.fabs( float(r1 ) )
    ar2 = math.fabs( float(r2 ) )
    if( fne( ar1, ar2, 0.001 ) ):
        return False
    return True
def hsym_lox(d1,s1,r1,t1,h1,n1):
    return get_hsym_lox(float(d1),float(s1),float(r1),t1,int(h1),bool(n1))
def hsym_loy(d1,s1,r1,t1,h1,n1):
    return get_hsym_loy(float(d1),float(s1),float(r1),t1,int(h1),bool(n1))
def hsym_hix(d1,s1,r1,t1,h1,n1):
    return get_hsym_hix(float(d1),float(s1),float(r1),t1,int(h1),bool(n1))
def hsym_hiy(d1,s1,r1,t1,h1,n1):
    return get_hsym_hiy(float(d1),float(s1),float(r1),t1,int(h1),bool(n1))
def hsym_dx(d1,s1,r1,t1,h1,n1):
    if feq( float(s1), 0.0, 0.03 ):
        # regular holes work correctly without intervention
        return 0.0
    rot = math.fabs(float(r1))
    if flt( rot, 25.0, 0.001 ):
        if h1 < 0:
            return hsym_hix(d1,s1,r1,t1,h1,n1)
        return hsym_lox(d1,s1,r1,t1,h1,n1)
    if flt( rot, 75.0, 0.001 ):
        lox = hsym_lox(d1,s1,r1,t1,h1,n1)
        hix = hsym_hix(d1,s1,r1,t1,h1,n1)
        return lox + (hix - lox) / 2.0
    if h1 < 0:
        return hsym_lox(d1,s1,r1,t1,h1,n1)
    return hsym_hix(d1,s1,r1,t1,h1,n1)
def hsym_dy(d1,s1,r1,t1,h1,n1):
    return hsym_hiy(d1,s1,r1,t1,h1,n1)
def sec_hsym_dy(d1,s1,r1,d2,s2,r2,t1,handle,no_text_rot):
    prev_ymax = hsym_hiy(d1,s1,r1,t1,handle,no_text_rot)
    prev_ymin = hsym_loy(d1,s1,r1,t1,handle,no_text_rot)
    this_ymax = hsym_hiy(d2,s2,r2,"",handle,no_text_rot)
    return (prev_ymax - prev_ymin) + this_ymax
def pad_hsym_dx(d1,s1,r1,t1,h1,n1):
    padding = len(pad_label("")) / 2
    return hsym_dx(d1,s1,r1,t1,h1,n1) + (h1 * padding)
def ns_fs_plate_callout_text(mem, material, material_num, usage):
    if usage == "Brace Web Fill Plate":
        return "1-%s %s\nFILL PLATE\nNS/FS" % (get_material_desc(material), material.MinorMark)
    else:
        bolt_info = get_shop_bolts_on_material(mem, int(material_num) )[0];
        return "1-%s %s NS/FS\n%i-%sx%s %s" % (get_material_desc(material), material.MinorMark, bolt_info.Quantity/2, to_dim(bolt_info.Length), to_dim(bolt_info.Diameter), bolt_info.Name)
#Sort the holes by group and material
def sort_by_group(match_dict, match, var_name):
    val = match.vars[var_name]
    if not match_dict.has_key(val):
        match_dict[val] = []
    match_dict[val].append(match)
#Goes through a group of matches and returns those that
#maximize the expression
def get_minimums(group, expression):
    group.sort(lambda m1, m2: cmp(expression(m1), expression(m2)))
    min_val = expression(group[0]) + .001
    return filter(lambda m: expression(m) < min_val, group)   
def get_hgrp_minimums(group, expression):
    return get_minimums(group, expression)
def get_hole_minimums(group, expression):
    group.sort(lambda m1, m2: cmp((m1.locations[0].props["Hole Group"],expression(m1)),(m2.locations[1].props["Hole Group"],expression(m2))))
    min_val = expression(group[0]) + .001
    return filter(lambda m: expression(m) < min_val, group)   
def group_by_variable(matches, var_name):
    match_dict = {}
    for match in matches:
        sort_by_group(match_dict, match, var_name)
    
    return match_dict.values()
# Groups a list of detail rule matches by a specified property value at a 
#   specified location.  Matches are also separated by View Number so matches
#   in different views that would otherwise be grouped together will be in
#   separate groups.
# matches: the list of matches.
# locationIdx: the index of the location in the match's locations list.
# propName: the name of the property to group by.
# Returns a dictionary where the key is the group's property value and view 
#   number and the value is the list of detail rule matches that have that 
#   property value.  Any matches that do not have the specified property will 
#   be placed in a group with the view number as the key.
def group_by_prop(matches, locationIdx, propName):
    groups = {}
    for match in matches:
        location = match.locations[locationIdx]
        viewNum = location.props["View Number"]
        if location.props.has_key(propName):
            propVal = location.props[propName] + ", " + viewNum
        else:
            propVal = viewNum
        if not groups.has_key(propVal):
            groups[propVal] = []
        groups[propVal].append(match)
    return groups
# Groups a list of detail rule matches by a specified variable value.
# matches: the list of matches.
# varName: the name of the variable by which to group the matches.
# Returns a dictionary where the key is the group's variable value and 
#   the dictionary value is the list of matches for that group.
def group_by_var(matches, varName):
    groups = {}
    for match in matches:
        var = ""
        if match.vars.has_key(varName):
            var = match.vars[varName]
        if not groups.has_key(var):
            groups[var] = []
        groups[var].append( match )
    return groups

def minimize_expression(match_groups, expression):
    matches = []
    for group in match_groups:
        matches += get_minimums(group, expression)
    return matches
def minimize_hgrp_expression(match_groups, expression):
    matches = []
    for group in match_groups:
        matches += get_hgrp_minimums(group, expression)
    return matches
def minimize_hole_expression(match_groups, expression):
    matches = []
    for group in match_groups:
        matches += get_hole_minimums(group, expression)
    return matches
def get_maximums(group,expression):
    group.sort(lambda m1, m2: cmp(expression(m1), expression(m2)))
    max_val = expression(group[0]) - .001
    return filter(lambda m: expression(m) > max_val, group)
def get_hgrp_maximums(group, expression):
    return get_maximums(group, expression)
def get_hole_maximums(group, expression):
    group.sort(lambda m1, m2: cmp((m1.locations[0].props["Hole Group"],expression(m1)),(m2.locations[1].props["Hole Group"],expression(m2))))
    max_val = expression(group[0]) - .001
    return filter(lambda m: expression(m) < max_val, group)   
def maximize_expression(match_groups, expression):
    matches = []
    for group in match_groups:
        matches += get_maximums(group, expression)
    return matches
def maximize_hgrp_expression(match_groups, expression):
    matches = []
    for group in match_groups:
        matches += get_hgrp_maximums(group, expression)
    return matches
def maximize_hole_expression(match_groups, expression):
    matches = []
    for group in match_groups:
        matches += get_hole_maximums(group, expression)
    return matches
# used in python:filter -- functionality repeated frequently.  useful
# for chaining dimensions with constraint 'running dimension' (which
# i think is poorly named -- 'running' says 'extension' to me).
#
# it keeps the locations-of-interest weeded-down to successive points.
# for example, say that we have 3 points 'a', 'b' and 'c'.  if we don't
# use this type of thing, then we get matches for (a-b), (a-c) and (b-c)
# instead of just (a-b) and (b-c)
#
# sample usage:
#   from detail_lib import *
#   matches = filter_min_hgrp(matches,"HoleGroup1")
#
# .. and having additional python:condition akin to
#   HoleGroup1 != HoleGroup2
#
def filter_min_hgrp(matches,var):
    match_groups = group_by_variable(matches,var)
    return minimize_hgrp_expression(match_groups,lambda m1: dist_squared(m1.locations[0], m1.locations[1]))
# sample usage:
#   from detail_lib import *
#   matches = filter_min_hole(matches,"HoleCol1")
#
# .. and having additional python:condition akin to
#   HoleCol1 != HoleCol2
#
def filter_min_hole(matches,var):
    match_groups = group_by_variable(matches,var)
    return minimize_hole_expression(match_groups,lambda m1: dist_squared(m1.locations[0], m1.locations[1]))
    
class location:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
def dot(v1, v2):
    return v1.x * v2.x + v1.y * v2.y
def length(v):
    return math.sqrt(v.x * v.x + v.y * v.y)
def normalize(v):
    mag = length(v)
    return location(v.x / mag, v.y / mag)
def angle_between(v1, v2):
    return math.acos( dot( v1, v2 ) )
def is_point_between(end1, end2, between):
    v1_2 = normalize(location(end2.x - end1.x, end2.y - end1.y))
    v2_1 = location(-v1_2.x, -v1_2.y)
    v1_b = normalize(location(between.x - end1.x, between.y - end1.y))
    v2_b = normalize(location(between.x - end2.x, between.y - end2.y))
    
    return ((angle_between(v1_2, v1_b) < math.pi / 2.0) and
        (angle_between(v2_1, v2_b) < math.pi / 2.0))
# Groups a list of matches by a specified location into a dictionary.
# matchList: the list of matches
# locationIdx: the locations array index of the location to group by.
# returns a dictionary where the key is a string of the location's x and y 
#       coordinates separated by a comma, and the value is the list of matches
#       grouped by that location.
def group_by_location( matchList, locationIdx ):
    groups = {}
    for match in matchList:     
        loc = match.locations[int(locationIdx)]
        mtrlNum = ""
        if loc.props.has_key("Material Number"):
            mtrlNum = loc.props["Material Number"]
        holeGroup = ""
        if loc.props.has_key("Hole Group"):
            holeGroup = loc.props["Hole Group"]
        key = str(loc.x) + "," + str(loc.y) + "_" + mtrlNum + "_" + holeGroup
        if not groups.has_key( key ):
            groups[key] = []
        groups[key].append( match )
    return groups
# Groups matches whose anchors are colinear.
# matches: the list of matches.
# anchor1Idx: the index of the first anchor point.
# anchor2Idx: the index of the second anchor point.
# rotationVar: the variable name for the annotation direction.
def group_by_anchors( matches, anchor1Idx, anchor2Idx, rotationVar ):
    groups = {}
    for m in matches:
        found = False
        rotation = float( m.vars[rotationVar] )
        for key in groups:
            oDist1 = ortho_dist(key.locations[anchor1Idx], m.locations[anchor1Idx], rotation)
            oDist2 = ortho_dist(key.locations[anchor2Idx], m.locations[anchor2Idx], rotation)
            if feq(oDist1, 0.0, 0.0001) and feq(oDist2, 0.0, 0.0001):
                found = True
                groups[key].append(m)
        if not found:
            groups[m] = []
            groups[m].append(m)
    return groups
# Calculates the squared distance between two locations in a match.
# match: the rule match.
# loc1Idx: the locations array index of the first location.
# loc2Idx: the locations array index of the second location.
# returns the squared distance between the two locations.
def location_dist_sq( match, loc1Idx, loc2Idx ):
    return dist_squared( match.locations[int(loc1Idx)], match.locations[int(loc2Idx)] )
# Finds the items in a list of matches that have the shortest distance between two 
#       specified locations. 
# matchList: the list of matches.
# loc1Idx: the locations array index of the first location.
# loc2Idx: the locations array index of the second location.
# returns the match(es) that minimize the distance between the specified locations.
def filter_by_min_distance( matchList, loc1Idx, loc2Idx ):
    matchList.sort(lambda m1, m2: cmp(location_dist_sq(m1, int(loc1Idx), int(loc2Idx)), location_dist_sq(m2, int(loc1Idx), int(loc2Idx))) ) 
    minDist = location_dist_sq(matchList[0], int(loc1Idx), int(loc2Idx))    
    return filter(lambda m: feq(minDist, location_dist_sq(m, int(loc1Idx), int(loc2Idx)), 0.0000001), matchList)    
# Finds the items in a list of matches that have the greatest distance between two 
#       specified locations. 
# matchList: the list of matches.
# loc1Idx: the locations array index of the first location.
# loc2Idx: the locations array index of the second location.
# returns the match(es) that maximize the distance between the specified locations.
def filter_by_max_distance( matchList, loc1Idx, loc2Idx ):
    matchList.sort(lambda m1, m2: -cmp(location_dist_sq(m1, int(loc1Idx), int(loc2Idx)), location_dist_sq(m2, int(loc1Idx), int(loc2Idx))) ) 
    maxDist = location_dist_sq(matchList[0], int(loc1Idx), int(loc2Idx))    
    return filter(lambda m: feq(maxDist, location_dist_sq(m, int(loc1Idx), int(loc2Idx)), 0.0000001), matchList)    
# Gets the squared distance between two points as specified by two x/y coordinate 
#   floating point pairs.
# x1: the x coordinate for the first point.
# y1: the y coordinate for the first point.
# x2: the x coordinate for the second point.
# y2: the y coordinate for the second point.
def sq_dist( x1, y1, x2, y2 ):    
    dx = x2 - x1
    dy = y2 - y1    
    return math.sqrt(dx * dx + dy * dy)
# Filter that limits matches to those that are furthest from or nearest to the annotation.  
# This is a private procedure that can be accessed through filter_to_furthest_hole_row_or_col(...)
# and filter_to_nearest_hole_row_or_col(...)
#
# matches: list of all candidate matches.
# groupByLoc: a location which is the same for all matches for a single 
#   annotation (e.g. hole group reference, material anchor). 
# holeLoc: a named location to a hole that is annotated.
# rotationVar: the name of the variable that gives the annotation rotation 
# (e.g. provided by a Named Rotation constraint).
# column: True to filter to the furthest/nearest column and false to filter to the furthest/nearest row.
# furthest: True to filter to the furthest match and False to filter to the nearest match.
# Returns the filtered list of matches.
def __filter_to_hole_row_or_col( matches, groupByLoc, holeLoc, rotationVar, column, furthest ):
    groups = group_by_location( matches, groupByLoc )
    matches = []
    for key in groups:
        g = groups[key]
        angle = math.radians( float( g[0].vars[rotationVar] ) + 90.0 )
        x = g[0].locations[holeLoc].x + math.cos(angle) * 999.0
        y = g[0].locations[holeLoc].y + math.sin(angle) * 999.0    
        initializeDist = 0.0 if furthest else 9999999.0
        bestDist = initializeDist
        bestGrp = []
        holeProp = "Hole Col" if column else "Hole Row"
        if not g[0].locations[holeLoc].props.has_key(holeProp):
            holeProp = "Stagger " + holeProp
        holePropGrps = group_by_prop(g, holeLoc, holeProp)    
        for cGKey in holePropGrps:
            colG = holePropGrps[cGKey]        
            dist = initializeDist
            for m in colG:
                loc = m.locations[holeLoc] 
                d = sq_dist( x, y, loc.x, loc.y )
                if (furthest and d > dist) or ((not furthest) and d < dist):
                    dist = d
            if (furthest and dist > bestDist) or ((not furthest) and dist < bestDist):
                bestDist = dist
                bestGrp = colG
        matches.extend( bestGrp )
    return matches
# Filter that limits matches to those that are furthest from the annotation.  
#
# For a given hole group we ususally want the annotation lines to go through all 
# rows/columns in the hole group.  This filter groups all candidate matches for
# each annotation and removes any that don't dimension to the furthest row/column.
# matches: list of all candidate matches.
# groupByLoc: a location which is the same for all matches for a single 
#   annotation (e.g. hole group reference, material anchor). 
# holeLoc: a named location to a hole that is annotated.
# rotationVar: the name of the variable that gives the annotation rotation 
# (e.g. provided by a Named Rotation constraint).
# column: True to filter to the furthest column and false to filter to the furthest row.
# Returns the filtered list of matches.
def filter_to_furthest_hole_row_or_col( matches, groupByLoc, holeLoc, rotationVar, column ):
    return __filter_to_hole_row_or_col( matches, groupByLoc, holeLoc, rotationVar, column, True )
# Filter that limits matches to those that are nearest to the annotation.  
#
# This filter groups all candidate matches for each annotation and removes any that 
# don't dimension to the nearest row/column.
# matches: list of all candidate matches.
# groupByLoc: a location which is the same for all matches for a single 
#   annotation (e.g. hole group reference, material anchor). 
# holeLoc: a named location to a hole that is annotated.
# rotationVar: the name of the variable that gives the annotation rotation 
# (e.g. provided by a Named Rotation constraint).
# column: True to filter to the nearest column and false to filter to the nearest row.
# Returns the filtered list of matches.
def filter_to_nearest_hole_row_or_col( matches, groupByLoc, holeLoc, rotationVar, column ):
    return __filter_to_hole_row_or_col( matches, groupByLoc, holeLoc, rotationVar, column, False )
# Filter that limits matches to those that are furthest from or nearest to the annotation.  
# This is a private procedure that can be accessed through filter_to_nearest_location(...)
# and filter_to_furthest_location(...)
#
# matches: list of all candidate matches.
# groupByLoc: a location which is the same for all matches for a single 
#   annotation (e.g. hole group reference, material anchor). 
# loc: the location to which the furthest/nearest to annotation filter is applied.
# rotationVar: the name of the variable that gives the annotation rotation 
# (e.g. provided by a Named Rotation constraint).
# furthest: True to filter to the furthest match and False to filter to the nearest match.
# Returns the filtered list of matches.
def __filter_to_location( matches, groupByLoc, loc, rotationVar, furthest ):
    groups = group_by_location( matches, groupByLoc )
    matches = []
    for key in groups:
        g = groups[key]
        angle = math.radians( float( g[0].vars[rotationVar] ) + 90.0 )
        x = g[0].locations[loc].x + math.cos(angle) * 999.0
        y = g[0].locations[loc].y + math.sin(angle) * 999.0    
        initializeDist = 0.0 if furthest else 9999999.0
        bestDist = initializeDist
        bestGrp = []        
        locGrps = group_by_location(g, loc )    
        for lKey in locGrps:
            lGrp = locGrps[lKey]        
            dist = initializeDist
            for m in lGrp:
                l = m.locations[loc] 
                d = sq_dist( x, y, l.x, l.y )
                if (furthest and d > dist) or ((not furthest) and d < dist):
                    dist = d
            if (furthest and dist > bestDist) or ((not furthest) and dist < bestDist):
                bestDist = dist
                bestGrp = lGrp
        matches.extend( bestGrp )
    return matches
# Filter that limits matches to those that are nearest to the annotation.  
#
# matches: list of all candidate matches.
# groupByLoc: a location which is the same for all matches for a single 
#   annotation (e.g. hole group reference, material anchor). 
# loc: the location to which the nearest to annotation filter is applied.
# rotationVar: the name of the variable that gives the annotation rotation 
# (e.g. provided by a Named Rotation constraint).
# Returns the filtered list of matches.
def filter_to_nearest_location( matches, groupByLoc, loc, rotationVar ):
    return __filter_to_location( matches, groupByLoc, loc, rotationVar, False )
# Filter that limits matches to those that are furthest from the annotation.  
#
# matches: list of all candidate matches.
# groupByLoc: a location which is the same for all matches for a single 
#   annotation (e.g. hole group reference, material anchor). 
# loc: the location to which the furthest from annotation filter is applied.
# rotationVar: the name of the variable that gives the annotation rotation 
# (e.g. provided by a Named Rotation constraint).
# Returns the filtered list of matches.
def filter_to_furthest_location( matches, groupByLoc, loc, rotationVar ):
    return __filter_to_location( matches, groupByLoc, loc, rotationVar, True )
# Gives the dot product of two vectors
# vec1: the first vector.
# vec2: the second vector.
# Returns the dot product.
def dot_prod( vec1, vec2 ):
    return sum(p*q for p,q in zip(vec1, vec2))
# Gives the distance orthogonal (normal/perpendicular) to a specified direction between two points.
# l1: the first location point.
# l2: the second location point.
# rotation: the direciton.
# Returns the orthogonal distance.
def ortho_dist( l1, l2, rotation ):
    angle = math.radians( float(rotation) + 90.0 )
    h = math.sqrt( dist_squared( l1, l2 ) )
    vec1 = (math.cos(angle), math.sin(angle))
    vec2 = (l2.x - l1.x, l2.y - l1.y)
    v2Mag = math.sqrt(dot_prod(vec2,vec2))
    if feq( v2Mag, 0.0, 0.0001 ):
        return 0.0
    vec2 = (vec2[0] / v2Mag, vec2[1] / v2Mag)
    dotV1_V2 = dot_prod( vec1, vec2 )
    if feq( dotV1_V2, 0.0, 0.0001 ):
        return h
    # The result of the dot product is clamped here to prevent 'math domain errors' caused
    # by truncation errors causing the value to be outside of [-1.0,1.0].
    dotAngle = math.acos( min(1.0, max(dotV1_V2, -1.0) ) ) 
    return h * math.sin(dotAngle)
# Gives the distance orthogonal (normal/perpendicular) to a specified direction between two points.
# match: the rule match that is being tested.
# loc1Idx: the index of the first point.
# loc2Idx: the index of the second point.
# rotationVar: the variable name for the direciton.
# Returns the orthogonal distance.
def orthogonal_distance( match, loc1Idx, loc2Idx, rotationVar ):    
    l1 = match.locations[loc1Idx]
    l2 = match.locations[loc2Idx]
    return ortho_dist(l1, l2, float(match.vars[rotationVar]))
# Filter that limits matches to those with the minimum distance orthogonal to a 
# specified direction between two points.
# matchList: the list of candidate rule matches.
# loc1Idx: the index of the first point.
# loc2Idx: the index of the second point.
# rotationVar: the variable name for the direciton.
# Returns the matches with the minimum orthogonal distance.
def filter_by_min_orthogonal_distance( matchList, loc1Idx, loc2Idx, rotationVar ):
    matchList.sort(lambda m1, m2: cmp(orthogonal_distance(m1, int(loc1Idx), int(loc2Idx), rotationVar), orthogonal_distance(m2, int(loc1Idx), int(loc2Idx), rotationVar)) ) 
    minDist = orthogonal_distance(matchList[0], int(loc1Idx), int(loc2Idx), rotationVar)    
    return filter(lambda m: feq(minDist, orthogonal_distance(m, int(loc1Idx), int(loc2Idx), rotationVar), 0.0001), matchList)
# Filter that limits matches to those with the maximum distance orthogonal to a 
# specified direction between two points.
# matchList: the list of candidate rule matches.
# loc1Idx: the index of the first point.
# loc2Idx: the index of the second point.
# rotationVar: the variable name for the direciton.
# Returns the matches with the maximum orthogonal distance.
def filter_by_max_orthogonal_distance( matchList, loc1Idx, loc2Idx, rotationVar ):
    matchList.sort(lambda m1, m2: -cmp(orthogonal_distance(m1, int(loc1Idx), int(loc2Idx), rotationVar), orthogonal_distance(m2, int(loc1Idx), int(loc2Idx), rotationVar)) ) 
    maxDist = orthogonal_distance(matchList[0], int(loc1Idx), int(loc2Idx), rotationVar)    
    return filter(lambda m: feq(maxDist, orthogonal_distance(m, int(loc1Idx), int(loc2Idx), rotationVar), 0.0001), matchList)
# Rotates a point by a specified angle about a specified pivot point.
# rotatedPt: the point that gets rotated.
# angle: the magnitude of the rotation in radians.
# pivotPt: the pivot about which the point is rotated.
# Returns the point after it has been rotated.
def rotate_location( rotatedPt, angle, pivotPt ):
    pt = (rotatedPt[0] - pivotPt[0], rotatedPt[1] - pivotPt[1] )
    x = pt[0] * math.cos( angle ) - pt[1] * math.sin( angle )
    y = pt[0] * math.sin( angle ) + pt[1] * math.cos( angle )
    return (pivotPt[0] + x, pivotPt[1] + y)
# Splits a set of rule matches into two groups where the specified location falls on either
# side of a line defined by a split point and rotation direction.
# matches: the set of rule matches to split.
# locIdx: the index of the location that is checked to determine which side of the 
# split line it falls on.
# splitLocIdx: the index of the location through which the split line passes.
# rotationVar: the name of the variable that gives the rotation direction of the split line.
# Returns two lists of matches that fall on either side of the split line.
def split_locations( matches, locIdx, splitLocIdx, rotationVar ):
    groups = {}
    groups["high"] = []
    groups["low"] = []
    for m in matches:
        angle = math.radians( 360.0 - (float( m.vars[rotationVar] ) + 90.0) )        
        loc = m.locations[locIdx]
        splitLoc = m.locations[splitLocIdx]
        pt = rotate_location( (loc.x, loc.y), angle, (splitLoc.x, splitLoc.y) )
        if fge( pt[1], splitLoc.y, 0.0001 ):
            groups["high"].append( m )
        else:
            groups["low"].append( m )
    return groups
# Represents a two-dimensional point.
# Most procedures that take rule match location (e.g. matches[0].locations[0])
# as a parameter will also work with a Point2D and vice versa.
class Point2D:    
    x = 0.0 # the X coordinate
    y = 0.0 # the Y coordinate
    # Static factory method copy constructor for instantiating a Point2D from another
    # Point2D or a rule match location.
    # other: the object from which to initialize the new Point2D.
    @classmethod
    def Clone(cls, other):        
        p = cls()
        p.x = other.x
        p.y = other.y
        return p
    # Static factory method for instantiating a Point2D from x-y coordinates.
    # x: the X coordinate.
    # y: the Y coordinate.
    @classmethod
    def FromXY(cls, x, y):
        p = cls()
        p.x = x
        p.y = y
        return p
    # Checks if this point's x & y-coordinates are equal to another point's.
    # other: the other point.
    # accuracy: the accuracy of the comparison.
    def equals(self, other, accuracy = 0.0001):
        return self.xeq(other, accuracy) and self.yeq(other, accuracy)
                
        # Checks if this point's x & y-coordinates are equal to another point's.
    # other: the other point.
    # accuracy: the accuracy of the comparison.
    def notequals(self, other, accuracy = 0.0001):
        return self.xne(other, accuracy) and self.yne(other, accuracy)
    # Checks if this point's x-coordinate is equal to another point's.
    # other: the other point.
    # accuracy: the accuracy of the comparison.
    def xeq(self, other, accuracy = 0.0001):
        return feq(self.x, other.x, accuracy)
    # Checks if this point's x-coordinate is not equal to another point's.
    # other: the other point.
    # accuracy: the accuracy of the comparison.
    def xne(self, other, accuracy = 0.0001):
        return not self.xeq(other, accuracy)
    # Checks if this point's x-coordinate is greater than another point's.
    # other: the other point.
    # accuracy: the accuracy of the comparison.
    def xgt(self, other, accuracy = 0.0001):
        return fgt(self.x, other.x, accuracy)
    # Checks if this point's x-coordinate is less than another point's.
    # other: the other point.
    # accuracy: the accuracy of the comparison.
    def xlt(self, other, accuracy = 0.0001):
        return flt(self.x, other.x, accuracy)
    # Checks if this point's x-coordinate is less than or equal to another point's.
    # other: the other point.
    # accuracy: the accuracy of the comparison.
    def xle(self, other, accuracy = 0.0001):
        return not self.xgt(other, accuracy)
    # Checks if this point's x-coordinate is greater than or equal to another point's.
    # other: the other point.
    # accuracy: the accuracy of the comparison.
    def xge(self, other, accuracy = 0.0001):
        return not self.xlt(other, accuracy)
    # Checks if this point's y-coordinate is equal to another point's.
    # other: the other point.
    # accuracy: the accuracy of the comparison.
    def yeq(self, other, accuracy = 0.0001):
        return feq(self.y, other.y, accuracy)
    # Checks if this point's y-coordinate is not equal to another point's.
    # other: the other point.
    # accuracy: the accuracy of the comparison.
    def yne(self, other, accuracy = 0.0001):
        return not self.yeq(other, accuracy)
    # Checks if this point's y-coordinate is greater than another point's.
    # other: the other point.
    # accuracy: the accuracy of the comparison.
    def ygt(self, other, accuracy = 0.0001):
        return fgt(self.y, other.y, accuracy)
    # Checks if this point's y-coordinate is less than another point's.
    # other: the other point.
    # accuracy: the accuracy of the comparison.
    def ylt(self, other, accuracy = 0.0001):
        return flt(self.y, other.y, accuracy)
    # Checks if this point's y-coordinate is less than or equal to another point's.
    # other: the other point.
    # accuracy: the accuracy of the comparison.
    def yle(self, other, accuracy = 0.0001):
        return not self.ygt(other, accuracy)
    # Checks if this point's y-coordinate is greater than or equal to another point's.
    # other: the other point.
    # accuracy: the accuracy of the comparison.
    def yge(self, other, accuracy = 0.0001):
        return not self.ylt(other, accuracy)
    # Gives the Euclidean distance between this and another point.
    # other: the other point.
    # Returns the distance.
    def dist(self, other):
        dX = self.x - other.x
        dY = self.y - other.y
        return math.sqrt( dX * dX + dY * dY )
    # Gives the distance between this and another point in the x-direction.
    # other: the other point.
    # Returns the distance.
    def xDist(self, other):
        return math.fabs( self.x - other.x )
    # Gives the distance between this and another point in the y-direction.
    # other: the other point.
    # Returns the distance.
    def yDist(self, other):
        return math.fabs( self.y - other.y )
    # Assuming the point is a vector returns its magnitude.
    # Returns: the magnitude.
    def vectorMagnitude(self):
        return self.dist( Point2D.FromXY( 0, 0 ) )
    # Calculates the sum of two points that represent vectors.
    # other: the other vector.
    # Returns the vector sum.
    def vectorAdd(self, other):
        return Point2D.FromXY( self.x + other.x, self.y + other.y )
    # Calculates the difference of two points that represent vectors.
    # other: the other vector.
    # Returns the vector difference.
    def vectorSubtract(self, other):
        return Point2D.FromXY( self.x - other.x, self.y - other.y )
    # Calculates the product of this vector and a scalar factor.
    # factor: the scalar factor.
    # Returns the procuct.
    def scalarMultiply(self, factor):
        return Point2D.FromXY( self.x * factor, self.y * factor )
    # Assuming the point is a vector returns the unit vector.
    # Returns the unit vector.
    def unit(self):
        dot = self.dot(self)
        if dot == 0.0:
            return self
        return self.scalarMultiply( 1.0 / math.sqrt(dot) )                
    # Calculates the dot product of this and another vector.
    # other: the other vector.
    # Returns the dot product.
    def dot(self, other):
        return self.x * other.x + self.y * other.y
    # Checks if this point is colinear with two other points (i.e. they are all on the same line).
    # other1: the first point.
    # other2: the second point.
    # Returns True if the points are colinear and false otherwise.
    def IsColinear(self, other1, other2, accuracy = 0.0001):
        pt1 = Point2D.Clone( other1 )
        pt2 = Point2D.Clone( other2 )
        if self.equals(pt1) or self.equals(pt2):
            return True                
        v1 = pt1.vectorSubtract( self ).unit()
        v2 = pt2.vectorSubtract( self ).unit()
        dot = math.fabs( v1.dot(v2) )
        return feq( dot, 1.0, accuracy )
    # Checks if this point is colinear with another point along a line that passes through the 
    # point at a specified angle.
    # other: the other point.
    # angle: the angle of the line.
    # Returns True if the points are colinear and False otherwise.
    def IsColinearAngle(self, other, angle, accuracy = 0.0001 ):        
        o = Point2D.Clone(other)
        ang = math.radians(float(angle)+90.0)
        d = self.dist(o) * 2.0
        ox = Point2D.FromXY(d * math.cos(ang), d * math.sin(ang))
        os = o.vectorSubtract(ox)
        oe = o.vectorAdd(ox)
        dr = os.dist(oe)
        dps = os.dist(self)
        dpe = oe.dist(self)
        return feq(dr, dps + dpe, accuracy)
    # Rotates the point around a specified pivot by a specified angle.
    # angle: the angle to rotate.
    # pivot: the pivot point for the rotation.
    # Returns the rotated point.
    def rotate( self, angle, pivot ):
        ang = math.radians(float(angle)+90.0)
        pt = self.vectorSubtract(pivot)
        dx = pt.x * math.cos( ang ) - pt.y * math.sin( ang )
        dy = pt.x * math.sin( ang ) + pt.y * math.cos( ang )
        return Point2D.FromXY(pivot.x + dx, pivot.y + dy)
    
# Gives the centroid of a list of points.
# locationList: the list of points for which to calculate the centroid.
# Returns the centroid.
def centroid( locationList ):
    xSum = 0;
    ySum = 0;
    for l in locationList:
        xSum += l.x
        ySum += l.y
    size = len( locationList )
    return Point2D.FromXY( xSum / size, ySum / size )

def is_setup(key,value):
    return get_drawing_setup_option(key) == value