1
2
3var nasal_dir = getprop("/sim/fg-root") ~ "/Aircraft/Instruments-3d/ISFD/";
4io.load_nasal(nasal_dir ~ 'ISFD_gui.nas', "isfd");
5io.load_nasal(nasal_dir ~ 'ISFDGenericController.nas', "isfd");
6io.load_nasal(nasal_dir ~ 'PhysicalController.nas', "isfd");
7
8# main wrapper object
9var ISFD = {
10
11baseSize: 512,
12halfSize: 256,
13hsiHeight: 358,
14hsiWidth: 306,
15speedTapeWidth: 92,
16altTapeWidth: 114,
17pitchLadderDegreeSpacing: 8,
18rollBaseRadius: 160,
19roseRadius: 512,
20boxHeight: 48,
21
22needleBoxSize: 220,
23needleBoxHeight: 20,
24
25new : func(controller = nil) {
26    var obj = {
27        parents : [ISFD],
28        _mode : ''
29    };
30
31    ISFD.hsiLeft = ISFD.speedTapeWidth - ISFD.halfSize;
32    ISFD.hsiXCenter = ISFD.hsiLeft + ISFD.hsiWidth/2;
33    ISFD.hsiRight = ISFD.halfSize - ISFD.altTapeWidth;
34    ISFD.hsiBottom = ISFD.hsiHeight/2;
35    ISFD.modeBoxHeight = (ISFD.baseSize - ISFD.hsiHeight)/2;
36
37    obj.canvas = canvas.new({
38            "name" : "ISFD Canvas",
39            "size" : [ISFD.baseSize, ISFD.baseSize],
40            "view" : [ISFD.baseSize, ISFD.baseSize],
41            "mipmapping": 0,
42            });
43
44    obj.root = obj.canvas.createGroup();
45    # centering transform
46    obj.root.setTranslation(ISFD.halfSize, ISFD.halfSize);
47
48    var controllerClass = (controller == nil) ? isfd.GenericController : controller;
49    obj._controller = controllerClass.new(obj);
50    obj.createContents();
51    obj._updateTimer = maketimer(0.05, func obj.update(); );
52    obj._updateTimer.start();
53    obj._appMode = nil; # set to nil to force update on first update() trigger
54
55    return obj;
56},
57
58del: func()
59{
60    print('Deleting ISFD');
61    me._updateTimer.stop();
62},
63
64# display the ISFD canvas on the specified object
65display : func(target_object) {
66  me.canvas.addPlacement({"node": target_object});
67},
68
69displayGUI : func(my_isfd, scale=1.0) {
70    var gui = isfd.GUI.new(my_isfd, my_isfd.canvas, scale);
71},
72
73createContents : func()
74{
75    me.createPitchLadder();
76    me.createRollTicks();
77    me.createCompassRose();
78    me.createAltitudeTape();
79    me.createSpeedTape();
80
81    me.createSpeedBox();
82    me.createAltitudeBox();
83
84    me.createAltimeterSetting();
85    # mach readout - not on the B737 model?
86    me.createModeText();
87
88    me.createLocalizer();
89    me.createGlideslope();
90
91    me.createAirplaneMarker();
92
93},
94
95createAirplaneMarker : func()
96{
97    var markerGroup = me.root.createChild("group", "airplane-indicator-group");
98    markerGroup.setTranslation(ISFD.hsiXCenter, 0);
99
100	var m = markerGroup.createChild("path", "airplane-indicator");
101    m.setColorFill(0, 0, 0);
102    m.setStrokeLineWidth(2);
103    m.setColor(1, 1, 1);
104
105    var markerWidth = 8;
106    var hw = markerWidth / 2;
107    var horWidth = 80;
108    var vertExtension = markerWidth * 1.5;
109
110    m.moveTo(-hw, -hw);
111    m.line(markerWidth, 0);
112    m.line(0, markerWidth);
113    m.line(-markerWidth, 0);
114    m.close();
115
116    # left L
117    m.moveTo(-hw - markerWidth, -hw);
118    m.line(-horWidth, 0);
119    m.line(0, markerWidth);
120    m.line(horWidth - markerWidth, 0);
121    m.line(0, vertExtension);
122    m.line(markerWidth, 0);
123    m.close();
124
125
126    # right L
127    m.moveTo(hw + markerWidth, -hw);
128    m.line(horWidth, 0);
129    m.line(0, markerWidth);
130    m.line(markerWidth - horWidth, 0);
131    m.line(0, vertExtension);
132    m.line(-markerWidth, 0);
133    m.close();
134},
135
136# add a single radially aligned tick mark between radiui one and two
137addPolarTick : func(path, angle, r1, r2)
138{
139    var horAngle = angle + 90; # angle from +ve X axis
140    var sa = math.sin(horAngle * D2R);
141    var ca = math.cos(horAngle * D2R);
142
143    path.moveTo(ca * r1, sa * r1);
144    path.lineTo(ca * r2, sa * r2);
145    return path;
146},
147
148addHorizontalSymmetricPolarTick : func(path, angle, r1, r2)
149{
150    var horAngle = angle + 90; # angle from +ve X axis
151    var sa = -math.sin(horAngle * D2R);
152    var ca = math.cos(horAngle * D2R);
153
154    path.moveTo(ca * r1, sa * r1);
155    path.lineTo(ca * r2, sa * r2);
156    path.moveTo(-ca * r1, sa * r1);
157    path.lineTo(-ca * r2, sa * r2);
158    return path;
159},
160
161addHorizontalSymmetricLine : func(path, positiveLength, y)
162{
163    path.moveTo(-positiveLength, y);
164    path.lineTo(positiveLength, y);
165    return path;
166},
167
168addDiamond : func(path, radius)
169{
170    path.move(-radius, 0);
171    path.line(radius, -radius); # top
172    path.line(radius, radius); # right
173    path.line(-radius, radius); # bottom
174    path.close();
175},
176
177createDigitTape : func(parent, name, suffix = nil)
178{
179    var t = parent.createChild('text', name);
180    # 'top' zero (above 9)
181    var s = '0' ~ chr(10);
182    if (suffix != nil) {
183        s = '0' ~ suffix ~ chr(10);
184    }
185
186    for (var i=9; i>=0; i-=1) {
187        if (suffix != nil) {
188            s = s ~ i ~ suffix ~ chr(10);
189        } else {
190            s = s ~ i ~ chr(10);
191        }
192    }
193    t.setText(s);
194    t.setFont("LiberationFonts/LiberationMono-Regular.ttf");
195    t.setFontSize(44);
196   # t.set('line-height', 0.9);
197    t.setAlignment("left-bottom");
198    return t;
199},
200
201createRollTicks : func()
202{
203    # these don't move!
204    # center filled white arrow pointing down
205    # large tick at 30 deg
206    # minor tick at 10, 20 deg
207    # minor tick at 45?
208    # and major tick at 60 by the looks of it
209
210    me._rollGroup = me.root.createChild("group", "roll-group");
211    me._rollGroup.setTranslation(ISFD.hsiXCenter, 0);
212    me._rollGroup.set("clip-frame", canvas.Element.GLOBAL);
213    me._rollGroup.set("clip", "rect(77px, 398px, 435px, 92px)");
214
215	var rollScale = me.root.createChild("path", "roll-scale");
216    rollScale.setTranslation(ISFD.hsiXCenter, 0);
217
218    rollScale.setStrokeLineWidth(2);
219    rollScale.setColor(1, 1, 1);
220    var baseR = ISFD.rollBaseRadius;
221    var minorTick = 16;
222    var majorTick = 30;
223
224    me.addHorizontalSymmetricPolarTick(rollScale, 10, baseR, baseR + minorTick);
225    me.addHorizontalSymmetricPolarTick(rollScale, 20, baseR, baseR + minorTick);
226    me.addHorizontalSymmetricPolarTick(rollScale, 30, baseR, baseR + majorTick);
227    me.addHorizontalSymmetricPolarTick(rollScale, 45, baseR, baseR + minorTick);
228
229    # we cap the length of these to avoid sticking into the speed/alt tapes
230    me.addHorizontalSymmetricPolarTick(rollScale, 60, baseR, baseR + minorTick);
231    rollScale.close();
232
233    # add filled path for the zero arrow
234    rollZeroArrow = me.root.createChild("path", "roll-zero-mark");
235    rollZeroArrow.setColorFill(1, 1, 1);
236    rollZeroArrow.setTranslation(ISFD.hsiXCenter, 0);
237
238    # arrow extends from the roll radius to the top of HSI
239    var arrowHeight = (ISFD.hsiHeight / 2) - ISFD.rollBaseRadius;
240    var arrowHWidth = arrowHeight * (2/3);
241
242    rollZeroArrow.moveTo(0, -baseR);
243    rollZeroArrow.line(arrowHWidth, -arrowHeight);
244    rollZeroArrow.line(-arrowHWidth * 2, 0);
245    rollZeroArrow.close();
246
247    # and the moving arrow
248	var rollMarker = me._rollGroup.createChild("path", "roll-indicator");
249    rollMarker.setColorFill(0, 0, 0);
250    rollMarker.setStrokeLineWidth(2);
251    rollMarker.setColor(1, 1, 1);
252    rollMarker.moveTo(0, -baseR);
253    rollMarker.line(arrowHWidth, arrowHeight);
254    rollMarker.line(-arrowHWidth * 2, 0);
255    rollMarker.close();
256},
257
258createPitchLadder : func()
259{
260     # sky rect
261    var box = me.root.rect(ISFD.hsiLeft, -ISFD.hsiHeight/2,
262                           ISFD.hsiWidth, ISFD.hsiHeight);
263    box.setColorFill('#69B3F4');
264
265    me._pitchRotation = me.root.createChild("group", "pitch-rotation");
266    me.pitchGroup = me._pitchRotation.createChild("group", "pitch-group");
267
268    var bkGroup = me.root.createChild("group", "hsi-clip-group");
269
270
271    # ground rect
272    var box = me.pitchGroup.rect(-1000, 0, 2000, 2000);
273    box.setColorFill('#8F9552');
274    box.set("clip-frame", canvas.Element.GLOBAL);
275    box.set("clip", "rect(77px, 398px, 435px, 92px)");
276
277    var ladderGroup = me.pitchGroup.createChild("group", "pitch-ladder");
278    ladderGroup.set("clip-frame", canvas.Element.GLOBAL);
279    ladderGroup.set("clip", "rect(140px, 368px, 435px, 122px)");
280
281	var pitchLadder = ladderGroup.createChild("path", "pitch-ladder-ticks");
282    pitchLadder.setStrokeLineWidth(2);
283    pitchLadder.setColor(1, 1, 1);
284
285    var sp = ISFD.pitchLadderDegreeSpacing; # shorthand
286    var tenDegreeWidth = 64;
287    var fiveDegreeWidth = 32;
288    var twoFiveDegreeWidth = 24;
289
290    # add line at zero
291    me.addHorizontalSymmetricLine(pitchLadder, ISFD.halfSize, 0);
292
293    for (var i=1; i<=9; i+=1)
294    {
295        var d = i * 10;
296        me.addHorizontalSymmetricLine(pitchLadder, tenDegreeWidth, d * sp);
297        me.addHorizontalSymmetricLine(pitchLadder, tenDegreeWidth, -d * sp);
298
299        me.addHorizontalSymmetricLine(pitchLadder, fiveDegreeWidth, (d - 5) * sp);
300        me.addHorizontalSymmetricLine(pitchLadder, fiveDegreeWidth, (5 - d) * sp);
301
302        # 2.5 and 7.5 degree lines
303        me.addHorizontalSymmetricLine(pitchLadder, twoFiveDegreeWidth, (d - 2.5) * sp);
304        me.addHorizontalSymmetricLine(pitchLadder, twoFiveDegreeWidth, (2.5 - d) * sp);
305        me.addHorizontalSymmetricLine(pitchLadder, twoFiveDegreeWidth, (d - 7.5) * sp);
306        me.addHorizontalSymmetricLine(pitchLadder, twoFiveDegreeWidth, (7.5 - d) * sp);
307
308        # add text as well
309        var textUp = ladderGroup.createChild("text", "pitch-ladder-legend-" ~ d);
310        textUp.setText(d);
311        textUp.setAlignment("right-center");
312        textUp.setTranslation(-tenDegreeWidth, d * sp);
313        textUp.setFontSize(36);
314        textUp.setFont("LiberationFonts/LiberationMono-Bold.ttf");
315
316        var textDown = ladderGroup.createChild("text", "pitch-ladder-legend-" ~ d);
317        textDown.setText(d);
318        textDown.setAlignment("right-center");
319        textDown.setTranslation(-tenDegreeWidth, -d * sp);
320        textDown.setFontSize(36);
321        textDown.setFont("LiberationFonts/LiberationMono-Bold.ttf");
322    }
323},
324
325createSpeedTape : func()
326{
327    # background box
328    var box = me.root.rect(0, -ISFD.halfSize, ISFD.speedTapeWidth - 1, ISFD.baseSize);
329    box.setColorFill('#738A7E');
330    box.setTranslation(-256, 0);
331    box.set('z-index', -1);
332
333    # short lines for text
334    # long lines for 'odd' tens
335    # 3 digit monospace text fits exactly between left side and short tick
336    # maximum 5 (6?) visible text pieces
337
338    me._speedTapeGroup = me.root.createChild("group", "speed-tape-group");
339    me._speedTapeGroup.setTranslation(ISFD.hsiLeft, 0);
340    me._speedTapeGroup.set('z-index', 2);
341
342	var tapePath = me._speedTapeGroup.createChild("path", "speed-tape");
343    tapePath.setStrokeLineWidth(2);
344    tapePath.setColor(1, 1, 1);
345
346    var knotSpacing = 4;
347    var tenKnotWidth = 16;
348    var twentyKnotWidth = 8;
349
350    for (var i=0; i<=25; i+=1)
351    {
352        var tenKnotY = ((i * 20) + 10) * knotSpacing;
353        var twentyKnot = ((i+1) * 20);
354
355        tapePath.moveTo(0, -tenKnotY).line(-tenKnotWidth, 0);
356        tapePath.moveTo(0, -twentyKnot * knotSpacing).line(-twentyKnotWidth, 0);
357
358        var text = me._speedTapeGroup.createChild("text", "speed-tape-legend-" ~ twentyKnot);
359        text.setText(twentyKnot);
360        text.setAlignment("right-center");
361        text.setFont("LiberationFonts/LiberationMono-Bold.ttf");
362        text.setFontSize(36);
363        text.setTranslation(-twentyKnotWidth-2, -twentyKnot * knotSpacing);
364    }
365},
366
367updateSpeedTape : func()
368{
369    # special case when speed is close to zero (show 'bottom' only)
370    # translate based on start value
371    # update digits in text
372
373    # given it's a 100kt range, maybe fixed graphic for the tape reduces updates?
374
375    var yOffset = 4 * me._controller.getIndicatedAirspeedKnots();
376    me._speedTapeGroup.setTranslation(ISFD.hsiLeft, yOffset);
377},
378
379createAltitudeTape : func()
380{
381    # tick every 50' interval
382    # text, monospace, 5 digits, ever 100'
383
384    # background box
385    var box = me.root.rect(ISFD.hsiRight + 1, -ISFD.halfSize,
386                           ISFD.altTapeWidth, ISFD.baseSize);
387    box.setColorFill('#738A7E');
388    box.set('z-index', -1);
389
390    me._altTapeGroup = me.root.createChild("group", "altitude-tape-group");
391    me._altTapeGroup.setTranslation(ISFD.hsiRight, 0);
392
393	var tapePath = me._altTapeGroup.createChild("path", "altitude-tape");
394    tapePath.setStrokeLineWidth(2);
395    tapePath.setColor(1, 1, 1);
396
397    var hundredFtSpacing = 64;
398    var twoHundredFtSpacing = hundredFtSpacing * 2;
399    var twoHundredFtWidth = 20;
400
401    # empty array to hold altitude tape texts for easy updating
402    me._altitudeTapeTexts = [];
403
404    for (var i=0; i<=12; i+=1)
405    {
406        var y = (i - 6) * twoHundredFtSpacing;
407        var tickY = y + hundredFtSpacing;
408
409        tapePath.moveTo(0, -tickY).line(twoHundredFtWidth, 0);
410
411        var text = me._altTapeGroup.createChild("text", "altitude-tape-legend-" ~ i);
412        text.setText(text);
413        text.setFontSize(36);
414        text.setAlignment("left-center");
415        text.setFont("LiberationFonts/LiberationMono-Regular.ttf");
416        text.setTranslation(2, -y);
417
418        # we will update the text very often, ensure we only do
419        # real work if it actually changes
420        text.enableUpdate();
421
422    # save for later updating
423        append(me._altitudeTapeTexts, text);
424    }
425},
426
427updateAltitudeTape : func()
428{
429    var altFt = me._controller.getAltitudeFt();
430    var alt200 = int(altFt/200);
431    var altMod200 = altFt - (alt200 * 200);
432
433    var offset = 128 * (altMod200 / 200.0);
434    me._altTapeGroup.setTranslation(ISFD.hsiRight, offset);
435
436    # compute this as current alt - half altitude range
437    var lowestAlt = (alt200 - 6) * 200;
438
439    for (var i=0; i<=12; i+=1)
440    {
441        var alt = lowestAlt + (i * 200);
442        # printf with 5 digits
443        var s = sprintf("%05i", alt);
444        me._altitudeTapeTexts[i].updateText(s);
445    }
446
447    # compute transform on group to put the actual altitude on centre
448},
449
450createCompassRose : func()
451{
452
453
454
455    # clip group for numerals
456    var clipGroup = me.root.createChild("group", "rose-clip-group");
457    clipGroup.set("clip-frame", canvas.Element.LOCAL);
458    clipGroup.set("clip", "rect(176px, 142px, 256px, -164px)");
459    clipGroup.set('z-index', 2);
460
461    var roseBoxHeight = ISFD.modeBoxHeight;
462    var hh = roseBoxHeight / 2;
463
464    # background of the compass
465    var p = clipGroup.createChild('path', 'rose-background');
466    p.moveTo(ISFD.hsiXCenter - ISFD.roseRadius, ISFD.hsiBottom + 12 + ISFD.roseRadius);
467    p.arcSmallCW(ISFD.roseRadius, ISFD.roseRadius, 0,
468                 ISFD.roseRadius * 2, 0);
469    p.close();
470    p.setColorFill('#738A7E');
471
472    # add path for the heading arrow
473    var arrow = me.root.createChild("path", "rose-arrow");
474    arrow.setTranslation(ISFD.hsiXCenter, ISFD.hsiBottom);
475    arrow.setStrokeLineWidth(2);
476    arrow.setColor(1, 1, 1);
477
478    # same size as the roll arrow
479    var arrowHeight = (ISFD.hsiHeight / 2) - ISFD.rollBaseRadius;
480    var arrowHWidth = arrowHeight * (2/3);
481
482    arrow.moveTo(0, arrowHeight);
483    arrow.line(arrowHWidth, -arrowHeight);
484    arrow.line(-arrowHWidth * 2, 0);
485    arrow.close();
486    arrow.set('z-index', 4);
487
488    me._roseGroup = clipGroup.createChild('group', 'rose-group');
489    me._roseGroup.setTranslation(ISFD.hsiXCenter, ISFD.hsiBottom + 12 + ISFD.roseRadius);
490
491    var roseTicks = me._roseGroup.createChild('path', 'rose-ticks');
492    roseTicks.setStrokeLineWidth(2);
493    roseTicks.setColor(1, 1, 1);
494    roseTicks.set('z-index', 2);
495
496    var textR = (ISFD.roseRadius) - 16;
497    for (var i=0; i<36; i+=1) {
498        # create ten degree text
499        # TODO: 30 degree sizes should be bigger
500
501        var text = me._roseGroup.createChild("text", "compass-rose-" ~ i);
502        text.setText(i);
503        text.setFontSize(36);
504        text.setAlignment("center-top");
505        text.setFont("LiberationFonts/LiberationMono-Bold.ttf");
506
507        var horAngle = 90 - (i * 10); # angle from +ve X axis
508        var sa = math.sin(horAngle * D2R);
509        var ca = math.cos(horAngle * D2R);
510        text.setTranslation(ca * textR, -sa * textR);
511        text.setRotation(i * 10 * D2R);
512
513        me.addPolarTick(roseTicks, i * 10, ISFD.roseRadius, ISFD.roseRadius - 8);
514        me.addPolarTick(roseTicks, (i * 10) + 5, ISFD.roseRadius, ISFD.roseRadius - 16);
515    }
516
517    roseTicks.close();
518
519},
520
521createAltitudeBox : func()
522{
523    var halfBoxHeight = ISFD.boxHeight / 2;
524    var box = me.root.rect(ISFD.hsiRight - 10, -halfBoxHeight,
525                           ISFD.altTapeWidth + 10, ISFD.boxHeight);
526    box.setColorFill('#000000');
527    box.setColor('#ffffff');
528    box.setStrokeLineWidth(2);
529    box.set('z-index', 2);
530
531    var clipGroup = me.root.createChild('group', 'altitude-box-clip');
532    clipGroup.set("clip-frame", canvas.Element.LOCAL);
533    clipGroup.set("clip", "rect(-24px," ~ (ISFD.altTapeWidth + 10) ~ "px, 24px, 0px)");
534    clipGroup.setTranslation(ISFD.hsiRight - 10, 0);
535    clipGroup.set('z-index', 3);
536
537    var text = clipGroup.createChild('text', 'altitude-box-text');
538    me._altitudeBoxText = text;
539
540    text.setText('88 ');
541    text.setFontSize(44);
542    text.setAlignment("left-center");
543    text.setFont("LiberationFonts/LiberationMono-Regular.ttf");
544
545    me._altitudeDigits00 = me.createDigitTape(clipGroup, 'altitude-digits00', '0');
546    me._altitudeDigits00.setFontSize(32);
547    me._altitudeDigits00.set('z-index', 4);
548},
549
550createSpeedBox : func()
551{
552    var halfBoxHeight = ISFD.boxHeight / 2;
553    var box = me.root.rect(-ISFD.halfSize - 2, -halfBoxHeight,
554                           ISFD.speedTapeWidth + 4, ISFD.boxHeight);
555    box.setColorFill('#000000');
556    box.setColor('#ffffff');
557    box.setStrokeLineWidth(2);
558    box.set('z-index', 2);
559
560    var clipGroup = me.root.createChild('group', 'speed-box-clip');
561    clipGroup.set("clip-frame", canvas.Element.LOCAL);
562    clipGroup.set("clip", "rect(-24px," ~ (ISFD.speedTapeWidth + 4) ~ "px, 24px, 0px)");
563    clipGroup.setTranslation(-ISFD.halfSize, 0);
564    clipGroup.set('z-index', 3);
565
566    var text = clipGroup.createChild('text', 'speed-box-text');
567    me._speedBoxText = text;
568
569    text.setText('88 ');
570    text.setFontSize(44);
571    text.setAlignment("left-center");
572    text.setFont("LiberationFonts/LiberationMono-Regular.ttf");
573
574    me._speedDigit0 = me.createDigitTape(clipGroup, 'speed-digit0');
575    me._speedDigit0.set('z-index', 4);
576},
577
578createAltimeterSetting: func()
579{
580    me._altimeterText = me.root.createChild('text', 'altimeter-setting-text');
581    me._altimeterText.setText('1013');
582    me._altimeterText.setAlignment("right-center");
583    me._altimeterText.setFont("LiberationFonts/LiberationMono-Regular.ttf");
584    me._altimeterText.setColor('#00ff00');
585
586    var midTextY = -ISFD.halfSize + (ISFD.modeBoxHeight * 0.5);
587    me._altimeterText.setTranslation(ISFD.hsiRight - 2, midTextY);
588},
589
590createModeText : func()
591{
592    me._modeText = me.root.createChild('text', 'mode-text');
593    me._modeText.setFontSize(44);
594    me._modeText.setText('APP');
595    me._modeText.setAlignment("left-center");
596    me._modeText.setFont("LiberationFonts/LiberationMono-Regular.ttf");
597    me._modeText.setColor('#00ff00');
598
599    var midTextY = -ISFD.halfSize + (ISFD.modeBoxHeight * 0.5);
600    me._modeText.setTranslation(ISFD.hsiLeft + 2, midTextY);
601},
602
603updateApproachMode: func
604{
605    if (me._appMode != 0) {
606        me._modeText.setText('APP');
607    }
608
609    me._modeText.setVisible(me._appMode);
610    me._localizerGroup.setVisible(me._appMode);
611    me._glideslopeGroup.setVisible(me._appMode);
612},
613
614createLocalizer: func
615{
616    var hw = ISFD.needleBoxSize * 0.5;
617    var g = me.root.createChild('group', 'localizer-group');
618    g.setTranslation(ISFD.hsiXCenter, 156);
619    me._localizerGroup = g;
620
621    var bkg = g.rect(-hw, 0, ISFD.needleBoxSize, ISFD.needleBoxHeight);
622    bkg.setColorFill('#000000');
623
624    # markers: white line and hollow dots
625    var m = g.createChild("path", "localizer-marker");
626    m.setStrokeLineWidth(2);
627    m.setColor(1, 1, 1);
628    m.moveTo(0, 0);
629    m.line(0, ISFD.needleBoxHeight);
630    m.close();
631
632    var r =( ISFD.needleBoxHeight - 8) * 0.5;
633    var hh = ISFD.needleBoxHeight * 0.5;
634
635    # four dots
636    for (var i = -2; i <= 2; i +=1) {
637        if (i == 0) continue;
638        var x = i * 50;
639        m.moveTo(x - r, hh);
640        m.arcSmallCW(r, r, 0, r * 2, 0);
641        m.arcSmallCW(r, r, 0, -r * 2, 0);
642        m.close();
643    }
644
645    me._localizerPointer = g.createChild("path", "localizer-pointer");
646    me._localizerPointer.moveTo(0, hh);
647    me.addDiamond(me._localizerPointer , hh);
648    me._localizerPointer.setColorFill('#ff00ff'); # magenta!
649},
650
651createGlideslope: func
652{
653    var g = me.root.createChild('group', 'glideslope-bar');
654    me._glideslopeGroup = g;
655
656    var hh = ISFD.needleBoxSize * 0.5;
657    var hw = ISFD.needleBoxHeight * 0.5;
658
659    g.setTranslation(ISFD.hsiRight - 36, 0);
660    me._localizerGroup = g;
661
662    var bkg = g.rect(0, -hh, ISFD.needleBoxHeight, ISFD.needleBoxSize);
663    bkg.setColorFill('#000000');
664
665    # markers: white line and hollow dots
666    var m = g.createChild("path", "gs-marker");
667    m.setStrokeLineWidth(2);
668    m.setColor(1, 1, 1);
669    m.moveTo(0, 0);
670    m.line(ISFD.needleBoxHeight, 0);
671    m.close();
672
673    var r =( ISFD.needleBoxHeight - 8) * 0.5;
674
675    # four dots
676    for (var i = -2; i <= 2; i +=1) {
677        if (i == 0) continue;
678        var y = i * 50;
679        m.moveTo(hw, y - r);
680        m.arcSmallCW(r, r, 0, 0, r * 2);
681        m.arcSmallCW(r, r, 0, 0, -r * 2);
682        m.close();
683    }
684
685    me._gsPointer = g.createChild("path", "gs-pointer");
686    me._gsPointer.moveTo(hw, 0);
687    me.addDiamond(me._gsPointer , hw);
688    me._gsPointer.setColorFill('#ff00ff'); # magenta!
689},
690
691updateILS: func
692{
693    var hsz = ISFD.needleBoxSize * 0.5;
694
695    me._localizerPointer.setVisible(me._controller.isLocalizerValid());
696    var locDev = me._controller.getLocalizerDeviationNorm() * hsz;
697    me._localizerPointer.setTranslation(locDev, 0);
698
699    me._gsPointer.setVisible(me._controller.isGSValid());
700    var gsDev = me._controller.getGSDeviationNorm() * hsz;
701    me._gsPointer.setTranslation(0, gsDev);
702},
703
704pressButtonAPP : func
705{
706    me._controller.toggleApproachMode();
707},
708
709update : func()
710{
711    # read LOC/GS deviation
712    # read Mach for some options
713    me._controller.update();
714
715# pitch and roll
716    var roll = me._controller.getBankAngleDeg() * D2R;
717    var pitch = me._controller.getPitchDeg();
718    me.pitchGroup.setTranslation(ISFD.hsiXCenter, pitch * me.pitchLadderDegreeSpacing);
719    me._pitchRotation.setRotation(-roll);
720    me._rollGroup.setRotation(-roll);
721
722# heading
723    me._roseGroup.setRotation(-me._controller.getHeadingDeg() * D2R);
724
725    me.updateAltitudeTape();
726    me.updateSpeedTape();
727
728# speed box
729    var spd = me._controller.getIndicatedAirspeedKnots();
730    var spdDigit0 = math.mod(spd, 10);
731    me._speedDigit0.setTranslation(53, 16 + spdDigit0 * 44);
732    var s = sprintf("%02i ", math.floor(spd / 10));
733    me._speedBoxText.setText(s);
734
735# altitude box
736    var alt = me._controller.getAltitudeFt();
737    var altDigits00 = math.mod(alt / 10, 10);
738    me._altitudeDigits00.setTranslation(80, 16 + altDigits00 * 32);
739    var s = sprintf("%03i ", math.floor(alt / 100));
740    me._altitudeBoxText.setText(s);
741
742# barometric
743    if (me._controller.isSTDBarometricPressure()) {
744        me._altimeterText.setFontSize(44);
745        me._altimeterText.setText('STD');
746    } else {
747        var s = '';
748        if (me._controller.isHPaBarometer()) {
749            s = sprintf('%4d HPA', me._controller.getBarometricPressureSettingHPa());
750        } else {
751            s = sprintf('%4.2f IN', me._controller.getBarometricPressureSettingInHg());
752        }
753
754        # smaller text to fit
755        me._altimeterText.setFontSize(32);
756        me._altimeterText.setText(s);
757    }
758
759# APProach mode
760    if (me._appMode != me._controller.isApproachMode()) {
761        me._appMode = me._controller.isApproachMode;
762        me.updateApproachMode();
763    }
764
765    if (me._appMode != 0) {
766        me.updateILS();
767    }
768}
769
770};
771