FieldGraph

A PyObjC Example without documentation

Sources

CGraphController.py

import Cocoa
import objc
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

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

import objc
from AppKit import NSBezierPath
from fieldMath import bessel, degToRad, polarToRect
from Foundation import NSObject


# ____________________________________________________________
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

from math import cos, pi, sin

import Cocoa
import objc
from fieldMath import degToRad
from objc import super  # noqa: A004

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

BLACK = Cocoa.NSColor.blackColor()
BLUE = Cocoa.NSColor.blueColor()
GREEN = Cocoa.NSColor.greenColor()


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().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")
        self.crossCursor = Cocoa.NSCursor.alloc().initWithImage_hotSpot_(
            crosshairImage, (8, 8)
        )
        self.trackingRect = self.addTrackingRect_owner_userData_assumeInside_(
            self.bounds(), self, 0, 0
        )

    @objc.python_method
    def setGridColor(self, color=GREEN):
        self.gridColor = color

    @objc.python_method
    def setRmsColor(self, color=BLUE):
        self.rmsColor = color

    @objc.python_method
    def setGraphColor(self, color=BLACK):
        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

import CGraphController  # noqa: F401
import CGraphModel  # noqa: F401
import CGraphView  # noqa: F401
from PyObjCTools import AppHelper

AppHelper.runEventLoop()

fieldMath.py

from math import cos, pi, sin

# 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 = {"CFBundleName": "FieldGraph"}
setup(
    name="FieldGraph",
    app=["Main.py"],
    data_files=["English.lproj", "CrossCursor.tiff", "Map.png"],
    options={"py2app": {"plist": plist}},
    setup_requires=["py2app", "pyobjc-framework-Cocoa"],
)