Howto:Implement a Vertical Situation Display in Nasal: Difference between revisions

From FlightGear wiki
Jump to navigation Jump to search
m (https://forum.flightgear.org/viewtopic.php?f=30&t=32882&start=30#p319234)
m (→‎Objective: archiving related stuff)
Line 10: Line 10:
== Objective ==
== Objective ==
Implement a VSD (Vertical Situation Display) instrument using Nasal and XML.
Implement a VSD (Vertical Situation Display) instrument using Nasal and XML.
clm76 adapted Omega95's VSD to the CitationX<ref>{{cite web
  |url    =  https://forum.flightgear.org/viewtopic.php?p=319234#p319234
  |title  =  <nowiki> Re: Spoken GCA </nowiki>
  |author =  <nowiki> clm76 </nowiki>
  |date  =  Sep 23rd, 2017
  |added  =  Sep 23rd, 2017
  |script_version = 0.40
  }}</ref>
<syntaxhighlight lang="nasal">
#### Citation X - Vertical Situation Display ####
#### Narendran M (c) 2014
#### Adapted by C. Le Moigne (clm76) - 2017 ####
props.globals.initNode("instrumentation/efis/inputs/vsd",0,"BOOL");
var alt = 0;
var  alt_ind = "/instrumentation/altimeter/indicated-altitude-ft";
var asel = "/autopilot/settings/asel";
var  curWpt = "/autopilot/route-manager/current-wp";
var dep_alt = "/autopilot/route-manager/departure/field-elevation-ft";
var dest_alt = "/autopilot/route-manager/destination/field-elevation-ft";
var dist_rem = "autopilot/route-manager/distance-remaining-nm";
var fp_active = "/autopilot/route-manager/active";
var  heading_ind =  "/instrumentation/heading-indicator/indicated-heading-deg";
var num_wpts = "/autopilot/route-manager/route/num";
var set_range = "/instrumentation/efis//inputs/range-nm";
var svg_path = "/Aircraft/CitationX/Models/Instruments/MFD/canvas/Images/vsd.svg";
var tg_alt = "autopilot/settings/tg-alt-ft";
var toggle_vsd = "/instrumentation/efis//inputs/vsd";
var totDist = "autopilot/route-manager/total-distance";
var  vert_spd = "/velocities/vertical-speed-fps";
var rangeHdg = [];
var brk_next = nil;
var color = nil;
var text = nil;
var pos = nil;
var check_hdg = nil;
var elev = nil;
var vs_fps = nil;
var gs_fps = nil;
var fpa = nil;
var info = nil;
var elevation = nil;
      #### VSD CLASS ###
