FieldGraph¶
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 # 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"],
)