# -*- coding: utf-8 -*-


VERSION = 20200103


'''
DESCRIPTION
    This calculator applies the cantilever method to perform the structural analysis of
    a horizontal or vertical (circular) cylindrical antenna structure, 
    subject to both wind and gravitational lateral loads.


USAGE
    See: https://hamwaves.com/tapers/en/index.html


COPYRIGHT
    Copyright (i) 2015-2020 Serge Y. Stroobandt

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program. If not, see http://www.gnu.org/licenses/


CONTACT
    ON4AA
    xdg-open mailto:$(echo c2VyZ2VAc3Ryb29iYW5kdC5jb20K |base64 -d)


TODO
    - Make Cdrag a function of wind speed.
    - Check validity with additional references about:
                Material('Al 6063-T832', 247, 2700), \
                Material('Al 6063-T835', 282, 2700), \
'''


### IMPORTS ###


from browser import alert, document
from browser.html import OPTION

from math import *
import time


### CLASSES ###


class Material(object):

    def __init__(self, name, YS, rho):
        self.name = name    # material name
        self.YS = YS        # yield strength in N/mm²
        self.rho = rho      # volumetric mass density in kg/m³


class Section(object):
    pass


### GLOBAL VARIABLES ###


MATERIALS = [
            Material('', '', ''), \
            Material('AlMgSi 0.5 F22 (EU)', 160, 2700), \
            Material('AlMgSi 0.5 6060-T66', 160, 2700), \
            Material('Al 6061-T6', 240, 2700), \
            Material('Al 6063-T6', 170, 2700), \
            Material('Al 6063-T832', 247, 2700), \
            Material('Al 6063-T835', 282, 2700), \
            Material('stainless 1.4301', 340, 7900), \
            Material('stainless 1.4404', 300, 7980), \
            Material('steel S235/Gr. B', 235, 7850), \
            Material('steel S275/X-42', 275, 7850), \
            Material('steel S355/X-52', 355, 7850), \
            Material('steel S420/X-60', 420, 7850), \
            Material('steel S460/X-65', 460, 7850), \
            Material('steel X-70', 485, 7850), \
            Material('brass C330', 344, 8411), \
            Material('Cu hard', 322, 8960), \
            Material('Cu soft', 46, 8960), \
            Material('PVC hard', 58, 1400) \
            ]

N = 16    # number of section columns

SECTIONS = [Section() for _ in range(N+1)]    # SECTIONS[0] is for totals.


### FUNCTIONS ###


def powerise10(x):
    ''' Returns x as a*10**b with 0 <= a < 10. '''

    if x == 0: return 0, 0

    Neg = x < 0

    if Neg: x = -x

    a = 1.0 * x / 10**(floor(log10(x)))
    b = int(floor(log10(x)))

    if Neg: a = -a

    return a, b


def eng(x):
    ''' Returns a string representing x in an engineer friendly notation. '''

    if x == None: return ''

    a, b = powerise10(x)

    if -3 < b < 3: return '%.4g' % x

    a = a * 10**(b%3)
    b = b - b%3

    return '%.4gE%s' % (a, b)


def list_materials():

    for i in range(1, N+1):    # Iterate over sections.
        for material in range(1, len(MATERIALS)):    # Iterate over MATERIALS.
            document['material_' + str(i)] <= OPTION(MATERIALS[material].name)


def get_sections(something):

    if something:
        try:
            # If it is an event, this will get the section number and return it in a list.
            return [int(something.target.id.rpartition('_')[2])]
        except:
            # If it happens to be a section number; it will be returned in a list.
            return [something]
    else:
        # None was given; iterate over the sections and return these in a list.
        return range(1, N+1)


def calculate(event):

    wind = document['wind']
    vwind = wind.options[wind.selectedIndex].value
    document['vwind'].value = vwind

    vwind = float(vwind) / 3.6    # in m/s

    if document['circular'].checked:
        Cd = 1.18
    if document['square'].checked:
        Cd = 2.05

    global pwind
    pwind = 1.3413 / 2 * vwind**2 * Cd

    document['pwind'].value = '%d' % pwind

    calculate_geometry(None)