var vsd = {
  terrain_color:    [0.44, 0.19, 0.09, 0.5],    # A brownish color
  path_color:    [1,0,1,1],      # Magenta
  cstr_color:    [0,1,0,1],      # Green
  tod_color :      [1,1,0,1],      # Yellow
  elev_pts: 21,                        # Number of elevation data points
  elev_profile: [],
  range: 20,
  lastalt:0,
  lastaltset:0,
  alt_ceil: 10000,
  altitude: 0,
  peak: 0,
  alt_ceil_px: 180,      # Pixel length of vertical axis
  max_range_px: 810,  # Pixel length of horizontal axis
  terr_offset: 22,      # Offset between start of terrain polygon y and bottom_left corner
  bottom_left: {x:190, y:294},  # {x:x,y:y_max - y} of bottom-left corner of plot area - looks like canvas starts it's y axis from the top going down - old 233,294
  new: func() {
      var m = {parents:[vsd]};
      m.display = canvas.new({
        "name": "vsdScreen",
        "size": [1024, 320],
        "view": [1024, 320],
        "mipmapping": 1
      });
 
      m.display.addPlacement({"node": "Vsd.screen"});
 
      ### Create canvas group
      m.group = m.display.createGroup();            # Group for canvas elements and paths
      m.text = m.display.createGroup();              # Group for waypoints text
      m.terrain = m.group.createChild("path");  # Terrain Polygon
      m.path = m.group.createChild("path");        # Flightplan Path
 
      ### Load Vertical Situation Display
      canvas.parsesvg(m.group, svg_path); 
      setsize(m.elev_profile,m.elev_pts);
      ### Create empty geo.Coord object for waypoints calculation
      m.wpt_this = geo.Coord.new();
      m.wpt_next = geo.Coord.new();
      m.wpt_tod = geo.Coord.new();
      ### Display init ###
      m.h_range = getprop(set_range);
      m.group.getElementById("text_range1").setText(sprintf("%3.0f",m.h_range*0.25));
      m.group.getElementById("text_range2").setText(sprintf("%3.0f",m.h_range*0.5));
      m.group.getElementById("text_range3").setText(sprintf("%3.0f",m.h_range*0.75));
      m.group.getElementById("text_range4").setText(sprintf("%3.0f",m.h_range));
      m.group.getElementById("tgt_altitude").setText(sprintf("%5.0f",getprop(tg_alt)));
      ### Variables ###
      m.fp = flightplan();
      m.alt_set = getprop(tg_alt);
      m.lastWp = 0;
      m.lastWp_alt = 0;
      m.lastWp_dist = 0;
      m.prevWp_alt = 0;
      m.prevWp_dist = 0;
      m.v_alt = nil;
      return m;
  }, # end of new
        ### Listeners ###
  listen : func {     
      setlistener(fp_active, func(n) {
        if (n.getValue()) {
            me.fp = flightplan();
            me.tot_dist = getprop(totDist);
            me.update();
            me.v_alt = fms.vsd_alt(); # Call altitudes vector from fms
        }
      },0,0);
      setlistener(set_range, func(n) {
        me.range = n.getValue();
        if(me.range > 10) {
            me.group.getElementById("text_range1").setText(sprintf("%3.0f",me.range*0.25));
            me.group.getElementById("text_range2").setText(sprintf("%3.0f",me.range*0.5));
            me.group.getElementById("text_range3").setText(sprintf("%3.0f",me.range*0.75));
            me.group.getElementById("text_range4").setText(sprintf("%3.0f",me.range));
        } else {
            me.group.getElementById("text_range1").setText(sprintf("%1.1f",me.range*0.25));
            me.group.getElementById("text_range2").setText(sprintf("%1.0f",me.range*0.5));
            me.group.getElementById("text_range3").setText(sprintf("%1.1f",me.range*0.75));
            me.group.getElementById("text_range4").setText(sprintf("%1.0f",me.range));
        }
      },0,0);
      setlistener(tg_alt, func(n) {
        me.alt_set = n.getValue();
        me.group.getElementById("tgt_altitude").setText(sprintf("%5.0f",me.alt_set));
        me.newSetPos = -me.alt_ceil_px*(me.alt_set/me.alt_ceil);
        me.group.getElementById("altitude_set").setTranslation(0,me.newSetPos-me.lastaltset);
        me.lastaltset = me.newSetPos;
      },0,0);
  }, # end of listen
  update: func {
      if(getprop(toggle_vsd) == 1) {
        # Generate elevation profile     
        me.altitude = getprop(alt_ind);
        if(me.altitude == nil) {
            me.altitude = 0;
        }
        me.alt_ceil = (getprop(asel)*100 == 0 ? 1 :getprop(asel)*100);
        me.new_markerPos = -me.alt_ceil_px*(me.altitude/me.alt_ceil);
        # Vertical Flight Path
            me.numWpts = me.fp.getPlanSize();
            me.currWpt = me.fp.current;
            if (me.numWpts > 1 and me.currWpt >= 0) {
              me.path.del();
              me.text.removeAllChildren();
              me.path = me.group.createChild("path");
              me.path.setColor(me.path_color)
                      .moveTo(me.bottom_left.x,me.bottom_left.y-4+me.new_markerPos)
                      .setStrokeLineWidth(2)
                      .show();
              me.wpt_this.set_latlon(me.fp.getWP(me.currWpt).lat, me.fp.getWP(me.currWpt).lon);
              me.rteLen = geo.aircraft_position().distance_to(me.wpt_this)*M2NM;
              brk_next = 0;
              # Calculate distance between waypoints
              for(var i=me.currWpt; i<me.numWpts; i=i+1) {
                  if (i == 0) {alt = getprop(dep_alt)}
                  else if (i == me.numWpts-1) {alt = getprop(dest_alt)}
                  else {
                    if (me.v_alt != nil) { # plan not activated
                    #### BASIC ###
                        if(me.fp.getWP(i).wp_type == "basic" and me.fp.getWP(i).wp_role == nil) {
                          ### SIDS ###
                          if (me.fp.getWP(i).distance_along_route < me.tot_dist/2) {
                              if (me.v_alt.vector[i] <= 0) {
                                alt = getprop(asel)*100;
                              } else {
                                alt = me.v_alt.vector[i];
                              }
                          } else {
                          ### STARS ###
                              alt = me.v_alt.vector[i];                       
                          }
                        } else {
                        ### NAVAIDS ###
                              alt = me.v_alt.vector[i];
                        }
                    }
                  }
                  if(me.rteLen > me.range) {brk_next = 1}
                  me.path.lineTo(me.bottom_left.x + me.max_range_px*(me.rteLen/me.range), me.bottom_left.y - me.alt_ceil_px*(alt/me.alt_ceil));
                  # Add waypoint ident
                  if (me.fp.getWP(i).wp_name == 'TOD') {
                    color = me.tod_color;
                    text = "TOD";
                  } else {
                    color = me.path_color;
                    text = me.fp.getWP(i).id;
                  }
                  me.text.createChild("text")
                        .setAlignment("left-bottom")
                        .setColor(color)
                        .setFontSize(24,1.0)
                        .setTranslation(me.bottom_left.x + 10 + me.max_range_px*(me.rteLen/me.range), me.bottom_left.y - 10 - me.alt_ceil_px*(alt/me.alt_ceil))
                        .setText(text);
                  me.text.createChild("text")
                        .setAlignment("center-center")
                        .setColor(color)
                        .setFontSize(28,1.2)
                        .setTranslation(me.bottom_left.x + me.max_range_px*(me.rteLen/me.range), me.bottom_left.y - me.alt_ceil_px*(alt/me.alt_ceil))
                        .setText("*");
                  if(i<me.numWpts-1) {
                    me.wpt_this.set_latlon(me.fp.getWP(i).lat, me.fp.getWP(i).lon);
                    me.wpt_next.set_latlon(me.fp.getWP(i+1).lat, me.fp.getWP(i+1).lon);
                    append(rangeHdg, {range: me.rteLen, course: me.wpt_this.course_to(me.wpt_next)});
                    me.rteLen = me.rteLen + me.wpt_this.distance_to(me.wpt_next)*M2NM;
                  }
                  if(brk_next == 1) {break}
              }
            } else {
              me.path.hide();
              me.text.removeAllChildren();
            }
 
        pos = geo.aircraft_position();
 
        # Get terrain profile along the flightplan route if WPT is enabled. If WPT is not enabled, the rangeHdg vector should be empty, so it's just going to get the elevation profile along the indicated aircraft heading
     
        me.peak = 0;
        forindex(var j; me.elev_profile) {
            check_hdg = getprop(heading_ind);
            foreach(var wpt; rangeHdg) {
              if(j*(me.range/me.elev_pts) > wpt.range) {
                  check_hdg = wpt.course;
              } else {break}
            }
            pos.apply_course_distance(check_hdg,(me.range/me.elev_pts)*NM2M);
            elev = me.get_elevation(pos.lat(), pos.lon());
            me.elev_profile[j] = elev;
            if(elev > me.peak) {
              me.peak = elev;
            }
        }
        # Set Altitude Numbers
        me.terrain.del();
        me.terrain = me.group.createChild("path");
        me.terrain
              .setColorFill(me.terrain_color)
              .moveTo(me.bottom_left.x,me.bottom_left.y + me.terr_offset);
 
        # Draw Terrain
        forindex(var k; me.elev_profile) {
            me.terrain.lineTo(me.bottom_left.x+(k*(me.max_range_px/(me.elev_pts-1))), me.bottom_left.y -6- me.alt_ceil_px*(me.elev_profile[k]/me.alt_ceil));
        }
            me.terrain.lineTo(me.bottom_left.x+me.max_range_px,me.bottom_left.y+ me.terr_offset);
 
        me.group.getElementById("text_alt1").setText(sprintf("%5.0f",me.alt_ceil/2));
        me.group.getElementById("text_alt2").setText(sprintf("%5.0f",me.alt_ceil));
        me.group.getElementById("aircraft_marker").setTranslation(0,me.new_markerPos-me.lastalt);
        ### Speed Arrow ###
        vs_fps = getprop(vert_spd);
        if(vs_fps == nil) {
            vs_fps = 0;
        }
        gs_fps = getprop("/velocities/groundspeed-kt")*1.46667; # KTS to FPS
        if(gs_fps > 60) {
            fpa = math.atan2(vs_fps, gs_fps);
            me.group.getElementById("speed_arrow")
                    .setTranslation(0,me.new_markerPos-me.lastalt)
                    .setRotation(-fpa)
                    .show();       
        } else {
            me.group.getElementById("speed_arrow").hide();
        }
        me.lastalt = me.new_markerPos;
      } else {
        me.path.hide();
        me.text.removeAllChildren();
      }
      settimer(func me.update(),1);
  }, # end of update
  get_elevation : func (lat, lon) {
      info = geodinfo(lat, lon);
      if (info != nil) {var elevation = info[0] * M2FT;}
      else {elevation = -1.0; }
      return elevation;
  }, # end of get_elevation
}; # end of VSD
### START ###
var vsd_stl = setlistener("sim/signals/fdm-initialized", func {
  var vsd = vsd.new();
  vsd.listen();
  print("VSD ... Ok");
  removelistener(vsd_stl);
},0,0);
<syntaxhighlight>


