FieldGraph

A PyObjC Example without documentation

Sources

CGraphController.py

import objc
import Cocoa
from PyObjCTools import AppHelper

from fieldMath import degToRad, radToDeg


#____________________________________________________________
class CGraphController(Cocoa.NSObject):
    graphModel = objc.IBOutlet()
    graphView = objc.IBOutlet()
    fieldNormalizeCheck = objc.IBOutlet()
    settingDrawer = objc.IBOutlet()
    fieldSlider0 = objc.IBOutlet()
    fieldSlider1 = objc.IBOutlet()
    fieldSlider2 = objc.IBOutlet()
    phaseSlider0 = objc.IBOutlet()
    phaseSlider1 = objc.IBOutlet()
    phaseSlider2 = objc.IBOutlet()
    spacingSlider = objc.IBOutlet()
    fieldDisplay0 = objc.IBOutlet()
    fieldDisplay1 = objc.IBOutlet()
    fieldDisplay2 = objc.IBOutlet()
    phaseDisplay0 = objc.IBOutlet()
    phaseDisplay1 = objc.IBOutlet()
    phaseDisplay2 = objc.IBOutlet()
    RMSGainDisplay = objc.IBOutlet()
    spacingDisplay = objc.IBOutlet()

#____________________________________________________________
# Update GUI display and control values

    def awakeFromNib(self):
        self.mapImage = Cocoa.NSImage.imageNamed_("Map")
        self.graphView.setMapImage(self.mapImage)
        self.drawGraph()

    def drawGraph(self):
        self.spacingDisplay.setFloatValue_(radToDeg(self.graphModel.getSpacing()))
        self.spacingSlider.setFloatValue_(radToDeg(self.graphModel.getSpacing()))
        self.fieldDisplay0.setFloatValue_(self.graphModel.getField(0))
        self.fieldDisplay1.setFloatValue_(self.graphModel.getField(1))
        self.fieldDisplay2.setFloatValue_(self.graphModel.getField(2))
        self.fieldSlider0.setFloatValue_(self.graphModel.getField(0))
        self.fieldSlider1.setFloatValue_(self.graphModel.getField(1))
        self.fieldSlider2.setFloatValue_(self.graphModel.getField(2))
        self.phaseDisplay0.setFloatValue_(radToDeg(self.graphModel.getPhase(0)))
        self.phaseDisplay1.setFloatValue_(radToDeg(self.graphModel.getPhase(1)))
        self.phaseDisplay2.setFloatValue_(radToDeg(self.graphModel.getPhase(2)))
        self.phaseSlider0.setFloatValue_(radToDeg(self.graphModel.getPhase(0)))
        self.phaseSlider1.setFloatValue_(radToDeg(self.graphModel.getPhase(1)))
        self.phaseSlider2.setFloatValue_(radToDeg(self.graphModel.getPhase(2)))

        totalField = self.graphModel.getField(0) + self.graphModel.getField(1) + self.graphModel.getField(2)

        RMSGain = self.graphModel.fieldGain()
        self.graphView.setGain(RMSGain, totalField)
        self.RMSGainDisplay.setFloatValue_(RMSGain*100.0)

        path, maxMag  = self.graphModel.getGraph()
        self.graphView.setPath(path, maxMag)


#____________________________________________________________
# Handle GUI values

    @objc.IBAction
    def fieldDisplay0_(self, sender):
        self.setNormalizedField(0, sender.floatValue())
        self.drawGraph()

    @objc.IBAction
    def fieldDisplay1_(self, sender):
        self.setNormalizedField(1, sender.floatValue())
        self.drawGraph()

    @objc.IBAction
    def fieldDisplay2_(self, sender):
        self.setNormalizedField(2, sender.floatValue())
        self.drawGraph()

    @objc.IBAction
    def fieldSlider0_(self, sender):
        self.setNormalizedField(0, sender.floatValue())
        self.drawGraph()

    @objc.IBAction
    def fieldSlider1_(self, sender):
        self.setNormalizedField(1, sender.floatValue())
        self.drawGraph()

    @objc.IBAction
    def fieldSlider2_(self, sender):
        self.setNormalizedField(2, sender.floatValue())
        self.drawGraph()

    @objc.python_method
    def setNormalizedField(self, t, v):
        if self.fieldNormalizeCheck.intValue():
            f = [0, 0, 0]
            cft = 0
            for i in range(3):
                f[i] = self.graphModel.getField(i)
                cft += f[i]

            aft = cft - v
            if aft < 0.001:
                v = cft - 0.001
                aft = 0.001
            f[t] = v

            nft = 0
            for i in range(3):
                nft +=  f[i]
            r = aft / (nft - f[t])

            for i in range(3):
                self.graphModel.setField(i, f[i] * r)
            self.graphModel.setField(t, v)

        else:
            self.graphModel.setField(t, v)


    @objc.IBAction
    def phaseDisplay0_(self, sender):
        self.graphModel.setPhase(0, degToRad(sender.floatValue()))
        self.drawGraph()

    @objc.IBAction
    def phaseDisplay1_(self, sender):
        self.graphModel.setPhase(1, degToRad(sender.floatValue()))
        self.drawGraph()

    @objc.IBAction
    def phaseDisplay2_(self, sender):
        self.graphModel.setPhase(2, degToRad(sender.floatValue()))
        self.drawGraph()

    @objc.IBAction
    def phaseSlider0_(self, sender):
        self.graphModel.setPhase(0, degToRad(sender.floatValue()))
        self.drawGraph()

    @objc.IBAction
    def phaseSlider1_(self, sender):
        self.graphModel.setPhase(1, degToRad(sender.floatValue()))
        self.drawGraph()

    @objc.IBAction
    def phaseSlider2_(self, sender):
        self.graphModel.setPhase(2, degToRad(sender.floatValue()))
        self.drawGraph()

    @objc.IBAction
    def spacingDisplay_(self, sender):
        self.graphModel.setSpacing(degToRad(sender.floatValue()))
        self.drawGraph()

    @objc.IBAction
    def spacingSlider_(self, sender):
        self.graphModel.setSpacing(degToRad(sender.floatValue()))
        self.drawGraph()

    @objc.IBAction
    def settingDrawerButton_(self, sender):
        self.settingDrawer.toggle_(self)

