|
From: <fli...@li...> - 2017-10-25 21:18:25
|
Revision: 3060
http://sourceforge.net/p/flightgear/fgaddon/3060
Author: rleibner
Date: 2017-10-25 21:18:22 +0000 (Wed, 25 Oct 2017)
Log Message:
-----------
minor changes\n
Added Paths:
-----------
trunk/Addons/SpokenGCA/
trunk/Addons/SpokenGCA/README.first
trunk/Addons/SpokenGCA/config.xml
trunk/Addons/SpokenGCA/control.nas
trunk/Addons/SpokenGCA/gca_class.nas
trunk/Addons/SpokenGCA/gca_gui.nas
trunk/Addons/SpokenGCA/main.nas
trunk/Addons/SpokenGCA/parscreen_class.nas
trunk/Addons/SpokenGCA/phraseology.xml
trunk/Addons/SpokenGCA/tools.nas
Added: trunk/Addons/SpokenGCA/README.first
===================================================================
--- trunk/Addons/SpokenGCA/README.first (rev 0)
+++ trunk/Addons/SpokenGCA/README.first 2017-10-25 21:18:22 UTC (rev 3060)
@@ -0,0 +1,59 @@
+ Spoken GCA
+ v. 0.3
+********************
+ See http://wiki.flightgear.org/Howto:Implementing_a_simple_GCA_system
+ See http://http://wiki.flightgear.org/Spoken_GCA
+
+This nasal code is not intended to compete with FGComm, Festival or other similar facilities.
+On the contrary, their use is indicated when they are not available (eg, few hardware resources, no Internet connection, etc.)
+
+- How to install (See http://http://wiki.flightgear.org/Spoken_GCA)
+-----------------
+ * Unzip all (keeping the directory structure !) into the $FG_HOME/Nasal directory .
+ * Edit the $FG_ROOT/keyboard.xml file and include the following lines in order to bind
+ the ">" key:
+
+ <key n="62">
+ <name>grater-than</name>
+ <desc>Spoken GCA</desc>
+ <binding>
+ <command>nasal</command>
+ <script><![CDATA[
+ gca.Control();
+ ]]>
+ </script>
+ </binding>
+ </key>
+
+And that's all !
+If you prefer (as I do), you can bind an unused joystick button too.
+
+- How to use it (See http://http://wiki.flightgear.org/Spoken_GCA)
+-----------------
+Once installed, launch FlighGear (or re-start it), tune Comm1 to the desired frequency,
+ and press the ">" key to request the GCA service.
+You will hear the GCA's answer:
+"<callsign>, this will be a P A R approach to <airport> Runway <rwy>."
+
+As an extra aid, a new "PAR screen" window will be opened. Keep it or close it, as you like.
+
+Controller's voice will instruct you to steer your plane towards the approach course.
+Follow his instructions and have a safe landing !
+
+- TODO list
+---------------------
+* In gca_class.nas: add validation of all setters arguments.
+* Move join() function and phraseology.xml to be accesible for gca_class.nas.
+* Add a version control.
+* and a lot of optimizations and improvements ( I mean A LOT !)
+
+
+Awaiting for your comments,
+all feedback is wellcome.
+Enjoy it !
+
+Regards,
+Rodolfo - rle...@gm...
+
+
+
Added: trunk/Addons/SpokenGCA/config.xml
===================================================================
--- trunk/Addons/SpokenGCA/config.xml (rev 0)
+++ trunk/Addons/SpokenGCA/config.xml 2017-10-25 21:18:22 UTC (rev 3060)
@@ -0,0 +1,225 @@
+<?xml version="1.0"?>
+
+<PropertyList>
+ <input>
+ <keyboard>
+ <key n="62">
+ <name>greater-than</name>
+ <desc>Gnd Controlled App</desc>
+ <binding>
+ <command>nasal</command>
+ <script><![CDATA[gca.Control();]]></script>
+ </binding>
+ </key>
+ </keyboard>
+ </input>
+ <gca>
+ <version type="double">0.1</version>
+ <tick type="double">1</tick>
+ <near type="bool">0</near>
+ <controlled type="bool">0</controlled>
+ <par type="bool">0</par>
+ <prev-msg-type type="string"></prev-msg-type>
+ <prev-apt-name type="string"></prev-apt-name>
+ <prev-phrase type="string"></prev-phrase>
+ <logic>
+ <!-- Relative Position: -->
+<!-- The order of the conditions does matter!! -->
+ <condition><!-- 0. entering left base -->
+ <equals>
+ <property>/gca/near</property>
+ <value>0</value>
+ </equals>
+ <equals>
+ <property>/gca/controlled</property>
+ <value>1</value>
+ </equals>
+ <less-than>
+ <property>/gca/gate-delta</property>
+ <value>0</value>
+ </less-than>
+ <less-than>
+ <property>/gca/dist-to-app-crse</property>
+ <value>1.5</value>
+ </less-than>
+ </condition>
+ <condition><!-- 1. entering right base -->
+ <equals>
+ <property>/gca/near</property>
+ <value>0</value>
+ </equals>
+ <equals>
+ <property>/gca/controlled</property>
+ <value>1</value>
+ </equals>
+ <less-than>
+ <property>/gca/dist-to-app-crse</property>
+ <value>1.5</value>
+ </less-than>
+ </condition>
+
+ <condition><!-- 2. oncourse -->
+ <!-- <greater-than>
+ <property>/position/altitude-agl-ft</property>
+ <value>20</value>
+ </greater-than> -->
+ <or>
+ <greater-than>
+ <property>/gca/rwy-delta</property>
+ <value>176</value>
+ </greater-than>
+ <less-than>
+ <property>/gca/rwy-delta</property>
+ <value>-176</value>
+ </less-than>
+ <equals>
+ <property>/gca/controlled</property>
+ <value>1</value>
+ </equals>
+ </or>
+ <less-than>
+ <property>/gca/dist-to-app-crse</property>
+ <value>0.08</value>
+ </less-than>
+ </condition>
+ <condition><!-- 3.- slightly left side-->
+ <equals>
+ <property>/gca/controlled</property>
+ <value>1</value>
+ </equals>
+ <less-than>
+ <property>/gca/rwy-delta</property>
+ <value>-173</value>
+ </less-than>
+ <less-than>
+ <property>/gca/dist-to-app-crse</property>
+ <value>0.25</value>
+ </less-than>
+ </condition>
+ <condition><!-- 4.- well left side-->
+ <equals>
+ <property>/gca/controlled</property>
+ <value>1</value>
+ </equals>
+ <less-than>
+ <property>/gca/gate-delta</property>
+ <value>0</value>
+ </less-than>
+ <less-than>
+ <property>/gca/dist-to-app-crse</property>
+ <value>1</value>
+ </less-than>
+ </condition>
+ <condition><!-- 5.- left side-->
+ <greater-than>
+ <property>/position/altitude-agl-ft</property>
+ <value>20</value>
+ </greater-than>
+ <equals>
+ <property>/gca/controlled</property>
+ <value>1</value>
+ </equals>
+ <less-than>
+ <property>/gca/gate-delta</property>
+ <value>0</value>
+ </less-than>
+ </condition>
+ <condition><!-- 6. left opposite hdng ok -->
+ <equals>
+ <property>/gca/controlled</property>
+ <value>0</value>
+ </equals>
+ <equals>
+ <property>/gca/heading-ok</property>
+ <value>1</value>
+ </equals>
+ <greater-than>
+ <property>/gca/gate-delta</property>
+ <value>-90</value>
+ </greater-than>
+ <less-than>
+ <property>/gca/gate-delta</property>
+ <value>0</value>
+ </less-than>
+ </condition>
+ <condition><!-- 7. left opposite side -->
+ <equals>
+ <property>/gca/controlled</property>
+ <value>0</value>
+ </equals>
+ <greater-than>
+ <property>/gca/gate-delta</property>
+ <value>-90</value>
+ </greater-than>
+ <less-than>
+ <property>/gca/gate-delta</property>
+ <value>0</value>
+ </less-than>
+ </condition>
+ <condition><!-- 8. right opposite hdng ok -->
+ <equals>
+ <property>/gca/controlled</property>
+ <value>0</value>
+ </equals>
+ <equals>
+ <property>/gca/heading-ok</property>
+ <value>1</value>
+ </equals>
+ <greater-than>
+ <property>/gca/gate-delta</property>
+ <value>0</value>
+ </greater-than>
+ <less-than>
+ <property>/gca/gate-delta</property>
+ <value>90</value>
+ </less-than>
+ </condition>
+ <condition><!-- 9. right opposite turn -->
+ <equals>
+ <property>/gca/controlled</property>
+ <value>0</value>
+ </equals>
+ <greater-than>
+ <property>/gca/gate-delta</property>
+ <value>0</value>
+ </greater-than>
+ <less-than>
+ <property>/gca/gate-delta</property>
+ <value>90</value>
+ </less-than>
+ </condition>
+ <condition><!-- 10. right side -->
+ <greater-than>
+ <property>/position/altitude-agl-ft</property>
+ <value>20</value>
+ </greater-than>
+ <greater-than>
+ <property>/gca/dist-to-app-crse</property>
+ <value>1</value>
+ </greater-than>
+ </condition>
+ <condition><!-- 11. slightly right side -->
+ <greater-than>
+ <property>/gca/rwy-delta</property>
+ <value>173</value>
+ </greater-than>
+ <less-than>
+ <property>/gca/dist-to-app-crse</property>
+ <value>0.25</value>
+ </less-than>
+ </condition>
+ <condition><!-- 12. well right side -->
+ <less-than>
+ <property>/gca/dist-to-app-crse</property>
+ <value>1</value>
+ </less-than>
+ </condition>
+ <condition><!-- 13. unknown -->
+ <equals>
+ <property>/gca/dist-to-app-crse</property>
+ <property>/gca/dist-to-app-crse</property>
+ </equals>
+ </condition>
+ </logic>
+ </gca>
+</PropertyList>
Added: trunk/Addons/SpokenGCA/control.nas
===================================================================
--- trunk/Addons/SpokenGCA/control.nas (rev 0)
+++ trunk/Addons/SpokenGCA/control.nas 2017-10-25 21:18:22 UTC (rev 3060)
@@ -0,0 +1,128 @@
+print("GCA control 0.3 loaded");
+var demo = nil;
+var Par = nil;
+var window = nil;
+var bye = 0;
+var prev_phrase = '';
+
+var Control=func() {
+if(getprop("/gca/callsign-fmt") ==nil) setprop("/gca/callsign-fmt",'');
+
+
+if( isa(demo, GCAController) ){
+ demo = nil; # Abort, end GCA service
+ var abort = join("abort");
+ setprop("/sim/sound/voices/atc", abort);
+ write(abort);
+ cleanAll();
+ return;
+ }
+
+# Choose destination from comm freq
+var icao = getprop("/instrumentation/comm/airport-id");
+var info = airportinfo(icao);
+if(getprop("/instrumentation/comm/volume")<0.1) {
+ gui.popupTip("Turn Comm1 on. Set volume",3);
+ return ;
+} elsif(info.name==nil or getprop("/instrumentation/comm/signal-quality-norm")<0.01) {
+ # if invalid freq or out of range
+ gui.popupTip("Check comm freq.!",3);
+ return ;}
+var aux1 = getprop("/instrumentation/comm/station-name");
+var aux =(string.match(aux1,"* *"))? capit(aux1) : string.replace(info.name,"Intl","International") ;
+setprop("/gca/station-name", aux);
+
+# Choose best rwy
+var best = chooseRwy(info.runways);
+#~ print("choosed rwy:",best);
+#~ setprop("/gca/rwy-in-use", spell(best, 0));
+
+# Instance GCAController
+demo = gca.GCAController.new( );
+demo.setAirport(icao);
+demo.setRunway(best);
+demo.setFinalApproach(10);
+
+prev_phrase = '';
+
+# 5) Check current position
+var rwy = info.runways[best];
+var elev = geo.elevation(demo.destination.rwy_object.lat
+ , demo.destination.rwy_object.lon); # (m)
+
+var tick =nil;
+
+# this callback will be invoked by the GCA controller when it has a new instruction
+var receiver = func(instruction) {
+ #~ var t2 = systime(); # record time
+ if(demo==nil) return "bye";
+ tick = tick==nil ? 0: tick+1;
+ var (crse, dist) = courseAndDistance(demo.destination.rwy_object); # to rwy
+ var delta = demo.destination.rwy_object.heading - demo.rwyCrse;
+ # autoZoom:
+ if(dist<7 and abs(dist*math.sin(delta*D2R))<1.3 and Par.zoom >7) Par.setZoom(7,3000);
+ if (Par!=nil) Par.appendTrack(dist, delta,elev*M2FT);
+
+ if(demo.phrase=="oncourse"
+ and dist*NM2M < 100
+ and (prev_phrase=="oncourse" or left(prev_phrase,6)=="slight")) {
+ instruction = join("bye");
+ demo.phrase = "bye";
+ bye = 1;
+ }
+
+ if(tick==demo.maxsecs or demo.phrase!=prev_phrase ){
+ setprop("/sim/sound/voices/atc", instruction);
+ tick = 0;
+ instruction = delayed(instruction);
+ write(instruction);
+ prev_phrase = demo.phrase;
+ }
+ if(bye) {
+ demo = nil;
+ cleanAll();
+ }
+ #~ var t3 = systime(); # record new time
+ #~ print("receiver() took ", (t3 - t2)*1000, " ms"); # print result
+} # receiver func
+
+if(receiver=="bye") return; # quit Control func
+demo.registerReceiver( receiver );
+
+# calling UI
+demo.openDialog();
+
+} # Control func
+
+var delayed = func(instruction){
+ var appnd = "";
+if((left(demo.phrase,4) =="left" or left(demo.phrase,5) =="right") and string.match(prev_phrase,"to?bas*")){
+ appnd = join("dont");
+ settimer(func(){if(demo!=nil) setprop("/sim/sound/voices/atc", appnd)},12);
+ }
+if(left(demo.phrase,6)=="turn90") {
+ appnd = join("glide");
+ settimer(func(){if(demo!=nil) setprop("/sim/sound/voices/atc", appnd)},40);
+ settimer(func(){if(demo!=nil) setprop("/gca/near", 1)},60);
+ }
+
+if((demo.phrase =="oncourse" or left(demo.phrase,6) =="slight") and demo.rwyDist <=demo.destination.final) {
+ appnd = join("pathalt");
+ settimer(func(){if(demo!=nil) setprop("/sim/sound/voices/atc", appnd);},1);
+ }
+ return instruction ~ appnd;
+} # delayed
+
+var cleanAll = func(){
+ bye = 0;
+ prev_phrase = '';
+ setprop("/gca/near",0);
+ setprop("/gca/controlled",0);
+ setprop("/gca/prev-msg",'');
+ setprop("/gca/prev-apt-name",'');
+} # cleanAll
+
+var write = func(str){
+ if(window==nil) window = screen.window.new(nil,-50,10,8);
+ window.write(str);
+} # write
Added: trunk/Addons/SpokenGCA/gca_class.nas
===================================================================
--- trunk/Addons/SpokenGCA/gca_class.nas (rev 0)
+++ trunk/Addons/SpokenGCA/gca_class.nas 2017-10-25 21:18:22 UTC (rev 3060)
@@ -0,0 +1,320 @@
+# GCAController class
+#
+# members:
+# .aircraft_state.altitude_ft
+# .aircraft_state.latitude_deg
+# .aircraft_state.longitude_deg
+# .aircraft_state.heading_magnetic_deg
+# .aircraft_object as geo.Coord
+# .destination.airport
+# .destination.runway
+# .destination.elevation
+# .destination.glidepath
+# .destination.safety_slope
+# .destination.decision_height
+# .destination.offset
+# .destination.rwy_obj
+# .destination.final
+# .timer
+# .phrase
+# .maxsecs
+# .callsign
+# .gateAlt
+# .rwyDist
+# .rwyCrse
+#
+# methods:
+# .stop()
+# .del()
+# .setPosition(node)
+# .setAirport(apt)
+# .setRunway(rwy)
+# .setFinalApproach(nm)
+# .setGlidepath(slope)
+# .setSafetySlope(slope)
+# .setDecisionHeight(ft)
+# .setTouchdownOffset(ft)
+# .setTransmissionChannel(node)
+# .restart(seconds)
+# .setAircraft(root)
+# .computeRequiredHeading()
+# .computeRequiredAltitude()
+
+
+
+var GCAController = {
+# constructor
+new: func() {
+ var m = {parents:[GCAController] };
+ m.aircraft_properties = {altitude_ft: "altitude-ft", latitude_deg: "latitude-deg", longitude_deg: "longitude-deg"};
+ m.aircraft_state = {latitude_deg:0.00, longitude_deg:0.00, altitude_ft:0.00, heading_magnetic_deg:0.00 };
+ m.aircraft_object = geo.Coord.new();
+ m.destination = {airport:'', runway:'', elevation:0.00, glidepath:0.00,
+ safety_slope:0.00, decision_height:0.00, offset:0.00,
+ rwy_object:'', final:0.00 };
+ m.receivers = []; # callbacks to receive instructions from the GCA controller
+ m.timer = maketimer(5, func m.update() );
+ m.timer.simulatedTime = 1;
+ m.phrase = '';
+ return m;
+ }, # new()
+
+# destructor (no really)
+del: func() {
+me.timer.stop();
+}, # del()
+
+##### Setters:
+#~ TODO: validate setters arguments
+
+
+setPosition: func(root) {
+me.root = root;
+# AI/MP models have their own callsign node:
+var callsign_node = props.getNode(root).getNode("callsign");
+
+# the main aircraft's callsign is stored under /sim/multiplay/callsign
+if (callsign_node == nil) {
+print("Using multiplayer callsign");
+callsign_node = props.getNode("/sim/multiplay/callsign");
+}
+else {
+print("Using AI/MP callsign");
+}
+
+me.callsign = callsign_node.getValue();
+setprop("/gca/callsign-fmt", alpha(me.callsign));
+}, # setPosition()
+
+setTransmissionInterval: func(secs) {
+me.phrase = 'prolog';
+me.maxsecs = '5';
+me.notifyReceivers(join("this"));
+me.timer.restart(num(secs));
+},
+
+setAirport: func(airport) {
+me.destination.airport = airport;
+setprop("/gca/apt-name", airportinfo(airport).name);
+me.destination.elevation = airportinfo(airport).elevation; # (m)
+ if(myDbg) printf("setAirport(%s)", airport);
+},
+
+setRunway: func(rwy) {
+me.destination.runway = rwy;
+me.destination.rwy_object = airportinfo(me.destination.airport).runways[rwy];
+setprop("/gca/rwy-in-use", spell(rwy, 0));
+me.touch = geo.Coord.new().set_latlon(me.destination.rwy_object.lat, me.destination.rwy_object.lon)
+ .apply_course_distance(me.destination.rwy_object.heading, me.destination.rwy_object.threshold);
+var latlon = me.touch.latlon();
+me.touch.set_alt(1+geo.elevation(latlon[0],latlon[1]));
+ if(myDbg) printf("setRunway(%s) ",rwy);
+ },
+
+setFinalApproach: func(final) {
+me.destination.final = final;
+ if(myDbg) printf("setFinalApproach(%i nm)", final);
+},
+
+setGlidepath: func(slope) {
+me.destination.glidepath = slope;
+var gateAlt = me.destination.elevation*M2FT+ me.destination.final *math.tan( me.destination.glidepath*D2R)*NM2M *M2FT;
+me.geoGate = geo.Coord.new().set_latlon(me.destination.rwy_object.lat
+ , me.destination.rwy_object.lon)
+ .apply_course_distance(me.destination.rwy_object.heading + 180
+ , me.destination.final*NM2M-me.destination.rwy_object.threshold)
+ .set_alt(gateAlt*FT2M);
+setprop("/gca/appgate-alt-fmt", sprintf("%i",math.round(gateAlt,100)));
+ if(myDbg) printf("setGlidepath(%i deg), gateAlt=%i ft.", slope,gateAlt);
+},
+
+setSafetySlope: func(slope) {
+#~ me.destination.safety_slope = slope;
+},
+
+setTerrainResolution: func(nm) {
+me.TerrainResolution = nm;
+},
+
+setDecisionHeight: func(height) {
+me.destination.decision_height = height;
+},
+
+setTouchdownOffset: func(offset) {
+me.destination.offset = offset;
+},
+
+setTransmissionChannel: func(node) {
+me.TransmissionChannel = node;
+},
+
+setVertGrid: func(ft) {
+me.VertGrid = ft;
+},
+
+setHzGrid: func(nm) {
+me.HzGrid = nm;
+},
+
+restart: func(s) {
+me.timer.restart(num(s));
+}, # restart()
+
+# stop/interrupt the GCA
+stop: func() {
+ me.timer.stop();
+}, # stop()
+
+##### end of Setters ################
+
+
+########################################
+## Helpers:
+###
+
+#####
+# this will be called by our timer
+update: func() {
+ #~ var t0 = systime(); # record time
+ me.updatePosition();
+ me.updateStage();
+ me.computeRequiredHeading();
+ me.computeRequiredAltitude();
+
+ var instruction = me.buildInstruction();
+ # now that we have an instruction, pass it to registered callbacks
+ me.notifyReceivers(instruction);
+ #~ var t1 = systime(); # record new time
+ #~ print("Controller_class() took ", (t1 - t0)*1000, " ms"); # print result
+}, # update()
+
+
+updatePosition: func() {
+foreach(var p; keys(me.aircraft_properties)) me.aircraft_state[p] = getprop(me.root ~'/'~ me.aircraft_properties[p]);
+me.aircraft_state["heading_magnetic_deg"] = getprop('/orientation/heading-magnetic-deg');
+}, # updatePosition()
+
+updateStage: func() {
+ var rwy = me.destination.rwy_object;
+ #~ print("rwy hdg:",rwy.heading);
+ var (crse, dist) = courseAndDistance(me.touch); # to rwy touchdown
+ var (rwyCrse, rwyDist) = courseAndDistance(rwy); # to rwy
+ #~ print(194," rwyCrse=",rwyCrse);
+ me.rwyDist = rwyDist;
+ me.rwyCrse = rwyCrse;
+ var (gateCrse, gateDist) = courseAndDistance(me.geoGate); # to App gate
+ #~ print(199," gateCrse=",gateCrse);
+ var geoLbase = geo.Coord.new().set_latlon(rwy.lat, rwy.lon)
+ .apply_course_distance(rwy.heading + 180, 1+me.destination.final*NM2M-rwy.threshold)
+ .apply_course_distance(rwy.heading - 90, 3*NM2M);
+ var (lBaseCrse, lBaseDist) = courseAndDistance(geoLbase); # to left base
+ var geoRbase = geo.Coord.new().set_latlon(rwy.lat, rwy.lon)
+ .apply_course_distance(rwy.heading + 180, 1+me.destination.final*NM2M-rwy.threshold)
+ .apply_course_distance(rwy.heading + 90, 3*NM2M);
+ var (rBaseCrse, rBaseDist) = courseAndDistance(geoRbase); # to right base
+
+ # - props
+ var hand =(geo.normdeg(lBaseCrse-me.aircraft_state.heading_magnetic_deg)<180)? "right " : "left ";
+ setprop("/gca/lbase-hand", hand);
+ hand =(geo.normdeg(rBaseCrse-me.aircraft_state.heading_magnetic_deg)<180)? "right " : "left ";
+ setprop("/gca/rbase-hand", hand);
+ var aux = abs(gateDist *math.sin(D2R*(gateCrse-rwy.heading-180)));
+ setprop("/gca/callsign-fmt", alpha(getprop("/sim/multiplay/callsign")) );
+ setprop("/gca/dist-to-rwy", sprintf("%.1f",me.rwyDist));
+ setprop("/gca/gate-delta", geo.normdeg180(180+gateCrse-rwy.heading));
+ setprop("/gca/rwy-delta", geo.normdeg180(180+rwyCrse-rwy.heading));
+ setprop("/gca/dist-to-app-crse", aux);
+ setprop("/gca/dist-to-app-crse-fmt", sprintf("%.1f",aux));
+ var vector = aux>2 ? gateCrse : (rwy.heading-90)*math.sgn(geo.normdeg180(rwy.heading-rwyCrse)) ;
+ vector = geo.normdeg(vector);
+ hand =(geo.normdeg(vector-me.aircraft_state.heading_magnetic_deg)<180)? "right " : "left ";
+ setprop("/gca/turn-hand", hand);
+ setprop("/gca/vector", spell(sprintf("%03.0f",vector),3));
+ setprop("/gca/course-to-rwy", spell(sprintf("%03.0f",rwyCrse),3));
+ setprop("/gca/course-to-lbase", spell(sprintf("%03.0f",lBaseCrse),3));
+ setprop("/gca/course-to-rbase", spell(sprintf("%03.0f",rBaseCrse),3));
+ if(abs(geo.normdeg180(lBaseCrse-me.aircraft_state.heading_magnetic_deg))<4
+ or abs(geo.normdeg180(rBaseCrse-me.aircraft_state.heading_magnetic_deg))<4){
+ setprop("/gca/heading-ok", 1);
+ } else {
+ setprop("/gca/heading-ok", 0);
+ }
+
+ if(getprop("/gca/prev-apt-name")!=airportinfo(me.destination.airport).name )
+ setprop("/gca/prev-msg-type", "");
+
+},# updateStage()
+
+computeRequiredHeading: func() {
+ # phrase structure is: [ phrase-type , max-secs ]
+ var phrases= [["turn90l",60],["turn90r",60],["oncourse",10],["slightleft",8],["wellleft",10]
+ ,["left",10],["tolbaseok",30],["tolbase",20],["torbaseok",30]
+ ,["torbase",20],["right",10],["slightright",8],["wellright",10],["none",8]];
+ forindex(var i; phrases) {
+ #~ print(257," i=",i);
+ if(props.condition(sprintf("/gca/logic/condition[%i]",i))) break;
+ }
+ if(i<6 or i>9) setprop("/gca/controlled",1); # if yet on CTR
+ if((i>1 and i<5) or i==11 or i==12) setprop("/gca/near",1); # if near
+ setprop("/gca/prev-msg-type", me.phrase);
+ me.phrase = phrases[i][0];
+ me.maxsecs = phrases[i][1] ;
+}, # computeRequiredHeading()
+
+computeRequiredAltitude: func() {
+ var condition = me.phrase!="oncourse" and left(me.phrase,3)!="well" and left(me.phrase,6)!="slight";
+ var dif = me.aircraft_state.altitude_ft - me.geoGate.alt()*M2FT;
+ setprop("/gca/climb", dif>200 and condition ? "Descend " : dif<-200 and condition ? "Climb " : "");
+
+ var difSlope = (me.aircraft_state.altitude_ft*FT2M-me.destination.elevation)
+ / me.rwyDist /NM2M - math.tan(me.destination.glidepath*D2R);
+ setprop("/gca/over-glidepath", difSlope>0.012 ? "Above " : difSlope<-0.005 ? "Below " : "On ");
+}, # computeRequiredAltitude()
+
+
+buildInstruction: func() {
+ var sources = [];
+ var destinations = ["/gca/value[0]","/gca/value[1]","/gca/value[2]"];
+ if(me.phrase=="oncourse" or left(me.phrase,3)=="well" ) {
+ append(sources,"/gca/course-to-rwy","/gca/dist-to-rwy");
+ } elsif(me.phrase=="left" or me.phrase=="right") {
+ append(sources,"/gca/turn-hand","/gca/vector","/gca/dist-to-app-crse-fmt" );
+ } elsif(left(me.phrase,7)=="tolbase") {
+ append(sources,"/gca/lbase-hand","/gca/course-to-lbase" );
+ } elsif(left(me.phrase,7)=="torbase") {
+ append(sources,"/gca/rbase-hand","/gca/course-to-rbase" );
+ } elsif(left(me.phrase,6)=="turn90") {
+ append(sources,"/gca/course-to-rwy" );
+ }
+ for(var i=0; i<size(sources); i+=1)
+ setprop(destinations[i],getprop(sources[i]));
+
+ if(getprop("/gca/climb") !='') return join(me.phrase) ~join("climb");
+
+ return join(me.phrase);
+}, # buildInstruction
+
+registerReceiver: func(receiver) {
+append(me.receivers, receiver);
+}, #registerReceiver()
+
+
+notifyReceivers: func(instruction) {
+foreach(var r; me.receivers) {
+ r(instruction);
+ }
+},
+
+openDialog:func() {
+fgcommand("pause");
+ var min = minSlope(me.touch, me.destination.rwy_object.heading+180, me.destination.final *NM2M);
+ if(myDbg) printf("SafeSlop(%.2f)",min);
+ var defValues = {icao:me.destination.airport, rwy:me.destination.runway
+ , safety_slope:min
+ , channel:'/sim/sound/voices/atc', interval:1.00};
+ var mask = {"Safety Slope":'',"Decision Height":'',"Position root":'' };
+ configureGCA(demo, defValues, mask);
+ #~ configureGCA(demo, defValues);
+},
+}; # end of GCAController class
+
Added: trunk/Addons/SpokenGCA/gca_gui.nas
===================================================================
--- trunk/Addons/SpokenGCA/gca_gui.nas (rev 0)
+++ trunk/Addons/SpokenGCA/gca_gui.nas 2017-10-25 21:18:22 UTC (rev 3060)
@@ -0,0 +1,303 @@
+###
+#
+# UI code starts here
+#
+###
+var ssr = nil;
+var UIwindow = nil;
+var STATUS = {SUCCESS:0 , FAILURE:1};
+
+var configureGCA = func( gcaObject=nil, values=nil, mask=nil ) {
+#~ fgcommand("pause");
+if( !(getprop("/sim/freeze/clock") or getprop("/sim/freeze/master")) ) {
+ setprop("/sim/freeze/clock",1);
+ setprop("/sim/freeze/master",1);
+ }
+if(mask==nil) mask = {};
+var defaults = {icao: 'KSFO' , rwy:'28R', safety_slope:0.0, channel:'/sim/messages/approach', interval:5.00};
+foreach(var key; keys(values)) {
+ defaults[key] = values[key];
+ if(myDbg) printf("%s <- %s",key,values[key]);
+ }
+if(UIwindow !=nil) UIwindow.del();
+var (width,height) = (240,550);
+var title = 'GCA Dialog ';
+
+UIwindow = canvas.Window.new([width,height],"dialog").set('title',title).clearFocus();
+
+UIwindow.del = func()
+{
+print("Cleaning up window:",title,"\n");
+call(canvas.Window.del, [], me);
+};
+
+# adding a canvas to the new window and setting up background colors/transparency
+var myCanvas = UIwindow.createCanvas().set("background", canvas.style.getColor("bg_color"));
+
+# creating the top-level/root group which will contain all other elements/group
+var root = myCanvas.createGroup();
+
+# create a new layout
+var myLayout = canvas.VBoxLayout.new();
+# assign it to the Canvas
+myCanvas.setLayout(myLayout);
+
+var setupWidgetTooltip = func(widget, tooltip) {
+ widget._view._root.addEventListener("mouseover", func gui.popupTip(tooltip) );
+} # setupWidgetTooltip
+
+
+var setupLabeledInput = func(root, layout, input) {
+
+
+var label = canvas.gui.widgets.Label.new(root, canvas.style, {wordWrap: 0});
+var unit_suffix = sprintf(" (%s):", input.unit);
+label.setText(input.text~unit_suffix);
+layout.addItem(label);
+
+var field = canvas.gui.widgets.LineEdit.new(root, canvas.style, {});
+layout.addItem(field);
+field.setText(sprintf(input.default_value));
+
+if (input.focus)
+field.setFocus();
+
+setupWidgetTooltip(widget:field, tooltip: input.tooltip);
+
+var el = field._view._root;
+el.addEventListener("keypress", func (e) {
+
+# colorize valid/invalid inputs
+var color = (validationHelpers[input.validate]( field.text() ) == 0) ? [0,1,0] : [1,0,0];
+field._view._root.setColorFill(color);
+
+});
+
+
+return field; # return to caller
+} # setupLabeledInput()
+
+var validationHelpers = {
+_internalState: {},
+
+'AircraftRoot': func(input) {
+var root = props.getNode(input);
+if (root == nil) return STATUS.FAILURE; # error
+
+var required_props = ['altitude-ft', 'longitude-deg', 'latitude-deg'];
+
+foreach(var rp; required_props) {
+ if (getprop(input ~"/" ~rp) == nil) return STATUS.FAILURE;
+} # foreach
+
+return STATUS.SUCCESS; # valid root node
+},
+
+'Airport': func(input) {
+var match = airportinfo(input);
+if (match == nil or typeof(match) != 'ghost') return STATUS.FAILURE;
+validationHelpers._internalState.airport = match;
+return STATUS.SUCCESS;;
+
+},
+
+'Runway': func(input) {
+
+var runways = validationHelpers._internalState.airport.runways;
+if (typeof(runways)!="hash" or !size(keys(runways))) return STATUS.FAILURE;
+if (runways[input] == nil) return STATUS.FAILURE;
+validationHelpers._internalState.rwy = input;
+
+return STATUS.SUCCESS;
+},
+
+'FinalApproach': func(input) {
+if (input <3) return STATUS.FAILURE;
+validationHelpers._internalState.final = input;
+return STATUS.SUCCESS;
+},
+
+'Glidepath': func(input) {
+if (input <1 or input>180) return STATUS.FAILURE;
+return STATUS.SUCCESS;
+},
+
+'SafetySlope': func(input) {
+if (input <0 or input>180) return STATUS.FAILURE;
+return STATUS.SUCCESS;
+},
+
+'TerrainResolution': func(input) {
+if (input <0 or input>10) return STATUS.FAILURE;
+return STATUS.SUCCESS;
+},
+
+'DecisionHeight': func(input) {
+if (input <0) return STATUS.FAILURE;
+return STATUS.SUCCESS;
+},
+
+'TouchdownOffset': func(input) {
+if (input <0) return STATUS.FAILURE;
+return STATUS.SUCCESS;
+},
+
+'TransmissionInterval': func(input) {
+if (input <0 ) return STATUS.FAILURE;
+return STATUS.SUCCESS;
+},
+
+'TransmissionProperty': func(input) {
+if (getprop(input) == nil) return STATUS.FAILURE;
+return STATUS.SUCCESS;
+},
+
+'VertGrid': func(input) {
+if (input <100 or input >4000) return STATUS.FAILURE;
+return STATUS.SUCCESS;
+},
+
+'HzGrid': func(input) {
+if (input <0.1 or input >10) return STATUS.FAILURE;
+return STATUS.SUCCESS;
+},
+
+}; # validationHelpers;
+
+
+var inputs = [
+{text: 'Position root', default_value:'/position', focus:0, callback:gcaObject.setPosition, tooltip:'property path to position node', validate: 'AircraftRoot', convert:nil, unit: 'property path'},
+{text: 'Airport', default_value:defaults.icao, focus:0, callback:gcaObject.setAirport, tooltip:'ICAO ID, e.g. KSFO', validate: 'Airport', convert:nil, unit:'ICAO'},
+{text: 'Runway', default_value:defaults.rwy, focus:0, callback:gcaObject.setRunway, tooltip:'runway identifier, e.g. 28L', validate: 'Runway', convert:nil, unit:'rwy'},
+{text: 'Touchdown Offset', default_value:'0.00', focus:0, callback:gcaObject.setTouchdownOffset, tooltip:'touchdown offset', validate: 'TouchdownOffset', convert:num, unit:'m'},
+{text: 'Final Approach', default_value:'10.00', focus:0, callback:gcaObject.setFinalApproach, tooltip:'length of final approach leg', validate: 'FinalApproach', convert:num, unit:'nm'},
+{text: 'Glidepath', default_value:'3.00', focus:0, callback:gcaObject.setGlidepath, tooltip:'glidepath in degrees, e.g. 3', validate: 'Glidepath', convert:num, unit:'degrees'},
+
+{text: 'Safety Slope', default_value:defaults.safety_slope, focus:0, callback:gcaObject.setSafetySlope, tooltip:'safety slope in degrees', validate: 'SafetySlope', convert:num, unit:'degrees'},
+{text: 'Decision Height', default_value:'200.00', focus:0, callback:gcaObject.setDecisionHeight, tooltip:'decision height (vertical offset)', validate: 'DecisionHeight', convert:num, unit:'ft'},
+{text: 'Terrain Resolution', default_value:'0.10', focus:0, callback:gcaObject.setTerrainResolution, tooltip:'granularity/resolution of the terrain sampling', validate: 'TerrainResolution', convert:num, unit:'nm'},
+{text: 'Horizontal Grid', default_value:'1.00', focus:0, callback:gcaObject.setHzGrid, tooltip:'horizontal grid resolution in Radar screen', validate: 'HzGrid', convert:num, unit:'nm/div'},
+{text: 'Vertical Grid', default_value:'1000', focus:0, callback:gcaObject.setVertGrid, tooltip:'vertical grid resolution in Radar screen', validate: 'VertGrid', convert:num, unit:'feet/div'},
+
+{text: 'Transmission channel', default_value:defaults.channel, focus:0, callback:gcaObject.setTransmissionChannel, tooltip:'property to use for transmissions. For example: /sim/multiplay/chat or /sim/sound/voices/approach', validate: 'TransmissionProperty', convert:nil, unit:'property'},
+{text: 'Transmission interval', default_value:defaults.interval, focus:0, callback:gcaObject.setTransmissionInterval, tooltip:'Controller/timer resolution', validate: 'TransmissionInterval', convert:num, unit:'secs'},
+# Warning: 'Transmission interval' must be the last one, since it launches the timer !!
+]; # input fields
+
+for(var i=0; i<size(inputs); i+=1) {
+ if(!contains(mask, inputs[i].text)){
+ inputs[i].widget = setupLabeledInput(root, myLayout, inputs[i]);
+ }
+}
+var validateFields = func() {
+var ret = STATUS.SUCCESS; # by default
+foreach(var f; inputs) {
+ if(contains(mask, f.text)) continue;
+ var result = validationHelpers[f.validate] ( f.widget.text() );
+ if (result == STATUS.FAILURE) {
+
+canvas.MessageBox.critical(
+ "Validation error",
+ "Error validating "~f.text,
+ cb = nil,
+ buttons = canvas.MessageBox.Ok
+); # MessageBox
+
+ret = STATUS.FAILURE;
+ } # error handling
+} # foreach
+return ret; # all validations passed
+} # validateFields()
+
+
+###
+# global stuff
+#
+
+var gcaRunning = 0;
+
+var toggleFields = func(enabled) {
+foreach(var i; inputs) {
+ if(!contains(mask, i.text)) i.widget.setEnabled(enabled);
+}
+}
+
+var createPar = func () {
+
+for(var i=0; i<size(inputs)-1; i+=1) {
+var value = (contains(mask, inputs[i].text)) ? inputs[i].default_value : inputs[i].widget.text();
+if (inputs[i].convert != nil and typeof(inputs[i].convert)=='func') {
+var converted = inputs[i].convert( value );
+}
+call(inputs[i].callback, [value], gcaObject );
+} # for
+
+ if(Par !=nil) Par.wndow.del();
+ gcaObject.updateStage();
+ Par = gca.PARScreen.new(444,300,rwy=gcaObject.destination.rwy_object
+ ,rwy_elev=gcaObject.destination.elevation);
+ Par.setGrid(gcaObject.HzGrid, gcaObject.VertGrid);
+ Par.setSafetySlope(minSlope(gcaObject.touch,
+ gcaObject.destination.rwy_object.heading+180,
+ gcaObject.destination.final *NM2M));
+ Par.setSlope(gcaObject.destination.glidepath);
+ Par.setTerrainResolution(gcaObject.TerrainResolution);
+ Par.setZoom(15,4000); # view (Hz nm, Vert ft)
+ Par.appendTrack(gcaObject.rwyDist,
+ gcaObject.destination.rwy_object.heading-gcaObject.rwyCrse,
+ gcaObject.destination.elevation*M2FT);
+} # createPar
+
+var buildCGA = func() {
+
+if(Par == nil) createPar();
+
+call(inputs[-1].callback, [inputs[-1].widget.text()], gcaObject );
+
+fgcommand("pause");
+ gcaObject.updateStage();
+ UIwindow.clearFocus();
+ UIwindow.del();
+print("demo starts");
+
+} # buildCGA
+
+var toggleGCA = func() {
+
+if (gcaRunning) {
+gcaObject.stop();
+gcaRunning = 0;
+toggleFields(!gcaRunning); # set editable
+return;
+}
+
+if (!gcaRunning and validateFields()==STATUS.SUCCESS) {
+gcaRunning = 1;
+toggleFields(!gcaRunning); # set readonly
+buildCGA();
+return;
+}
+} # toggleGCA()
+
+
+var button = canvas.gui.widgets.Button.new(root, canvas.style, {})
+ .setText("Start/Stop")
+ .setFixedSize(75, 25)
+ .listen("clicked", toggleGCA);
+
+var apply = canvas.gui.widgets.Button.new(root, canvas.style, {})
+ .setText("Apply")
+ .setFixedSize(55, 25)
+ .listen("clicked", createPar);
+
+
+if (gcaRunning) Pause();
+setupWidgetTooltip(widget:button, tooltip: "toggle GCA on/off");
+setupWidgetTooltip(widget:apply, tooltip: "view Par screen");
+myLayout.addItem(apply);
+myLayout.addItem(button);
+
+}; # configureGCA();
+
+
+
Added: trunk/Addons/SpokenGCA/main.nas
===================================================================
--- trunk/Addons/SpokenGCA/main.nas (rev 0)
+++ trunk/Addons/SpokenGCA/main.nas 2017-10-25 21:18:22 UTC (rev 3060)
@@ -0,0 +1,27 @@
+# ** Spoken GCA. **
+# *********************************************************
+# This file is part of FlightGear.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or any later version.
+
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+var main = func( root ) {
+ foreach(var f; ['control.nas','tools.nas', 'parscreen_class.nas', 'gca_class.nas', 'gca_gui.nas'] ) {
+ io.load_nasal( root ~ "/" ~ f, "gca" );
+ };
+ io.read_properties(root ~ "/" ~"config.xml", "/");
+ io.read_properties(root ~ "/" ~"phraseology.xml", "/gca/phrases");
+ print("Gca properties loaded.");
+}
Added: trunk/Addons/SpokenGCA/parscreen_class.nas
===================================================================
--- trunk/Addons/SpokenGCA/parscreen_class.nas (rev 0)
+++ trunk/Addons/SpokenGCA/parscreen_class.nas 2017-10-25 21:18:22 UTC (rev 3060)
@@ -0,0 +1,169 @@
+var PARScreen = {
+# constructor
+new: func(width=444,height=300,rwy=nil,rwy_elev=0) {
+ var m = {parents:[PARScreen] };
+ m.wndow = canvas.Window.new([width,height],"dialog").set("title","P A R screen (Vgrid=1000 ft , Hgrid=1 nm)" );
+ m.myCanvas = m.wndow.createCanvas().set("background", "#011101");
+ m.graph = m.myCanvas.createGroup().createChild("group");
+ m.size = {width:width, height:height};
+ m.vtGrid = 1000; # 1000 ft/div
+ m.grid = nil;
+ m.rwy = rwy;
+ m.rwy_elev = rwy_elev;
+ m.safety_slope = 0;
+ m.slope = 3;
+ m.Track1 = nil;
+ m.zoom = 15;
+ return m;
+ }, # new()
+
+# destructor
+del: func() {
+}, # del()
+
+##### Setters:
+setSafetySlope: func(slope) {
+ me.safety_slope = slope;
+ if(myDbg) print(27,slope);
+ }, # setSlope()
+
+setTerrainResolution: func(nm) {
+ me.TerrainResolution = nm;
+ if(myDbg) print(27,nm);
+ }, # setTerrainResolution
+
+setSlope: func(slope) {
+ me.slope = slope;
+ }, # setSlope()
+
+setGrid: func(hz, vt) {
+ me.vtGrid = vt;
+ me.hzGrid = hz;
+ me.wndow.set("title",sprintf("P A R screen (Vgrid=%i ft , Hgrid=%.2f nm)", me.vtGrid, me.hzGrid) );
+ }, # setSGrid()
+
+setZoom: func(nmH, ftV) { # view up to <nmH>mn, <ftV>feet
+#~ print("zoomed");
+
+if(me.grid !=nil) {
+ me.grid.del();
+ me.center.del();
+ me.cones.del();
+ me.terrain.del();
+ me.Track1.del();
+ me.Track2.del();
+ me.zoom = nmH;
+ } else {
+ me.wndow.move(5,60);
+}
+me.XYscale = (me.size.width-10)/nmH; # px/nm
+me.Zscale = (me.size.height*.4-10)/ftV; # px/ft
+(me.x0,me.y0,me.z0) = (10,me.size.height*0.7,me.size.height*0.4-10);
+me.grid = me.graph.createChild("path", "grid")
+.setColor(0.15,0.15,0.15)
+.setStrokeLineWidth(1);
+
+for(var z=me.z0; z>2; z-=me.vtGrid*me.Zscale){
+ me.grid.moveTo(2, z)
+ .lineTo(me.size.width-2, z);
+ }
+for(var x=0; me.x0+x<me.size.width-2; x+=me.hzGrid*me.XYscale){
+ me.grid.moveTo(me.x0+x, 2)
+ .lineTo(me.x0+x, me.size.height-2);
+ }
+for(var y=0; me.y0+y<me.size.height-2; y+=me.hzGrid*me.XYscale){
+ me.grid.moveTo(2, me.y0-y)
+ .lineTo(me.size.width-2, me.y0-y);
+ me.grid.moveTo(2, me.y0+y)
+ .lineTo(me.size.width-2, me.y0+y);
+ }
+
+me.center = me.graph.createChild("path", "center")
+ .moveTo(me.x0, me.z0)
+ .lineTo(me.x0+10*me.XYscale, me.z0-10*math.tan(me.slope*D2R)*NM2FT*me.Zscale)
+ .moveTo(me.x0, me.y0)
+ .lineTo(me.x0+10*me.XYscale, me.y0)
+ .setColor(0.3,0,0)
+ .setStrokeLineWidth(1);
+
+var t2 = systime(); # record time
+# get terrain profil (each 0.1nm)
+var h = getVertProfile(me.rwy,me.rwy_elev,me.z0,me.Zscale,me.zoom,me.TerrainResolution);
+#~ print("size(h): ",size(h));
+#~ debug.dump(h);
+var maxH = maxVect(h);
+#~ print(maxH);
+me.terrain = me.graph.createChild("path", "terrain")
+ .moveTo(0,me.z0 )
+ .lineTo(me.x0,me.z0 )
+ .setColor(0.15,0.10,0.06)
+ .setStrokeLineWidth(1);
+for(var i=0; i<size(h); i+=1) me.terrain.lineTo(me.x0+i*(me.TerrainResolution*me.XYscale), h[i]);
+
+me.terrain.lineTo(me.x0+i*(me.TerrainResolution*me.XYscale), maxH);
+me.terrain.lineTo(0, maxH);
+me.terrain.close();
+me.terrain.setColorFill(0.15,0.10,0.06);
+me.terrain.set('fill',0);
+ var t3 = systime(); # record new time
+ if(myDbg) print("drawing terrain took ", (t3 - t2)*1000, " ms"); # print result
+
+me.cones = me.graph.createChild("path", "cones")
+ .moveTo(me.x0+10*me.XYscale, me.y0-10*math.tan(6*D2R)*me.XYscale)
+ .lineTo(me.x0,me.y0 )
+ .lineTo(me.x0+10*me.XYscale, me.y0+10*math.tan(6*D2R)*me.XYscale)
+
+ .moveTo(me.x0+10*me.XYscale, me.z0-10*math.tan(me.safety_slope*D2R)*NM2FT*me.Zscale)
+ .lineTo(me.x0, me.z0)
+ .lineTo(me.x0+10*me.XYscale, me.z0-10*math.tan((2+me.slope)*D2R)*NM2FT*me.Zscale)
+ .setColor(.3,.3,.3)
+ .setStrokeLineWidth(1);
+
+var div = me.graph.createChild("path", "div")
+ .moveTo(0, me.size.height*0.4)
+ .lineTo(me.size.width, me.size.height*0.4)
+ .setColor(0,0,0)
+ .setStrokeLineWidth(3);
+
+me.Track1 = me.graph.createChild("path", "Track1")
+ .setColor(.3,.3,.8)
+ .setStrokeLineWidth(1);
+
+me.Track2 = me.graph.createChild("path", "Track2")
+ .setColor(.3,.3,.8)
+ .setStrokeLineWidth(1);
+
+me.wndow.clearFocus();
+
+}, # setZoom
+
+##### end of Setters ################
+
+
+########################################
+## Helpers:
+###
+appendTrack: func(dist,delta,rwyAlt) { # dist in nm, delta in deg, rwyAlt in ft.
+if(myDbg) printf("appndTrack( dist= %.2fnm ,delta=%.2f.deg , rwyAlt=%.2fft )",dist,delta,rwyAlt);
+ var x = int(me.x0+dist*math.cos(delta*D2R)*me.XYscale);
+ var y = int(me.y0-dist*math.sin(delta*D2R)*me.XYscale);
+ var z = int(me.z0 - (getprop("/position/altitude-ft") - rwyAlt) * me.Zscale);
+if(myDbg) printf("x=%i ,y=%i, z=%i px ",x,y,z);
+#~ debug.dump(me.Track1.getNumCoords());
+
+ if(me.Track1.getNumCoords() == 0) {
+ me.Track1.moveTo(x, z);
+ me.Track2.moveTo(x, y);
+ #~ me.prev = [x,y,z];
+ #~ print(111);
+} else {
+ me.Track1.lineTo(x, z);
+ if(y>me.size.height*0.4) {
+ me.Track2.lineTo(x, y);
+ } else {
+ me.Track2.moveTo(x, y);
+ }
+ }
+
+}, # appendTrack
+}; # PARScreen
Added: trunk/Addons/SpokenGCA/phraseology.xml
===================================================================
--- trunk/Addons/SpokenGCA/phraseology.xml (rev 0)
+++ trunk/Addons/SpokenGCA/phraseology.xml 2017-10-25 21:18:22 UTC (rev 3060)
@@ -0,0 +1,121 @@
+<?xml version="1.0"?>
+<!--
+ This file is part of SpokenGCA.
+ Copyright Rodolfo Leibner (rle...@gm...) 2017
+ under GPL licence, see <http://www.gnu.org/licenses/>
+-->
+<!--
+Misspelings are intentionally to improve intelligibility.
+must be declared as <replace>phoo:foo</replace>
+at the bottom of this file.
+-->
+
+<PropertyList>
+ <version type="double">2.1</version>
+<!-- # starting: -->
+ <short type="string">%/gca/callsign-fmt</short>
+ <short type="string">, </short>
+
+ <this type="string">%/gca/callsign-fmt</this>
+ <this type="string">, this will be a P A R approach to </this>
+ <this type="string">%/gca/apt-name</this>
+ <this type="string">, Runwaide </this>
+ <this type="string">%gca/rwy-in-use</this>
+ <this type="string">. </this>
+
+<!-- # left: Vector to app course -->
+ <left type="string">Turn </left>
+ <left type="string">%/gca/value[0]</left>
+ <left type="string"> heading </left>
+ <left type="string">%/gca/value[1]</left>
+ <left type="string">. </left>
+ <left type="string">%/gca/value[2]</left>
+ <left type="string"> miles from approach course. </left>
+
+<!-- # left: Vector to app course -->
+ <leftok type="string">Maintain heading </leftok>
+ <leftok type="string">~right[3]</leftok>
+
+<!-- # well left: nm to rwy -->
+ <wellleft type="string">Well left of course. </wellleft>
+ <wellleft type="string">~oncourse[3]</wellleft>
+
+<!-- # slightly left: nm to rwy -->
+ <slightleft type="string">Slightly left of course. </slightleft>
+
+<!-- # slightly right: nm to rwy -->
+ <slightright type="string">Slightly right of course. </slightright>
+
+<!-- # right: Vector to app course -->
+ <right type="string">~left[0]</right>
+
+<!-- # right: Vector to app course -->
+ <rightok type="string">Maintain heading </rightok>
+ <rightok type="string">~left[3]</rightok>
+
+<!-- # well right: Vector to rwy -->
+ <wellright type="string">Well right of course. </wellright>
+ <wellright type="string">~oncourse[3]</wellright>
+
+<!-- # on course: Vector to rwy -->
+ <oncourse type="string">On course. </oncourse>
+ <oncourse type="string">%/gca/value[1]</oncourse>
+ <oncourse type="string"> miles from touchdown. </oncourse>
+
+<!-- # downwind To left base ok -->
+ <tolbaseok type="string">Maintain heading </tolbaseok>
+ <tolbaseok type="string">~tolbase[3]</tolbaseok>
+
+<!-- # downwind To left base -->
+ <tolbase type="string">Turn </tolbase>
+ <tolbase type="string">%/gca/value[0]</tolbase>
+ <tolbase type="string"> heading </tolbase>
+ <tolbase type="string">%/gca/value[1]</tolbase>
+ <tolbase type="string"> . </tolbase>
+
+<!-- # downwind To right base ok -->
+ <torbaseok type="string">Maintain heading </torbaseok>
+ <torbaseok type="string">~tolbase[3]</torbaseok>
+
+<!-- # downwind To right base -->
+ <torbase type="string">~tolbase[0]</torbase>
+
+<!-- # climb / descend To appgate altitude -->
+ <climb type="string">%/gca/climb</climb>
+ <climb type="string">%/gca/appgate-alt-fmt</climb>
+ <climb type="string"> feet. </climb>
+
+<!-- # glide -->
+ <glide type="string">Approaching glidepath. Begin descent. </glide>
+
+<!-- # entering left base -->
+ <turn90l type="string">Make half standard rate turns. </turn90l>
+ <turn90l type="string">Turn left </turn90l>
+ <turn90l type="string"> heading </turn90l>
+ <turn90l type="string">%/gca/value[0]</turn90l>
+
+<!-- # entering right base -->
+ <turn90r type="string">Make half standard rate turns. </turn90r>
+ <turn90r type="string">Turn right </turn90r>
+ <turn90r type="string">~turn90l[2]</turn90r>
+
+<!-- # glidepath angle: -->
+ <pathalt type="string">%/gca/over-glidepath</pathalt>
+ <pathalt type="string">glidepath. </pathalt>
+
+<!-- # Others: -->
+ <dont type="string">. Do not acknowledge further transmissions. </dont>
+
+ <bye type="string">Over landing threshold. Your plane. </bye>
+
+ <abort type="string">%/gca/callsign-fmt</abort>
+ <abort type="string">, abort immediately and try again. Your plane. </abort>
+
+ <none type="string">I do not understand. </none>
+ <none type="string">Fasten your parachute and open the door.</none>
+
+<!-- # Replacements: -->
+ <replace type="string">Runwaide:Runway</replace>
+ <replace type="string">Winnded:Wind</replace>
+
+</PropertyList>
Added: trunk/Addons/SpokenGCA/tools.nas
===================================================================
--- trunk/Addons/SpokenGCA/tools.nas (rev 0)
+++ trunk/Addons/SpokenGCA/tools.nas 2017-10-25 21:18:22 UTC (rev 3060)
@@ -0,0 +1,200 @@
+# **** general debug flag **************************
+var myDbg = 0;
+var dbg = func() {
+ myDbg = myDbg? 0:1;
+ if(myDbg) printf("debug is %s.", myDbg ? "On" : "Off");
+}
+# **** point func. **************************
+var pnt = func(str) {
+ str = sprintf("%.3f",str); # accept numbers
+ if(right(str,1)=="0") str=left(str,size(str)-1);
+ return string.replace(str,"."," point ");
+}
+# **** spell func. ***************************
+# used to spell <str> numbers forced to<dig> digits. (dig=0 means no force).
+var spell = func(str, dig) {
+ if(dig>0 and size(str)<dig) { str = right("000000" ~str ,dig);}
+ var s = split("",str);
+ for(var i=0;i<size(s);i=i+1) {
+ if(streq(s[i],".")) s[i]="point" ;
+ }
+ return string.join(" ",s);
+}
+
+# **** alpha func. *****************************
+var alpha = func(str) {
+ var repl = {A:"Alpha", B:"Bravo", C:"Charlie", D:"Delta", E:"Echo", F:"Foxtrot",
+ G:"Golf", H:"Hotel", I:"India", J:"Juliet", K:"Kilo", L:"Lima", M:"Mike",
+ N:"November", O:"Oscar", P:"Papa",Q:"Quebec", R:"Romeo", S:"Sierra",
+ T:"Tango", U:"Uniform", V:"Victor", W:"Whiskey", X:"X-ray", Y:"Yanki", Z:"Zulu"};
+
+ var s = "";
+ for(var i=0;i<size(str);i=i+1) {
+ if(str[i]<91 and str[i]>64) { s ~=repl[substr(str,i,1)] ~" ";} # Translate only capital letters
+ if(str[i]<58 and str[i]>47) { s ~=substr(str,i,1) ~" ";} # and digits
+ };
+ return s;
+};
+
+# **** isEven func. *****************************
+var isEven = func(n) {
+ if(int(n/2)==n/2) {
+ return 1;
+ } else {
+ return 0; }
+}
+
+
+
+# **** join func. Joins /gca/phrases/key[] props ************
+var join = func(key="none", p="") {
+if(string.match(key,"*[][]?[][]")) {
+ var j = num(substr(key,-2,1));
+ key = left(key,size(key)-3);
+ } else {
+ var j = 0;}
+ var fs = props.globals.getNode("/gca/phrases").getChildren(key);
+ if(fs==[]) print("Error: node /gca/phrases/" ~key ~"not found !");
+ var str = "";
+ var i = 0;
+ foreach (var f; fs) {
+ if(i>=j){
+ str = f.getValue();
+ if(str==nil or str=="") continue;
+ if(left(str,1)=="%") {
+ str=getprop(string.trim(str, 0, func(c) c == `%` or c == ` `));
+ }
+ if(left(str,1)=="~"){
+ str= " " ~join(right(str,size(str)-1));}
+ p ~= str;}
+ i +=1;
+ }
+ return p;
+}
+
+# **** capit func. *****************************
+var capit = func(str) { # Rtrim 'TWR', 'APP',etc. and Capitalize words.
+ var len = size(str) - 4;
+ var out = str;
+ if(string.match(str,"*APP-DEP") or string.match(str,"*DEP-APP")) {
+ out = left(str,len-4); }
+ if(string.match(str,"*TWR") or string.match(str,"*GND") or string.match(str,"*APP")
+ or string.match(str,"*DEP")) {
+ out = left(str,len); }
+ out = string.lc(out);
+ var vec = split(" ",out);
+ for(var i=0;i<size(vec);i=i+1) {
+ vec[i] = string.uc(left(vec[i],1)) ~substr(vec[i],1);
+ }
+ return string.join(" ",vec);
+ }
+
+
+# **** say func. to test TTS phrases.
+# (key may be a /gca/phrases/xxx prop or any literal string. ************
+var say = func(key) {
+ p = join(key);
+ if(size(p)==0){
+ setprop("/sim/sound/voices/atc", key);
+ } else {
+ setprop("/sim/sound/voices/atc", p);
+ }
+}
+
+# **** getVertProfile func. returns h[ <terrain elevation(feet)> ...] (each <resolution> nm).
+# (rwy: rwy_object; rwy_elev: info.elev(m). ************
+var getVertProfile = func(rwy,rwy_elev,z0,Zscale,zoom,resolution) {
+var x0 = geo.Coord.new().set_latlon(rwy.lat, rwy.lon).latlon();
+var x1 = geo.Coord.new().set_latlon(rwy.lat, rwy.lon)
+ .apply_course_distance(rwy.heading+180, rwy.threshold+zoom*NM2M).latlon();
+var dlat = (x1[0]-x0[0])/(zoom/resolution);
+var dlon = (x1[1]-x0[1])/(zoom/resolution);
+var h = [];
+for(var i=0; i<=(zoom/resolution); i+=1) append(h, geo.elevation(x0[0]+i*dlat, x0[1]+i*dlon));
+# got h with elevations (m)
+var from = 999;
+var to=999;
+for(var i=0;i<size(h);i+=1){
+if(h[i]==nil and from==999) from=i;
+if(h[i]==nil and from!=999) to=i;
+}
+if(from !=999){
+ printf("Warning: found nils between %.2fnm and %.2fnm from rwy.",from*0.1,to*0.1);
+#~ debug.dump(h);
+}
+for(var i=0; i<=(zoom/resolution); i+=1) if(h[i]!=nil) h[i]=(h[i]-rwy_elev)*M2FT;
+# got h with elevations (ft) relative to rwy
+for(var i=0; i<=(zoom/resolution); i+=1)
+ h[i]=(h[i]!=nil and h[i]*Zscale<-10)or h[i]==nil ? int(z0 +10) : int(z0-h[i]*Zscale);
+# got h with elevations (px)
+return h;
+}
+
+# **** chooseRwy func.
+# (rwys: airportinfo().runways object.) ************
+var chooseRwy = func(rwys) {
+var best = "";
+var ang = 180.0;
+var dest_rwy = nil;
+var rm = 0;
+if (getprop("/autopilot/route-manager/active")) {
+ dest_rwy = getprop("/autopilot/route-manager/destination/runway");
+ rm = 1; }
+# Choise best rwy
+foreach(var rw; keys(rwys)){
+ #~ if (rw == getprop("sim/atc/runway") or (dest_rwy != nil and dest_rwy == rw)) {
+ if (0) {
+ best = rw;
+ break;
+ } else {
+ var a = abs(rwys[rw].heading - getprop("/environment/wind-from-heading-deg"));
+ if(a<ang) {
+ ang = a;
+ best = rw;}
+ }
+ }
+return best;
+}
+# **** maxVect func.
+var maxVect = func(vector) {
+var max = 0;
+foreach(var x;vector) if(x>max) max = x;
+ return max;
+}
+
+# **** minSlope func.
+var minSlope = func(geoA, dir, dist) { # dir in deg, dist en m.
+var v = nil;
+for(var a=0; a<10 ; a+=0.1) {
+# Check for terrain between geoA and geoB:
+var geoB = geo.Coord.new(geoA).apply_course_distance(dir,dist).set_alt(geoA.alt()+dist*math.tan(a*D2R));
+v = get_cart_ground_intersection({"x":geoA.x(), "y":geoA.y(), "z":geoA.z()}, {"x":geoB.x()-geoA.x(), "y":geoB.y()-geoA.y(), "z":geoB.z()-geoA.z()});
+if (v == nil) {
+ if(myDbg) print("min Slope=",a);
+ return a;
+} else {
+ var terrain = geo.Coord.new();
+ terrain.set_latlon(v.lat,v.lon,v.elevation);
+ var maxDist = geoA.direct_distance_to(geoB);
+ var terrainDist = geoA.direct_distance_to(terrain);
+ if (terrainDist < maxDist) {
+ #~ print("a=",a," :terrain found between ponts at ",terrainDist*M2NM," nm from touchdown.");
+ continue;
+ } else {
+ if(myDbg) print("min Slope=",a);
+ return a;
+ }
+}
+}
+return 10;
+}
+
+# ***********************************
+var NM2FT =6076;
+var XYscale =nil;
+var Zscale =nil;
+var Track1 =nil;
+var Track2 =nil;
+print("GCA tools loaded.");
+
+
|