# FieldGraph

- `Download example`

This shows an simple example of an MVC based application, that also makes use of `NSBezierPaths`.

The application calculates the field pattern and RMS field of an antenna array with up to three elements.

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

    # Convience 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(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")
            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"],
    )