def update_material_properties(something):

    for i in get_sections(something):

        SECTIONS[i].material = material = MATERIALS[document['material_' + str(i)].selectedIndex]

        if material.name:
            SECTIONS[i].YS = material.YS * 1E6    # in N/m²
            YS = material.YS    # in N/mm²
            SECTIONS[i].rho = rho = material.rho
        else:
            SECTIONS[i].YS = YS = ''
            SECTIONS[i].rho = rho = ''

            # Clear material/section function
            SECTIONS[i].OD = None
            SECTIONS[i].ID = None
            SECTIONS[i].A = None
            SECTIONS[i].S = None
            SECTIONS[i].l = None

            document['OD_' + str(i)].value = ''
            document['twall_' + str(i)].value = ''
            document['ID_' + str(i)].value = ''
            document['A_' + str(i)].value = ''
            document['S_' + str(i)].value = ''
            document['l_' + str(i)].value = ''

        document['YS_' + str(i)].value = YS    # in N/mm²
        document['rho_' + str(i)].value = rho

        calculate_maxima(i)

    calculate_loads(something)


def calculate_geometry(something):

    for i in get_sections(something):

        try:
            SECTIONS[i].OD = OD = float(document['OD_' + str(i)].value) * 1E-3
            SECTIONS[i].twall = twall = float(document['twall_' + str(i)].value) * 1E-3
            SECTIONS[i].ID = ID = OD - 2*twall

            if document['circular'].checked:
                SECTIONS[i].A = A = pi/4 * (SECTIONS[i].OD**2 - SECTIONS[i].ID**2)
                SECTIONS[i].S = S = pi/32 * (OD**4 - ID**4) / OD

            if document['square'].checked:
                SECTIONS[i].A = A = SECTIONS[i].OD**2 - SECTIONS[i].ID**2
                SECTIONS[i].S = S = 1/6 * (OD**4 - ID**4) / OD

        except:
            SECTIONS[i].ID = ID = None
            SECTIONS[i].A = A = None
            SECTIONS[i].S = S = None

        document['ID_' + str(i)].value = "%.1f" % (ID * 1E3) if ID else ''
        document['A_' + str(i)].value = eng(A)
        document['S_' + str(i)].value = eng(S)

        calculate_maxima(i)

    calculate_loads(something)


def calculate_maxima(i):

    try:
        SECTIONS[i].Fmax = Fmax = SECTIONS[i].YS * SECTIONS[i].A
        Fmax /= 1E3    # in kN
        SECTIONS[i].Mmax = Mmax = SECTIONS[i].YS * SECTIONS[i].S

    except:
        SECTIONS[i].Fmax = Fmax = None
        SECTIONS[i].Mmax = Mmax = None

    document['Fmax_' + str(i)].value = eng(Fmax)    # in kN
    document['Mmax_' + str(i)].value = eng(Mmax)


def calculate_loads(something):

    for i in get_sections(something):

        try:
            SECTIONS[i].l = l = float(document['l_' + str(i)].value)

            SECTIONS[i].mmaterial = mmaterial = SECTIONS[i].A * l * SECTIONS[i].rho
            qmaterial = 10 * mmaterial / l

            tice = document['tice'].value
            tice = 0 if tice == '' else float(tice) * 1E-3    # in m
            if document['circular'].checked:
                Aice = pi/4 * ((SECTIONS[i].OD + 2*tice)**2 - SECTIONS[i].OD**2)
            if document['square'].checked:
                Aice = (SECTIONS[i].OD + 2*tice)**2 - SECTIONS[i].OD**2
            rhoice = 916.8    # in kg/m³
            SECTIONS[i].mice = mice = Aice * l * rhoice
            qice = mice * 10 / l

            qwind = (SECTIONS[i].OD + 2*tice) * pwind

            if document['horizontal'].checked:
                SECTIONS[i].q = q = qmaterial + qice + qwind
            if document['vertical'].checked:
                SECTIONS[i].q = q = qwind

        except:
            mmaterial = None
            mice = None
            qwind = None
            SECTIONS[i].l = None
            SECTIONS[i].mmaterial = None
            SECTIONS[i].mice = None
            SECTIONS[i].q = q = None

        document['mmaterial_' + str(i)].value = eng(mmaterial)
        document['mice_' + str(i)].value = eng(mice)
        document['qwind_' + str(i)].value = eng(qwind)
        document['q_' + str(i)].value = eng(q)

    ltotal = 0
    mmaterialtotal = 0
    micetotal = 0

    for i in range(1, N+1):
        try: 
            ltotal += SECTIONS[i].l
        except:
            break
        try:
            mmaterialtotal += SECTIONS[i].mmaterial
            micetotal += SECTIONS[i].mice
        except:
            pass

    document['l'].value = eng(ltotal)
    document['mmaterial'].value = eng(mmaterialtotal)
    document['mice'].value = eng(micetotal)

    calculate_forces(None)