CGraphModel.py

import objc
from Foundation import NSObject
from AppKit import NSBezierPath

from fieldMath import bessel, degToRad, polarToRect
from math import cos, sin, sqrt, hypot, pi

#____________________________________________________________
class CGraphModel(NSObject):

    def init(self):
        self.field = [1.0, 1.12, 0.567]
        self.phase = [degToRad(0), degToRad(152.6), degToRad(312.9-360)]
        self.RMSGain = 0
        self.spacing = degToRad(90)
        return self

    def getGraph(self):
        path = NSBezierPath.bezierPath()

        maxMag = 0
        mag = self.fieldValue(0)

        maxMag = max(maxMag, mag)
        path.moveToPoint_(polarToRect((mag, 0)))
        for deg in range(1, 359, 1):
            r = (deg/180.0)*pi
            mag = self.fieldValue(r)
            maxMag = max(maxMag, mag)
            path.lineToPoint_(polarToRect((mag, r)))
        path.closePath()

        return path, maxMag;

    @objc.python_method
    def fieldGain(self):
        gain = 0
        Et = self.field[0] + self.field[1] + self.field[2]
        if Et:          # Don't want to divide by zero in the pathological case
            spacing = [0, self.spacing, 2*self.spacing]

            # This could easily be optimized--but this is just anexample :-)
            for i in range(3):
                for j in range(3):
                    gain += self.field[i]*self.field[j] * cos(self.phase[j]-self.phase[i]) * bessel(spacing[j]-spacing[i])
            gain = sqrt(gain) / Et

        self.RMSGain = gain
        return gain

    @objc.python_method
    def fieldValue(self, a):
        # The intermedate values are used to more closely match standard field equations nomenclature
        E0 = self.field[0]
        E1 = self.field[1]
        E2 = self.field[2]
        B0 = self.phase[0]
        B1 = self.phase[1] + self.spacing * cos(a)
        B2 = self.phase[2] + 2 * self.spacing * cos(a)

        phix = sin(B0) * E0  + sin(B1) * E1 + sin(B2) * E2
        phiy = cos(B0) * E0 + cos(B1) * E1 + cos(B2) * E2
        mag = hypot(phix, phiy)

        return mag


    @objc.python_method
    def setField(self, tower, field):
        self.field[tower] = field

    @objc.python_method
    def getField(self, tower):
        return self.field[tower]

    @objc.python_method
    def setPhase(self, tower, phase):
        self.phase[tower] = phase

    @objc.python_method
    def getPhase(self, tower):
        return self.phase[tower]

    @objc.python_method
    def setSpacing(self, spacing):
        self.spacing = spacing

    @objc.python_method
    def getSpacing(self):
        return self.spacing

CGraphView.py

import objc
from objc import super
import Cocoa
from math import pi, sin, cos

from fieldMath import degToRad

# Convience global variables
x, y = 0, 1
llc, sze = 0, 1 # Left Lower Corner, Size