[[File:Omega95-vsd-instrument.jpeg|200px|thumb|Omega95's VSD instrument]]
[[File:Omega95-vsd-instrument.jpeg|200px|thumb|Omega95's VSD instrument]]

Revision as of 19:11, 23 September 2017

This article or section contains out-of-date information

Please help improve this article by updating it. There may be additional information on the talk page.


  • Last updated: 01/2012
  • Contributors: Omega95,

Objective

Implement a VSD (Vertical Situation Display) instrument using Nasal and XML.

clm76 adapted Omega95's VSD to the CitationX[1]

#### Citation X - Vertical Situation Display ####
#### Narendran M (c) 2014
#### Adapted by C. Le Moigne (clm76) - 2017 ####

props.globals.initNode("instrumentation/efis/inputs/vsd",0,"BOOL");

var alt = 0;
var   alt_ind = "/instrumentation/altimeter/indicated-altitude-ft";
var asel = "/autopilot/settings/asel";
var   curWpt = "/autopilot/route-manager/current-wp";
var dep_alt = "/autopilot/route-manager/departure/field-elevation-ft";
var dest_alt = "/autopilot/route-manager/destination/field-elevation-ft";
var dist_rem = "autopilot/route-manager/distance-remaining-nm";
var fp_active = "/autopilot/route-manager/active";
var   heading_ind =   "/instrumentation/heading-indicator/indicated-heading-deg";
var num_wpts = "/autopilot/route-manager/route/num";
var set_range = "/instrumentation/efis//inputs/range-nm";
var svg_path = "/Aircraft/CitationX/Models/Instruments/MFD/canvas/Images/vsd.svg";
var tg_alt = "autopilot/settings/tg-alt-ft";
var toggle_vsd = "/instrumentation/efis//inputs/vsd";
var totDist = "autopilot/route-manager/total-distance";
var   vert_spd = "/velocities/vertical-speed-fps";
var rangeHdg = [];
var brk_next = nil;
var color = nil;
var text = nil;
var pos = nil;
var check_hdg = nil;
var elev = nil;
var vs_fps = nil;
var gs_fps = nil;
var fpa = nil;
var info = nil;
var elevation = nil;

      #### VSD CLASS ###
var vsd = {
   terrain_color:    [0.44, 0.19, 0.09, 0.5],    # A brownish color
   path_color:    [1,0,1,1],      # Magenta
   cstr_color:    [0,1,0,1],      # Green
   tod_color :      [1,1,0,1],      # Yellow
   elev_pts: 21,                        # Number of elevation data points
   elev_profile: [],
   range: 20,
   lastalt:0,
   lastaltset:0,
   alt_ceil: 10000,
   altitude: 0,
   peak: 0,
   alt_ceil_px: 180,      # Pixel length of vertical axis
   max_range_px: 810,   # Pixel length of horizontal axis
   terr_offset: 22,      # Offset between start of terrain polygon y and bottom_left corner
   bottom_left: {x:190, y:294},   # {x:x,y:y_max - y} of bottom-left corner of plot area - looks like canvas starts it's y axis from the top going down - old 233,294

   new: func() {
      var m = {parents:[vsd]};
      m.display = canvas.new({
         "name": "vsdScreen",
         "size": [1024, 320],
         "view": [1024, 320],
         "mipmapping": 1
      });
   
      m.display.addPlacement({"node": "Vsd.screen"});
   
      ### Create canvas group
      m.group = m.display.createGroup();            # Group for canvas elements and paths
      m.text = m.display.createGroup();               # Group for waypoints text
      m.terrain = m.group.createChild("path");   # Terrain Polygon
      m.path = m.group.createChild("path");         # Flightplan Path
   
      ### Load Vertical Situation Display
      canvas.parsesvg(m.group, svg_path);   
      setsize(m.elev_profile,m.elev_pts);

      ### Create empty geo.Coord object for waypoints calculation
      m.wpt_this = geo.Coord.new();
      m.wpt_next = geo.Coord.new();
      m.wpt_tod = geo.Coord.new();

      ### Display init ###
      m.h_range = getprop(set_range);
      m.group.getElementById("text_range1").setText(sprintf("%3.0f",m.h_range*0.25));
      m.group.getElementById("text_range2").setText(sprintf("%3.0f",m.h_range*0.5));
      m.group.getElementById("text_range3").setText(sprintf("%3.0f",m.h_range*0.75));
      m.group.getElementById("text_range4").setText(sprintf("%3.0f",m.h_range));
      m.group.getElementById("tgt_altitude").setText(sprintf("%5.0f",getprop(tg_alt)));

      ### Variables ###
      m.fp = flightplan();
      m.alt_set = getprop(tg_alt);
      m.lastWp = 0;
      m.lastWp_alt = 0;
      m.lastWp_dist = 0;
      m.prevWp_alt = 0;
      m.prevWp_dist = 0;
      m.v_alt = nil;

      return m;
   }, # end of new

         ### Listeners ###
   listen : func {      
      setlistener(fp_active, func(n) {
         if (n.getValue()) {
            me.fp = flightplan();
            me.tot_dist = getprop(totDist);
            me.update();
            me.v_alt = fms.vsd_alt(); # Call altitudes vector from fms
         }
      },0,0);

      setlistener(set_range, func(n) {
         me.range = n.getValue();
         if(me.range > 10) {
            me.group.getElementById("text_range1").setText(sprintf("%3.0f",me.range*0.25));
            me.group.getElementById("text_range2").setText(sprintf("%3.0f",me.range*0.5));
            me.group.getElementById("text_range3").setText(sprintf("%3.0f",me.range*0.75));
            me.group.getElementById("text_range4").setText(sprintf("%3.0f",me.range));
         } else {
            me.group.getElementById("text_range1").setText(sprintf("%1.1f",me.range*0.25));
            me.group.getElementById("text_range2").setText(sprintf("%1.0f",me.range*0.5));
            me.group.getElementById("text_range3").setText(sprintf("%1.1f",me.range*0.75));
            me.group.getElementById("text_range4").setText(sprintf("%1.0f",me.range));
         }
      },0,0);

      setlistener(tg_alt, func(n) {
         me.alt_set = n.getValue();
         me.group.getElementById("tgt_altitude").setText(sprintf("%5.0f",me.alt_set));
         me.newSetPos = -me.alt_ceil_px*(me.alt_set/me.alt_ceil);
         me.group.getElementById("altitude_set").setTranslation(0,me.newSetPos-me.lastaltset);
         me.lastaltset = me.newSetPos;
      },0,0);

   }, # end of listen

   update: func {
      if(getprop(toggle_vsd) == 1) {
         # Generate elevation profile      
         me.altitude = getprop(alt_ind);
         if(me.altitude == nil) {
            me.altitude = 0;
         }
         me.alt_ceil = (getprop(asel)*100 == 0 ? 1 :getprop(asel)*100);

         me.new_markerPos = -me.alt_ceil_px*(me.altitude/me.alt_ceil);

         # Vertical Flight Path
            me.numWpts = me.fp.getPlanSize();
            me.currWpt = me.fp.current;
            if (me.numWpts > 1 and me.currWpt >= 0) {
               me.path.del();
               me.text.removeAllChildren();
               me.path = me.group.createChild("path");
               me.path.setColor(me.path_color)
                      .moveTo(me.bottom_left.x,me.bottom_left.y-4+me.new_markerPos)
                      .setStrokeLineWidth(2)
                      .show();
               me.wpt_this.set_latlon(me.fp.getWP(me.currWpt).lat, me.fp.getWP(me.currWpt).lon);
               me.rteLen = geo.aircraft_position().distance_to(me.wpt_this)*M2NM;
               brk_next = 0;

               # Calculate distance between waypoints
               for(var i=me.currWpt; i<me.numWpts; i=i+1) {
                  if (i == 0) {alt = getprop(dep_alt)}
                  else if (i == me.numWpts-1) {alt = getprop(dest_alt)}
                  else {
                     if (me.v_alt != nil) { # plan not activated
                     #### BASIC ###
                        if(me.fp.getWP(i).wp_type == "basic" and me.fp.getWP(i).wp_role == nil) {

                           ### SIDS ###
                           if (me.fp.getWP(i).distance_along_route < me.tot_dist/2) {
                              if (me.v_alt.vector[i] <= 0) {
                                 alt = getprop(asel)*100;
                              } else {
                                 alt = me.v_alt.vector[i];
                              }
                           } else {

                           ### STARS ###
                              alt = me.v_alt.vector[i];                        
                           }
                        } else {

                        ### NAVAIDS ###
                              alt = me.v_alt.vector[i];
                        }
                     }
                  }
                  if(me.rteLen > me.range) {brk_next = 1}
                  me.path.lineTo(me.bottom_left.x + me.max_range_px*(me.rteLen/me.range), me.bottom_left.y - me.alt_ceil_px*(alt/me.alt_ceil));

                  # Add waypoint ident
                  if (me.fp.getWP(i).wp_name == 'TOD') {
                     color = me.tod_color;
                     text = "TOD";
                  } else {
                     color = me.path_color;
                     text = me.fp.getWP(i).id;
                  }

                  me.text.createChild("text")
                         .setAlignment("left-bottom")
                         .setColor(color)
                         .setFontSize(24,1.0)
                         .setTranslation(me.bottom_left.x + 10 + me.max_range_px*(me.rteLen/me.range), me.bottom_left.y - 10 - me.alt_ceil_px*(alt/me.alt_ceil))
                         .setText(text);

                  me.text.createChild("text")
                         .setAlignment("center-center")
                         .setColor(color)
                         .setFontSize(28,1.2)
                         .setTranslation(me.bottom_left.x + me.max_range_px*(me.rteLen/me.range), me.bottom_left.y - me.alt_ceil_px*(alt/me.alt_ceil))
                         .setText("*");

                  if(i<me.numWpts-1) {
                     me.wpt_this.set_latlon(me.fp.getWP(i).lat, me.fp.getWP(i).lon);
                     me.wpt_next.set_latlon(me.fp.getWP(i+1).lat, me.fp.getWP(i+1).lon);
                     append(rangeHdg, {range: me.rteLen, course: me.wpt_this.course_to(me.wpt_next)});
                     me.rteLen = me.rteLen + me.wpt_this.distance_to(me.wpt_next)*M2NM;
                  }
                  if(brk_next == 1) {break}
               }
            } else {
               me.path.hide();
               me.text.removeAllChildren();
            }
   
         pos = geo.aircraft_position();
   
         # Get terrain profile along the flightplan route if WPT is enabled. If WPT is not enabled, the rangeHdg vector should be empty, so it's just going to get the elevation profile along the indicated aircraft heading
      
         me.peak = 0;
         forindex(var j; me.elev_profile) {
            check_hdg = getprop(heading_ind);
            foreach(var wpt; rangeHdg) {
               if(j*(me.range/me.elev_pts) > wpt.range) {
                  check_hdg = wpt.course;
               } else {break}
            }
            pos.apply_course_distance(check_hdg,(me.range/me.elev_pts)*NM2M);
            elev = me.get_elevation(pos.lat(), pos.lon());
            me.elev_profile[j] = elev;
            if(elev > me.peak) {
               me.peak = elev;
            }
         }
         # Set Altitude Numbers
         me.terrain.del();
         me.terrain = me.group.createChild("path");
         me.terrain
               .setColorFill(me.terrain_color)
               .moveTo(me.bottom_left.x,me.bottom_left.y + me.terr_offset);
   
         # Draw Terrain
         forindex(var k; me.elev_profile) {
            me.terrain.lineTo(me.bottom_left.x+(k*(me.max_range_px/(me.elev_pts-1))), me.bottom_left.y -6- me.alt_ceil_px*(me.elev_profile[k]/me.alt_ceil));
         }
            me.terrain.lineTo(me.bottom_left.x+me.max_range_px,me.bottom_left.y+ me.terr_offset);
   
         me.group.getElementById("text_alt1").setText(sprintf("%5.0f",me.alt_ceil/2));
         me.group.getElementById("text_alt2").setText(sprintf("%5.0f",me.alt_ceil));
         me.group.getElementById("aircraft_marker").setTranslation(0,me.new_markerPos-me.lastalt);

         ### Speed Arrow ###
         vs_fps = getprop(vert_spd);
         if(vs_fps == nil) {
            vs_fps = 0;
         }
         gs_fps = getprop("/velocities/groundspeed-kt")*1.46667; # KTS to FPS
         if(gs_fps > 60) {
            fpa = math.atan2(vs_fps, gs_fps);
            me.group.getElementById("speed_arrow")
                     .setTranslation(0,me.new_markerPos-me.lastalt)
                     .setRotation(-fpa)
                     .show();         
         } else {
            me.group.getElementById("speed_arrow").hide();
         }
         me.lastalt = me.new_markerPos;
      } else {
         me.path.hide();
         me.text.removeAllChildren();
      }

      settimer(func me.update(),1);

   }, # end of update

   get_elevation : func (lat, lon) {
      info = geodinfo(lat, lon);
      if (info != nil) {var elevation = info[0] * M2FT;}
      else {elevation = -1.0; }
      return elevation;
   }, # end of get_elevation

}; # end of VSD

### START ###
var vsd_stl = setlistener("sim/signals/fdm-initialized", func {
   var vsd = vsd.new();
   vsd.listen();
   print("VSD ... Ok");
   removelistener(vsd_stl);
},0,0);
<syntaxhighlight>

[[File:Omega95-vsd-instrument.jpeg|200px|thumb|Omega95's VSD instrument]]

== Prerequisites ==
* [[Howto: Create a new Nasal module]]
* [[Howto: Create a new system in Nasal]]
* [[Howto: Create a new instrument in Nasal]]
* [[Howto: Terrain sampling in Nasal]]
* [[Howto: Processing route manager data in Nasal]]
* [[Howto: Working with AI and MP properties]]
* loopid loops, OOP, trigonometry

== ToDo ==
* add XML markup
* add Nasal snippets (one by one)
* upload omega95's screen shots
* link to geo.nas

== Code ==
<syntaxhighlight lang="php">
# placeholder (Nasal)


# placeholder (xml)

External links

  1. clm76  (Sep 23rd, 2017).  Re: Spoken GCA .