def calculate_forces(something):

    document['txt'].clear()

    document['txt'] <= 'Structural Analysis of Tapered Antenna Elements — v{}\n'.format(VERSION)
    document['txt'] <= '  https://hamwaves.com/tapers/\n'

    t = time.time()
    document['txt'] <= '\nNOTE\n'
    document['txt'] <= '  {}\n'.format(document['note'].value) if document['note'].value else ''
    document['txt'] <= '  {}\n'.format(time.strftime('%Y-%m-%d %H:%M', time.localtime(t)))

    offset = 16
    document['txt'] <= '\nCONDITIONS\n'
    document['txt'] <= '  {:{offset}} {}\n'.format('orientation', 'horizontal' if document['horizontal'].checked else 'vertical', offset=offset)
    document['txt'] <= '  {:{offset}} {}\n'.format('cross-section', 'circular' if document['circular'].checked else 'square', offset=offset)
    document['txt'] <= '  {:{offset}} t_ice = {} mm\n'.format('ice thickness', document['tice'].value, offset=offset)
    document['txt'] <= '  {:{offset}} v_wind = {} km/h\n'.format('wind speed', document['vwind'].value, offset=offset)

    offset = 26

    for i in range(1, N+1):    # Iterate over all sections.

        try:
            F0 = document['F0_' + str(i)].value
            F0 = 0 if F0 == '' else float(F0) * 1E3    # in N
            M0 = document['M0_' + str(i)].value
            M0 = 0 if M0 == '' else float(M0)
            q = SECTIONS[i].q
            l = SECTIONS[i].l

            if i == 1:
                SECTIONS[i].F = F = F0 + q * l
                SECTIONS[i].M = M = M0 + F0 * l + q * l**2 / 2
            else:
                SECTIONS[i].F = F = SECTIONS[i-1].F + F0 + q * l
                SECTIONS[i].M = M = SECTIONS[i-1].M + M0 + (SECTIONS[i-1].F + F0) * l + q * l**2 / 2

            document['txt'] <= '\nSECTION {}\n'.format(i)
            document['txt'] <= '  {:{offset}} OD = {} mm\n'.format('outer dimension', SECTIONS[i].OD *1E3, offset=offset)
            document['txt'] <= '  {:{offset}} t_wall = {} mm\n'.format('wall thickness', SECTIONS[i].twall *1E3, offset=offset)
            document['txt'] <= '  {:{offset}} ID = {:.1f} mm\n'.format('inner dimension', SECTIONS[i].ID *1E3, offset=offset)
            document['txt'] <= '  {:{offset}} {}\n'.format('material', SECTIONS[i].material.name, offset=offset)
            document['txt'] <= '  {:{offset}} YS = {} N/mm²\n'.format('yield strength', SECTIONS[i].material.YS, offset=offset)
            document['txt'] <= '  {:{offset}} ℓ = {} m\n'.format('length', SECTIONS[i].l, offset=offset)
            document['txt'] <= '  {:{offset}} m_material = {} kg\n'.format('material mass', eng(SECTIONS[i].mmaterial), offset=offset)
            document['txt'] <= '  {:{offset}} F_0 = {} kN\n'.format('initial shear force', F0 * 1E-3, offset=offset)
            document['txt'] <= '  {:{offset}} M_0 = {} Nm\n'.format('initial bending moment', M0, offset=offset)

            Fmax = SECTIONS[i].Fmax
            if 0.6 * Fmax < F < Fmax:
                document['F_' + str(i)].style = {'backgroundColor': '#fc3'}
                document['txt'] <= '  {:{offset}} F_i = {:>7} kN < {:>7} kN    CRITICAL\n'.format('terminal shear force', eng(F * 1E-3), eng(Fmax * 1E-3), offset=offset)
            elif F >= Fmax:
                document['F_' + str(i)].style = {'backgroundColor': '#f88'}
                document['txt'] <= '  {:{offset}} F_i = {:>7} kN ⩾ {:>7} kN    FAIL\n'.format('terminal shear force', eng(F * 1E-3), eng(Fmax * 1E-3), offset=offset)
            else:
                document['F_' + str(i)].style = {'backgroundColor': '#9e0'}
                document['txt'] <= '  {:{offset}} F_i = {:>7} kN ≪ {:>7} kN    SAFE\n'.format('terminal shear force', eng(F * 1E-3), eng(Fmax * 1E-3), offset=offset)

            Mmax = SECTIONS[i].Mmax
            if 0.6 * Mmax < M < Mmax:
                document['M_' + str(i)].style = {'backgroundColor': '#fc3'}
                document['txt'] <= '  {:{offset}} M_i = {:>7} Nm < {:>7} Nm    CRITICAL\n'.format('terminal bending moment', eng(M), eng(Mmax), offset=offset)
            elif M >= Mmax:
                document['M_' + str(i)].style = {'backgroundColor': '#f88'}
                document['txt'] <= '  {:{offset}} M_i = {:>7} Nm ⩾ {:>7} Nm    FAIL\n'.format('terminal bending moment', eng(M), eng(Mmax), offset=offset)
            else:
                document['M_' + str(i)].style = {'backgroundColor': '#9e0'}
                document['txt'] <= '  {:{offset}} M_i = {:>7} Nm ≪ {:>7} Nm    SAFE\n'.format('terminal bending moment', eng(M), eng(Mmax), offset=offset)

            F /= 1E3    # in kN


        except:
            SECTIONS[i].F = F = None
            SECTIONS[i].M = M = None
            document['F_' + str(i)].style = {'backgroundColor': '#e3e3d3'}
            document['M_' + str(i)].style = {'backgroundColor': '#e3e3d3'}

        document['F_' + str(i)].value = eng(F)    # in kN
        document['M_' + str(i)].value = eng(M)

    offset = 16
    document['txt'] <= '\nTOTALS\n'
    document['txt'] <= '  {:{offset}} ℓ = {} m\n'.format('total length', document['l'].value, offset=offset)
    document['txt'] <= '  {:{offset}} m_material = {} kg\n'.format('material mass', document['mmaterial'].value, offset=offset)
    document['txt'] <= '  {:{offset}} m_ice = {} kg\n'.format('ice mass', document['mice'].value, offset=offset)

    document['txt'] <= '\nDONATE\n'
    document['txt'] <= '  If this calculator proved any useful to you,\n'
    document['txt'] <= '  please, consider making a one-off donation\n'
    document['txt'] <= '  towards keeping me and the server up and running.\n'
    document['txt'] <= '  Thank you!'


### MAIN ###


document['brython'].style.display = 'initial'
list_materials()
calculate(None)


### EVENT HANDLERS ###


document['note'].bind('change', calculate_forces)
document['horizontal'].bind('change', calculate)
document['vertical'].bind('change', calculate)
document['tice'].bind('change', calculate)
document['circular'].bind('change', calculate)
document['square'].bind('change', calculate)
document['wind'].bind('change', calculate)

for i in range(1, N+1):    # Iterate over sections.
    document['material_' + str(i)].bind('change', update_material_properties)
    document['OD_' + str(i)].bind('input', calculate_geometry)
    document['twall_' + str(i)].bind('input', calculate_geometry)
    document['l_' + str(i)].bind('input', calculate_loads)
    document['F0_' + str(i)].bind('input', calculate_forces)
    document['M0_' + str(i)].bind('input', calculate_forces)