#____________________________________________________________
class CGraphView (Cocoa.NSView):

    azmuthSlider = objc.IBOutlet()
    mapOffsetEWSlider = objc.IBOutlet()
    mapOffsetNSSlider = objc.IBOutlet()
    mapScaleSlider = objc.IBOutlet()
    mapVisibleSlider = objc.IBOutlet()
    azmuthDisplay = objc.IBOutlet()
    mapOffsetEWDisplay = objc.IBOutlet()
    mapOffsetNSDisplay = objc.IBOutlet()
    mapScaleDisplay = objc.IBOutlet()

    def initWithFrame_(self, frame):
        super(CGraphView, self).initWithFrame_(frame)
        self.setGridColor()
        self.setRmsColor()
        self.setGraphColor()
        self.graphMargin = 2
        self.mapImage = 0
        self.mapRect = 0
        self.mapVisible = 0.70
        self.mapScale = 3.0
        self.mapOffsetEW = 0.27
        self.mapOffsetNS = 0.40
        self.mapBaseRadius = 200

        self.lines = 2
        self.gain = 0.5
        return self

    def awakeFromNib(self):
        self.setCrossCursor()
        self.mapVisibleSlider.setFloatValue_(self.mapVisible)
        self.setAzmuth_(125)
        self.setMapRect()

    @objc.python_method
    def setCrossCursor(self):
        crosshairImage = Cocoa.NSImage.imageNamed_("CrossCursor")
        imageSize = crosshairImage.size()
        self.crossCursor = Cocoa.NSCursor.alloc().initWithImage_hotSpot_(crosshairImage, (8, 8))
        rect = self.bounds()
        self.trackingRect = self.addTrackingRect_owner_userData_assumeInside_(self.bounds(), self, 0, 0)

    @objc.python_method
    def setGridColor(self, color=Cocoa.NSColor.greenColor()):
        self.gridColor = color

    @objc.python_method
    def setRmsColor(self, color=Cocoa.NSColor.blueColor()):
        self.rmsColor = color

    @objc.python_method
    def setGraphColor(self, color=Cocoa.NSColor.blackColor()):
        self.graphColor = color

    @objc.python_method
    def setGain(self, gain, total):
        self.gain = gain
        self.totalField = total

    @objc.python_method
    def setLines(self, lines):
        self.lines = lines

    @objc.python_method
    def setMapImage(self, mapImage):
        self.mapImage = mapImage
        self.mapRect = ((0, 0), mapImage.size())

    @objc.python_method
    def setPath(self, path, maxMag):
        self.path = path
        self.maxMag = maxMag
        self.setNeedsDisplay_(1)


    def drawRect_(self, rect):
        frame = self.frame()
        self.origin = frame[0]
        self.graphCenter = (frame[sze][x]/2, frame[sze][y]/2)
        self.graphRadius = (min(frame[sze][x], frame[sze][y]) / 2) - self.graphMargin

        Cocoa.NSColor.whiteColor().set()
        Cocoa.NSRectFill(self.bounds())

        self.drawMap()
        self.drawGrid()
        self.drawRMS()
        self.drawField()

    def drawMap(self):
        if self.mapImage == 0:
            return

        scale = self.mapScale * (self.graphRadius / self.mapBaseRadius) * self.gain / self.totalField
        xImageSize = scale * self.mapRect[sze][x]
        yImageSize = scale * self.mapRect[sze][y]
        xCenterMove = self.graphCenter[x] - self.graphRadius
        yCenterMove = self.graphCenter[y] - self.graphRadius

        xOffset = -((1 - self.mapOffsetEW) / 2) * xImageSize
        yOffset = -((1 + self.mapOffsetNS) / 2) * yImageSize
        xOffset += self.graphRadius + xCenterMove
        yOffset += self.graphRadius + yCenterMove

        drawInRect = ((xOffset, yOffset), (xImageSize, yImageSize))

        self.mapImage.drawInRect_fromRect_operation_fraction_(drawInRect, self.mapRect, Cocoa.NSCompositeSourceOver, self.mapVisible)

    def drawGrid(self):
        self.gridColor.set()
        self.drawCircle_(1.0)
        self.drawAxisLines()

    def drawCircle_(self, scale):
        center = self.graphCenter
        radius = self.graphRadius*scale
        x, y = 0, 1
        if radius >= 1:
            dotRect = ((center[x]-radius, center[y]-radius), (2*radius, 2*radius))
            path = Cocoa.NSBezierPath.bezierPathWithOvalInRect_(dotRect)
            path.stroke()

    def drawRMS(self):
        self.rmsColor.set()
        self.drawCircle_(self.gain)

    def drawAxisLines(self):
        center = self.graphCenter
        radius = self.graphRadius
        x, y = 0, 1
        path = Cocoa.NSBezierPath.bezierPath()
        for i in range(1, self.lines+1):
            iR = pi / i
            cosR = cos(iR) * radius
            sinR = sin(iR) * radius

            path.moveToPoint_((center[x] - cosR, center[y] - sinR))
            path.lineToPoint_((center[x] + cosR, center[y] + sinR))
        path.closePath()
        path.stroke()

    def drawField(self):
        if self.maxMag:         # Don't want to divide by zero in the pathological case
            self.graphColor.set()
            path = self.path.copy()

            transform = Cocoa.NSAffineTransform.transform()
            transform.rotateByRadians_(-(pi / 2.0) - self.azmuth)
            path.transformUsingAffineTransform_(transform)

            transform = Cocoa.NSAffineTransform.transform()
            center = self.graphCenter
            transform.translateXBy_yBy_(center[0], center[1])
            transform.scaleBy_(self.graphRadius / self.maxMag)
            path.transformUsingAffineTransform_(transform)

            path.stroke()


#____________________________________________________________
# Handle GUI values
    @objc.IBAction
    def mapVisibleSlider_(self, sender):
        self.mapVisible = (sender.floatValue())
        self.setNeedsDisplay_(1)

    @objc.IBAction
    def azmuthDisplay_(self, sender):
        self.setAzmuth_(sender.floatValue())

    @objc.IBAction
    def azmuthSlider_(self, sender):
        self.setAzmuth_(sender.floatValue())

    def setAzmuth_(self, value):
        self.azmuth = degToRad(value)
        self.azmuthSlider.setFloatValue_(value)
        self.azmuthDisplay.setFloatValue_(value)
        self.setNeedsDisplay_(1)

    @objc.IBAction
    def mapScaleDisplay_(self, sender):
        self.mapScale = sender.floatValue()
        self.setMapRect()

    @objc.IBAction
    def mapScaleSlider_(self, sender):
        self.mapScale = sender.floatValue()
        self.setMapRect()

    @objc.IBAction
    def mapOffsetNSDisplay_(self, sender):
        self.mapOffsetNS = sender.floatValue()
        self.setMapRect()

    @objc.IBAction
    def mapOffsetNSSlider_(self, sender):
        self.mapOffsetNS = sender.floatValue()
        self.setMapRect()

    @objc.IBAction
    def mapOffsetEWDisplay_(self, sender):
        self.mapOffsetEW = sender.floatValue()
        self.setMapRect()

    @objc.IBAction
    def mapOffsetEWSlider_(self, sender):
        self.mapOffsetEW = sender.floatValue()
        self.setMapRect()

    def mouseUp_(self, event):
        loc = event.locationInWindow()
        xLoc = loc[x] - self.origin[x]
        yLoc = loc[y] - self.origin[y]
        xDelta = self.graphCenter[x] - xLoc
        yDelta = self.graphCenter[y] - yLoc


        scale = 0.5 * self.mapScale * (self.gain / self.totalField) * (self.graphRadius / self.mapBaseRadius)
        xOffset = xDelta / (scale * self.mapRect[sze][x])
        yOffset = yDelta / (scale * self.mapRect[sze][y])

        self.mapOffsetEW += xOffset
        self.mapOffsetNS -= yOffset
        self.setMapRect()

    def mouseDown_(self, event):
        self.crossCursor.set()


    def setMapRect(self):
        self.mapScaleDisplay.setFloatValue_(self.mapScale)
        self.mapScaleSlider.setFloatValue_(self.mapScale)
        self.mapOffsetEWDisplay.setFloatValue_(self.mapOffsetEW)
        self.mapOffsetEWSlider.setFloatValue_(self.mapOffsetEW)
        self.mapOffsetNSDisplay.setFloatValue_(self.mapOffsetNS)
        self.mapOffsetNSSlider.setFloatValue_(self.mapOffsetNS)
        self.setNeedsDisplay_(1)

    def mouseEntered_(self, event):
        print('CGraphView: mouseEntered_')

    def mouseExited_(self, event):
        print('CGraphView: mouseExited_')

Main.py

from PyObjCTools import AppHelper
import CGraphController
import CGraphModel
import CGraphView

AppHelper.runEventLoop()

fieldMath.py

from math import pi, sin, cos, hypot, sqrt

# Math functions

def degToRad(deg):
    return (deg/180.0)*pi

def radToDeg(rad):
    return (rad/pi)*180.0

def polarToRect(polar):
    r = polar[0]
    theta = polar[1]
    return (r*cos(theta), r*sin(theta))

def bessel(z, t=0.00001):
    j = 1
    jn = 1
    zz4 = z*z/4
    for k in range(1, 100):
        jn *= -1 * zz4 / (k*k)
        j += jn

        if jn < 0:
            if jn > t:
                break
        else:
            if jn < t:
                break
    return j

setup.py

"""
Script for building the example.

Usage:
    python3 setup.py py2app
"""
from setuptools import setup

plist = dict(CFBundleName="FieldGraph")
setup(
    name="FieldGraph",
    app=["Main.py"],
    data_files=["English.lproj", "CrossCursor.tiff", "Map.png"],
    options=dict(py2app=dict(plist=plist)),
    setup_requires=[
        "py2app",
        "pyobjc-framework-Cocoa",
    ]
)