Board index FlightGear Development Canvas

Using a canvas map in the GUI

Canvas is FlightGear's new fully scriptable 2D drawing system that will allow you to easily create new instruments, HUDs and even GUI dialogs and custom GUI widgets, without having to write C++ code and without having to rebuild FlightGear.

Using a canvas map in the GUI

Postby stuart » Mon Sep 17, 2012 3:23 pm

Hi TheTom,

I've been playing around with the currently available canvas code for maps. I've improved the Select Airport dialog so that it includes a map of the selected airport, which looks pretty neat.

Image

Any objection to me committing it?

Also, it would be very handy to be able to navigate through all the children of a Group - not just retrieve them by name. I want to be able to highlight the selected runway. While I can do that fairly easily, it's difficult to "un-highlight" any previously selected runway without an external data model.

-Stuart
Last edited by stuart on Mon Sep 17, 2012 3:28 pm, edited 1 time in total.
Reason: Add image
G-MWLX
User avatar
stuart
Moderator
 
Posts: 1469
Joined: Wed Nov 29, 2006 9:56 am
Location: Edinburgh
Callsign: G-MWLX

Re: Using a canvas map in the GUI

Postby stuart » Mon Sep 17, 2012 7:24 pm

This is now checked in, if you'd like to take a look.

The code is heavily based on TheTom's replacement for the Map widget, so I doubt it contains any surprises.

Here's a short list of things that have still to be done:
1) Panning with the mouse.
2) Highlighting of the selected Runway/parking position. Thanks to the API changes, this should be straightforward. I'll take a look at that next
3) Addition of pavement/taxiways. This is a WIBNI, as currently the parking positions are just in the green background.
4) Display of aeordrome data on the map. It would be nice to have an overlay with the lat/lon/alt, frequency information displays just like an ICAO aerodrome plate.

-Stuart
G-MWLX
User avatar
stuart
Moderator
 
Posts: 1469
Joined: Wed Nov 29, 2006 9:56 am
Location: Edinburgh
Callsign: G-MWLX

Re: Using a canvas map in the GUI

Postby zakalawe » Wed Sep 19, 2012 12:34 pm

Just to say, I am planning to convert the MapWidget to use this code too, and then the NavDisplay. Support for PNG/texture icons as symbols will be needed for that, and I will also expose the 'route path' which is generated by C++ to Nasal, so a Canvas Path can be created from it.

There's also a long-requested feature to use the replay-code to extract the historical aircraft position, which would be another kind of data to show.

Deciding an API for limiting symbols / databoxes is going to be very important - the NavDisplay and MapWidget already have different solutions for that. In particular there's a notion of symbol priority - basically I sort the available symbols by priority, and the display can be limited to the most important 10/50/100. This is also the approach taken by some real world map displays.

The MapWidget also checks to ensures data-boxes don't overlap where possible - I've no clue how to do that in the current canvas code, however.
zakalawe
 
Posts: 1152
Joined: Sat Jul 19, 2008 4:48 pm
Location: Edinburgh, Scotland
Callsign: G-ZKLW
Version: next
OS: Mac

Re: Using a canvas map in the GUI

Postby Hooray » Wed Sep 19, 2012 12:56 pm

zakalawe wrote in Wed Sep 19, 2012 12:34 pm:Just to say, I am planning to convert the MapWidget to use this code too, and then the NavDisplay.

Yes, we've already been talking about some of this recently, and it should be possible to reuse some of your original code, by generalizing it a little more and making it a part of the core canvas system, so that all canvas "users" can make use of it.

Support for PNG/texture icons as symbols will be needed for that,

Yes, raster image support (CanvasImage) will also be needed to port the existing 2D panel/HUD system at some point, because it is making fairly heavy use of static textures.

and I will also expose the 'route path' which is generated by C++ to Nasal, so a Canvas Path can be created from it.

I am not sure that's needed: the path can be trivially created by via Nasal/Canvas because path-drawing is a supported OpenVG primitive already - so, drawing paths between lat/lon/alt positions is implicitly supported. In other words, the route manager merely needs to expose the waypoint data (which it does already).
What we need to work on, is a vertical projection mode, and a corresponding API - so that things like Omega95's VSD can be more easily expressed using the Canvas system:

Image

There's also a long-requested feature to use the replay-code to extract the historical aircraft position, which would be another kind of data to show.


That's definitely a good idea, accessing the replay/flight recorder buffers via a dedicated Nasal/property tree interface would be pretty cool, not just for showing historical path data, but also for computing a flight profile, or even for doing a full flight evaluation - which could also be done via Nasal/Canvas then, we just need Nasal hooks (i.e. ghosts) to access the replay/flight recorder subsystem and get data from it.

I've been talking to ThorstenB about exposing the system to Nasal a while ago, and when Tom and I played around with graph plotting, we also figured that it would be kinda cool to access such info from Nasal.

The combination of these features (replay/flight recorder access + Nasal + Canvas), would also make it possible to create a full flight instructor console. The original graph-plotting experiments were pretty encouraging:
Image
And 2D plotting is another long-standing feature request, originally brought up by Curt: http://www.mail-archive.com/flightgear- ... 03651.html

curt wrote:But, I was just thinking today that it might be cool to have a built
in grapher for simple / quick graphing needs.

With the property system it would be trivial to pick an arbitrary
property from the property tree and graph it over time -- superimposed
on top of everything else.


With the Canvas, it has now become trivial to create such graphs, but we still need to have Nasal-space access to the corresponding subsystems and data, i.e. the replay/flight recorder system.



Deciding an API for limiting symbols / databoxes is going to be very important - the NavDisplay and MapWidget already have different solutions for that. In particular there's a notion of symbol priority - basically I sort the available symbols by priority, and the display can be limited to the most important 10/50/100. This is also the approach taken by some real world map displays.

Yes, priority/precedence is important and can simplify things - but we could just as well look (again) at ARINC 661 and borrow some concepts there, instead of re-inventing the wheel.

The MapWidget also checks to ensures data-boxes don't overlap where possible - I've no clue how to do that in the current canvas code, however.

Like we mentioned earlier, the C++ code of the Canvas system would need to be extended such that it also computes bounding box dimensions for each drawable (or just OSGText labels) and writes that info to the tree, then it would be possible for Nasal scripts to find overlapping nodes, and even adjust label placement dynamically.
Tom's OSGText CanvasElement alread computes glyph dimensions, which is also used by the selection code, as can be seen in the demo video:
Please don't send support requests by PM, instead post your questions on the forum so that all users can contribute and benefit
Thanks & all the best,
Hooray
Help write next month's newsletter !
pui2canvas | MapStructure | Canvas Development | Programming resources
Hooray
 
Posts: 11340
Joined: Tue Mar 25, 2008 8:40 am

Re: Using a canvas map in the GUI

Postby zakalawe » Wed Sep 19, 2012 1:26 pm

The route-path does need to be exposed - for something like a hold or DME arc, there is only one waypoint in the route, but the route-path contains a series of line segments showing the actual arc / intercept / procedure turn. This also allows for correct rendering of turn-ancticpated waypoints - in the future.

But it's simply a list of lat,lon pairs so exposing it is really trivial - will get it done in the next few days. The Navcache changes are also in now, so papillion and I will look at porting the ground-radar to the canvas, and supporting the v860 pavement / taxiway primitives in the future.

And yes, CanvasImage will be getting some improvements to support 2D panels via the Canvas for sure.
zakalawe
 
Posts: 1152
Joined: Sat Jul 19, 2008 4:48 pm
Location: Edinburgh, Scotland
Callsign: G-ZKLW
Version: next
OS: Mac

Re: Using a canvas map in the GUI

Postby Hooray » Wed Sep 19, 2012 10:06 pm

holy cow ... look at what Stuart just committed - he's covered tons of stuff already while were still talking about it:

Image

PS: We'll need to investigate canvas performance now, the taxiway/airport geometry is causing severe lag while updating on old computers.
Please don't send support requests by PM, instead post your questions on the forum so that all users can contribute and benefit
Thanks & all the best,
Hooray
Help write next month's newsletter !
pui2canvas | MapStructure | Canvas Development | Programming resources
Hooray
 
Posts: 11340
Joined: Tue Mar 25, 2008 8:40 am

Re: Using a canvas map in the GUI

Postby stuart » Thu Sep 20, 2012 12:35 pm

You got there before me :).

Note that the Course information is currently wrong - we're displaying the course from the airport to the current position, where it should be the other way around.

Less obvious is that we now color the runway/taxiway based on the surface definition, so a turf/grass runway is green.

FYI, some of the other things I'm hoping to look at shortly:
- Speeding up taxiway rendering by displaying them all as a single object.
- By default displaying the airports within 100nm, so this dialog can be used to find some nice airports to fly to. That was one of my original motivations for doing this work - I like to do short VFR flights from a random airport without having to spend lots of time planning, so knowing the distance, course and airport information in-sim makes that easier. Not exactly realistic flight-planning, but fun enough.
- Indicating the selected runway direction in the highlight.
- Displaying the highlighted parking position irrespective of the checkbox state
- Only displaying parking positions at reasonable zoom levels
G-MWLX
User avatar
stuart
Moderator
 
Posts: 1469
Joined: Wed Nov 29, 2006 9:56 am
Location: Edinburgh
Callsign: G-MWLX

Re: Using a canvas map in the GUI

Postby Hooray » Thu Sep 20, 2012 12:59 pm

stuart wrote in Thu Sep 20, 2012 12:35 pm:- Speeding up taxiway rendering by displaying them all as a single object.

Yes, that sounds like a good idea - keep in mind though, that the original canvas demo (that the new Nasal code is based on), was still fairly simple and is handling some things in a fairly inefficient manner. In other words, airport features that can be individually toggled, really don't belong into the same canvas "group", so that the canvas doesn't need to delete/update vector data, but can simply hide/show (enable/disable) a canvas group once a checkbox is de/selected.

Whenever something can be toggled via a property (no matter if it's a GUI checkbox or a cockpit hot spot), it should really go into a separate canvas group (i.e. "layer") so that the code can be implemented in a better fashion.
Also, we may want to look into having a "lazy" property to stop updating textures/groups that were not modified, and simply use the cached/previous texture again - in cases like this one, where the scene is pretty static, that would save a lot of unnecessary updating (just look at the performance monitor when viewing KNUQ).

At the moment, there are several issues:
1) writing lots of paths to the canvas (500-1200+ nodes)
2) writing them all to the same, single, group, so that ALL paths are completely updated/redrawn whenever a feature is enabled or disabled
3) updating/redrawing the groups/texture even though nothing may have changed

So, it's not just instantiation that takes a while currently - but also runtime updating of the instantiated "map", which can also be seen when zooming the map, i.e. the delays caused then.

I've talked to Tom about adding a handful of SGTimeStamp statements to the canvas, so that each canvas/group or element exposes exactly how long it took to update things. That would also allow us to come up with a performance dialog for canvases, so that we know exactly where time is burnt (what canvas, what group).
That should be a fairly simple but useful addition, and I can provide the patches for this.

- By default displaying the airports within 100nm, so this dialog can be used to find some nice airports to fly to. That was one of my original motivations for doing this work - I like to do short VFR flights from a random airport without having to spend lots of time planning, so knowing the distance, course and airport information in-sim makes that easier. Not exactly realistic flight-planning, but fun enough.


I am not sure if that should go into the same airport selection dialog? Wouldn't a separate "flight planning" dialog be more appropriate?
That said, it shouldn't matter at all - because the underlying infrastructure would need to be implemented in the map.nas module.

At the moment, there are still some things in the airport selection dialog that we should probably move to the map.nas module, i.e. methods for zooming, highlighting, map-centering etc - these would all be shared requirements, which will also be needed once the map dialog and the navdisplay get ported.



- Indicating the selected runway direction in the highlight.

- Displaying the highlighted parking position irrespective of the checkbox state
- Only displaying parking positions at reasonable zoom levels

These things would probably be better dealt with by introducing a possibility to register custom Nasal callbacks?
Please don't send support requests by PM, instead post your questions on the forum so that all users can contribute and benefit
Thanks & all the best,
Hooray
Help write next month's newsletter !
pui2canvas | MapStructure | Canvas Development | Programming resources
Hooray
 
Posts: 11340
Joined: Tue Mar 25, 2008 8:40 am

Re: Using a canvas map in the GUI

Postby Hooray » Fri Sep 21, 2012 2:52 am

I have now rebased some of the changes that I have been working on, i.e. some code got shuffled around, some new classes and methods added - the underlying idea is to prepare a real OOP design, where we have maps that consist of layers, which in turn contain elements. Elements can be implemented as file names to raster/vector images,or simply as Nasal callbacks. All, neatly mapped to the corresponding canvas primitives, so that individual feature layers can be toggled, without having to redraw all vector data.

At the moment, I have just copied the original code and moved it to separate functions, so that it will be easier to turn those into hashes, and methods later on. So far,, the priority has been not to introduce any breakage.

The new design should satisfy most of our needs i.e. related to porting the MapWidget or the NavDisplay, but also showing multiple airports or adding the same "widget" to the route manager for route planning purposes.

While this is still work in progress, I'd still like to share it now, because there are currently several people working with the code, and personally, I'd rather not see even more "spaghetti" code ;-) added to the GUI XML files, while we should really be working on a real component ;-)

Feedback/improvements appreciated!

Code: Select all
diff --git a/Nasal/canvas/api.nas b/Nasal/canvas/api.nas
index 9c8c4b2..dedc363 100644
--- a/Nasal/canvas/api.nas
+++ b/Nasal/canvas/api.nas
@@ -736,7 +736,7 @@ var Image = {
 # Element factories used by #Group elements to create children
 Group._element_factories = {
   "group": Group.new,
-  "map": Map.new,
+  "map": Map.new, 
   "text": Text.new,
   "path": Path.new,
   "image": Image.new
diff --git a/Nasal/canvas/map.nas b/Nasal/canvas/map.nas
index 8aaeab8..a8d8891 100644
--- a/Nasal/canvas/map.nas
+++ b/Nasal/canvas/map.nas
@@ -1,49 +1,65 @@
-
-# Mapping from surface codes to
-var SURFACECOLORS = {
-  1 : { type: "asphalt",  r:0.2,  g:0.2, b:0.2 },
-  2 : { type: "concrete", r:0.3,  g:0.3, b:0.3 },
-  3 : { type: "turf",     r:0.2,  g:0.5, b:0.2 },
-  4 : { type: "dirt",     r:0.4,  g:0.3, b:0.3 },
-  5 : { type: "gravel",   r:0.35, g:0.3, b:0.3 },
-#  Helipads 
-  6 : { type: "asphalt",  r:0.2,  g:0.2, b:0.2 },
-  7 : { type: "concrete", r:0.3,  g:0.3, b:0.3 },
-  8 : { type: "turf",     r:0.2,  g:0.5, b:0.2 },
-  9 : { type: "dirt",     r:0.4,  g:0.3, b:0.3 },
-  0 : { type: "gravel",   r:0.35, g:0.3, b:0.3 },
-};
-
-# Runway
+var new = func return {parents:arg};
+
+##
+# for runways, navaids, fixes etc
+var LayerElement = {};
+ LayerElement.new = func new(LayerElement);
+ LayerElement.setImplementation = func (imp) {} # either an image filename, or a callback to a custom implementation
+
+##
+# A layer is mapped to a canvas group
+# Layers are linked to a single boolean property to toggle them on/off
+var Layer = {};
+ Layer.new = func new(Layer);
+ Layer.setDrawCallback = func {}
+ Layer.hide = func {me.is_visible=0;} # TODO: hide group
+ Layer.show = func {me.is_visible=1;} # TODO: show group
+ Layer.setToggleProperty = func {}    # TODO: link listener to property to toggle layer visibility
+ Layer.is_visible = func {return me.is_visble;}
+ Layer.del = func {}
+
+##
+# A map consists of several layers
 #
-var Runway = {
-  # Create Runway from hash
-  #
-  # @param rwy  Hash containing runway data as returned from
-  #             airportinfo().runways[ <runway designator> ]
-  new: func(rwy)
-  {
-    return {
-      parents: [Runway],
-      rwy: rwy
-    };
-  },
-  # Get a point on the runway with the given offset
-  #
-  # @param pos  Position along the center line
-  # @param off  Offset perpendicular to the center line
-  pointOffCenterline: func(pos, off = 0)
-  {
-    var coord = geo.Coord.new();
-    coord.set_latlon(me.rwy.lat, me.rwy.lon);
-    coord.apply_course_distance(me.rwy.heading, pos - 0.5 * me.rwy.length);
-
-    if( off )
-      coord.apply_course_distance(me.rwy.heading + 90, off);
-
-    return ["N" ~ coord.lat(), "E" ~ coord.lon()];
-  }
-};
+var MapExt = { ranges:[0.1, 0.25, 0.5, 1, 2.5, 5], zoom_property:nil, listeners:[], update_property:nil,};
+ Map.new2 = func(parent, name) return new(MapExt, parent.createChild("map",name) );
+ Map.listen = func(p,c) {
+   append(me.listeners, setlistener(p, c));
+   }
+
+ Map.update = func {
+  # foreach(var layer; me.get_active_layers() )
+  #   layer.update();
+}
+ Map.addLayer = func(name) me.createChild("group", name);
+ Map.clear = func me.removeAllChildren() and me;
+ Map.redraw = func me.clear() and me.update() and me;
+ Map.setRefPos = func(lat, lon) {
+  me._node.getNode("ref-lat", 1).setDoubleValue(lat);
+  me._node.getNode("ref-lon", 1).setDoubleValue(lon);
+ }
+ Map.setHdg = func(hdg) me._node.getNode("hdg",1).setDoubleValue(hdg);
+
+ Map.updateZoom = func {
+   var z = getprop(me.zoom_property) or 0.00;
+        var zoom = me.ranges[size(me.ranges)-1 - z];
+        me._node.getNode("range", 1).setDoubleValue(zoom);
+   me;
+
+ }
+ # hash arg - extract to me fields: ERR
+ Map.setupZoom = func(ranges, zoom_property) {
+   me.ranges = ranges;
+   me.zoom_property=zoom_property;
+   # print(me.zoom_property); # GC issue ???
+   setlistener(zoom_property, func me.updateZoom() );
+   me;
+   }
+ Map.setZoom = func {}
+ Map.del = func {
+  foreach(var l; me.listeners)
+   removelistener(l);
+ }
 
 # AirportMap
 #
@@ -51,14 +67,20 @@ var AirportMap = {
   # Create AirportMap from hash
   #
   # @param apt   Hash containing airport data as returned from airportinfo()
-  # @param rwy   Whether to display runways (default=1)
-  # @param taxi  Whether to display taxiways (default=1)
-  # @param park  Whether to display parking positions (default = 1)
-  # @param twr   Whether to display tower positions (default = 1)
-  new: func(apt, rwy=1, taxi=1, park=1, twr=1)
+  # @param features   enabled airport features (layers)
+  new: func(apt, features= nil)
   {
+    if (typeof(features)!="hash") { # enable all layers
+   var rwy=1; var taxi=1; var park=1; var twr=1;
+   }
+   else {  # otherwise extract settings from hash
+      var rwy = features.display_runways;
+      var taxi = features.display_taxiways;
+      var park = features.display_parking;
+      var twr = features.display_tower;
+   }
     return {
-      parents: [AirportMap],
+      parents: [Map, AirportMap],
       _apt: apt,
       _display_runways: rwy,
       _display_taxiways: taxi,
@@ -68,186 +90,49 @@ var AirportMap = {
   },
   # Build the graphical representation of the represented airport
   #
-  # @param layer_runways  canvas.Group to attach airport map to
-  build: func(layer_runways)
+  # @param layer_airports  canvas.Group to attach airport map to
+  build: func(layer_airports)
   {
-    var rws_done = {};
+    me.grp_apt = layer_airports.createChild("group", "apt-" ~ me._apt.id);
+    # At the moment, all features are moved to separate functions
+    # Next, we'll want to turn those into real "layer" classes that can be updated easily
+    # For now, this is hopefully better than having a single huge function trying to do everything at once :)
+    #
+    draw_taxiways(me);
+    draw_runways(me);
+    draw_parking(me);
+    draw_tower(me);
+  },
 
-    me.grp_apt = layer_runways.createChild("group", "apt-" ~ me._apt.id);
-   
-    # Taxiways drawn first so the runways and parking positions end up on top.
-    if (me._display_taxiways)
-    {
-      foreach(var taxi; me._apt.taxiways)
-      {
-        var clr = SURFACECOLORS[taxi.surface];
-        if (clr == nil) { clr = SURFACECOLORS[0]};
         
-        var icon_taxi =
-          me.grp_apt.createChild("path", "taxi")
-                    .setStrokeLineWidth(0)
-                    .setColor(clr.r, clr.g, clr.b)
-                    .setColorFill(clr.r, clr.g, clr.b);
-
-        var txi = Runway.new(taxi);
-        var beg1 = txi.pointOffCenterline(0,  0.5 * taxi.width);
-        var beg2 = txi.pointOffCenterline(0, -0.5 * taxi.width);
-        var end1 = txi.pointOffCenterline(taxi.length,  0.5 * taxi.width);
-        var end2 = txi.pointOffCenterline(taxi.length, -0.5 * taxi.width);
-
-        icon_taxi.setDataGeo
-        (
-          [ canvas.Path.VG_MOVE_TO,
-            canvas.Path.VG_LINE_TO,
-            canvas.Path.VG_LINE_TO,
-            canvas.Path.VG_LINE_TO,
-            canvas.Path.VG_CLOSE_PATH ],
-          [ beg1[0], beg1[1],
-            beg2[0], beg2[1],
-            end2[0], end2[1],
-            end1[0], end1[1] ]
-        );
-      }
-    }
-   
-    if (me._display_runways)
-    {
-      foreach(var rw; keys(me._apt.runways))
-      {
-        var is_heli = substr(rw, 0, 1) == "H";
-        var rw_dir = is_heli ? nil : int(substr(rw, 0, 2));
+};
 
-        var rw_rec = "";
-        var thresh_rec = 0;
-        if( rw_dir != nil )
-        {
-          rw_rec = sprintf("%02d", math.mod(rw_dir - 18, 36));
-          if( size(rw) == 3 )
-          {
-            var map_rec = {
-              "R": "L",
-              "L": "R",
-              "C": "C"
-            };
-            rw_rec ~= map_rec[substr(rw, 2)];
+####
+## TODO: turn this into a method:
+
+var updateAirportMap = func(map) {
+            var id = getprop("/sim/gui/dialogs/airports/selected-airport/id");
+         
+            if (id != "") {
+              var apt = airportinfo(id);
+             
+              map.removeAllChildren();
+              var layer_airports = map.addLayer("airports");
+         
+                var airport_features = {   
+                        display_runways: 1, # will not be toggled, i.e. airports always have runways :)
+                        display_taxiways: getprop("/sim/gui/dialogs/airports/display-taxiways"),
+                        display_parking: getprop("/sim/gui/dialogs/airports/display-parking"),
+                        display_tower: getprop("/sim/gui/dialogs/airports/display-tower"),
+                };
+             
+              var airport = canvas.AirportMap.new(apt, airport_features );
+              airport.build(layer_airports);
+
+              map.setRefPos(lat:apt.lat, lon:apt.lon);
+              map.setHdg(0.0);
+              map.updateZoom();       
+            }
           }
 
-          if( rws_done[rw_rec] != nil )
-            continue;
-
-          var rw_rec = me._apt.runways[rw_rec];
-          if( rw_rec != nil )
-            thresh_rec = rw_rec.threshold;
-        }
-
-        rws_done[rw] = 1;
-
-        rw = me._apt.runways[rw];
-       
-        var clr = SURFACECOLORS[rw.surface];
-        if (clr == nil) { clr = SURFACECOLORS[0]};
-       
-        var icon_rw =
-          me.grp_apt.createChild("path", "runway-" ~ rw.id)
-                    .setStrokeLineWidth(0.5)
-                    .setColor(1.0,1.0,1.0)
-                    .setColorFill(clr.r, clr.g, clr.b);
-
-        var rwy = Runway.new(rw);
-        var beg_thr  = rwy.pointOffCenterline(rw.threshold);
-        var beg_thr1 = rwy.pointOffCenterline(rw.threshold,  0.5 * rw.width);
-        var beg_thr2 = rwy.pointOffCenterline(rw.threshold, -0.5 * rw.width);
-        var beg1 = rwy.pointOffCenterline(0,  0.5 * rw.width);
-        var beg2 = rwy.pointOffCenterline(0, -0.5 * rw.width);
-
-        var end_thr  = rwy.pointOffCenterline(rw.length - thresh_rec);
-        var end_thr1 = rwy.pointOffCenterline(rw.length - thresh_rec,  0.5 * rw.width);
-        var end_thr2 = rwy.pointOffCenterline(rw.length - thresh_rec, -0.5 * rw.width);
-        var end1 = rwy.pointOffCenterline(rw.length,  0.5 * rw.width);
-        var end2 = rwy.pointOffCenterline(rw.length, -0.5 * rw.width);
-
-        icon_rw.setDataGeo
-        (
-          [ canvas.Path.VG_MOVE_TO,
-            canvas.Path.VG_LINE_TO,
-            canvas.Path.VG_LINE_TO,
-            canvas.Path.VG_LINE_TO,
-            canvas.Path.VG_CLOSE_PATH ],
-          [ beg1[0], beg1[1],
-            beg2[0], beg2[1],
-            end2[0], end2[1],
-            end1[0], end1[1] ]
-        );
-
-        if( rw.length / rw.width > 3 and !is_heli )
-        {
-          # only runways which are much longer than wide are
-          # real runways, otherwise it's probably a heliport.
-          var icon_cl =
-            me.grp_apt.createChild("path", "centerline")
-                      .setStrokeLineWidth(0.5)
-                      .setColor(1,1,1)
-                      .setStrokeDashArray([15, 10]);
 
-          icon_cl.setDataGeo
-          (
-            [ canvas.Path.VG_MOVE_TO,
-              canvas.Path.VG_LINE_TO ],
-            [ beg_thr[0], beg_thr[1],
-              end_thr[0], end_thr[1] ]
-          );
-
-          var icon_thr =
-            me.grp_apt.createChild("path", "threshold")
-                      .setStrokeLineWidth(1.5)
-                      .setColor(1,1,1);
-
-          icon_thr.setDataGeo
-          (
-            [ canvas.Path.VG_MOVE_TO,
-              canvas.Path.VG_LINE_TO,
-              canvas.Path.VG_MOVE_TO,
-              canvas.Path.VG_LINE_TO ],
-            [ beg_thr1[0], beg_thr1[1],
-              beg_thr2[0], beg_thr2[1],
-              end_thr1[0], end_thr1[1],
-              end_thr2[0], end_thr2[1] ]
-          );
-        }
-      }
-    }
-   
-    if (me._display_parking)
-    {
-      foreach(var park; me._apt.parking())
-      {
-        var icon_park =
-          me.grp_apt.createChild("text", "parking-" ~ park.name)
-                    .setDrawMode( canvas.Text.ALIGNMENT
-                                + canvas.Text.TEXT )
-                    .setText(park.name)
-                    .setFont("LiberationFonts/LiberationMono-Bold.ttf")
-                    .setGeoPosition(park.lat, park.lon)
-                    .setFontSize(15, 1.3);
-      }
-    }
-   
-    if (me._display_tower)
-    {   
-      var icon_tower =
-              me.grp_apt.createChild("path", "tower")
-                 .setStrokeLineWidth(1)
-                 .setScale(1.5)
-                 .setColor(0.2,0.2,1.0)
-                 .moveTo(-3, 0)
-                 .vert(-10)
-                 .line(-3, -10)
-                 .horiz(12)
-                 .line(-3, 10)
-                 .vert(10);
-                 
-      var pos = me._apt.tower();
-      icon_tower.setGeoPosition(pos.lat, pos.lon);               
-    }   
-  }
-};
diff --git a/Nasal/canvas/map_elements.nas b/Nasal/canvas/map_elements.nas
new file mode 100644
index 0000000..2abca2e
--- /dev/null
+++ b/Nasal/canvas/map_elements.nas
@@ -0,0 +1,36 @@
+########################
+## This file will eventually contain implementations for all required map elements, such as callbacks to draw symbols for
+## navaids, fixes, runways, towers etc
+##
+
+# Runway
+#
+var Runway = {
+  # Create Runway from hash
+  #
+  # @param rwy  Hash containing runway data as returned from
+  #             airportinfo().runways[ <runway designator> ]
+  new: func(rwy)
+  {
+    return {
+      parents: [Runway],
+      rwy: rwy
+    };
+  },
+  # Get a point on the runway with the given offset
+  #
+  # @param pos  Position along the center line
+  # @param off  Offset perpendicular to the center line
+  pointOffCenterline: func(pos, off = 0)
+  {
+    var coord = geo.Coord.new();
+    coord.set_latlon(me.rwy.lat, me.rwy.lon);
+    coord.apply_course_distance(me.rwy.heading, pos - 0.5 * me.rwy.length);
+
+    if( off )
+      coord.apply_course_distance(me.rwy.heading + 90, off);
+
+    return ["N" ~ coord.lat(), "E" ~ coord.lon()];
+  }
+};
+
diff --git a/Nasal/canvas/map_layers.nas b/Nasal/canvas/map_layers.nas
new file mode 100644
index 0000000..19d4b3c
--- /dev/null
+++ b/Nasal/canvas/map_layers.nas
@@ -0,0 +1,192 @@
+####
+## map layers (currently just plain functions, but will be turned into classes soon)
+##
+
+var draw_taxiways = func(me) {
+    # Taxiways drawn first so the runways and parking positions end up on top.
+    if (me._display_taxiways)
+    {
+      foreach(var taxi; me._apt.taxiways)
+      {
+        var clr = SURFACECOLORS[taxi.surface];
+        if (clr == nil) { clr = SURFACECOLORS[0]};
+
+        var icon_taxi =
+          me.grp_apt.createChild("path", "taxi")
+                    .setStrokeLineWidth(0)
+                    .setColor(clr.r, clr.g, clr.b)
+                    .setColorFill(clr.r, clr.g, clr.b);
+
+        var txi = Runway.new(taxi);
+        var beg1 = txi.pointOffCenterline(0,  0.5 * taxi.width);
+        var beg2 = txi.pointOffCenterline(0, -0.5 * taxi.width);
+        var end1 = txi.pointOffCenterline(taxi.length,  0.5 * taxi.width);
+        var end2 = txi.pointOffCenterline(taxi.length, -0.5 * taxi.width);
+
+        icon_taxi.setDataGeo
+        (
+          [ canvas.Path.VG_MOVE_TO,
+            canvas.Path.VG_LINE_TO,
+            canvas.Path.VG_LINE_TO,
+            canvas.Path.VG_LINE_TO,
+            canvas.Path.VG_CLOSE_PATH ],
+          [ beg1[0], beg1[1],
+            beg2[0], beg2[1],
+            end2[0], end2[1],
+            end1[0], end1[1] ]
+        );
+      }
+    }
+
+}
+
+var draw_parking = func(me) {
+
+if (me._display_parking)
+    {
+      foreach(var park; me._apt.parking())
+      {
+        var icon_park =
+          me.grp_apt.createChild("text", "parking-" ~ park.name)
+                    .setDrawMode( canvas.Text.ALIGNMENT
+                                + canvas.Text.TEXT )
+                    .setText(park.name)
+                    .setFont("LiberationFonts/LiberationMono-Bold.ttf")
+                    .setGeoPosition(park.lat, park.lon)
+                    .setFontSize(15, 1.3);
+      }
+    }
+
+}
+
+var draw_tower = func (me) {
+  if (me._display_tower)
+    {
+      var icon_tower =
+              me.grp_apt.createChild("path", "tower")
+                 .setStrokeLineWidth(1)
+                 .setScale(1.5)
+                 .setColor(0.2,0.2,1.0)
+                 .moveTo(-3, 0)
+                 .vert(-10)
+                 .line(-3, -10)
+                 .horiz(12)
+                 .line(-3, 10)
+                 .vert(10);
+
+      var pos = me._apt.tower();
+      icon_tower.setGeoPosition(pos.lat, pos.lon);
+    }
+}
+
+var draw_runways = func(me) {
+   var rws_done = {};
+   if (me._display_runways)
+    {
+      foreach(var rw; keys(me._apt.runways))
+      {
+        var is_heli = substr(rw, 0, 1) == "H";
+        var rw_dir = is_heli ? nil : int(substr(rw, 0, 2));
+
+        var rw_rec = "";
+        var thresh_rec = 0;
+        if( rw_dir != nil )
+        {
+          rw_rec = sprintf("%02d", math.mod(rw_dir - 18, 36));
+          if( size(rw) == 3 )
+          {
+            var map_rec = {
+              "R": "L",
+              "L": "R",
+              "C": "C"
+            };
+            rw_rec ~= map_rec[substr(rw, 2)];
+          }
+
+          if( rws_done[rw_rec] != nil )
+            continue;
+
+          var rw_rec = me._apt.runways[rw_rec];
+          if( rw_rec != nil )
+            thresh_rec = rw_rec.threshold;
+        }
+
+        rws_done[rw] = 1;
+
+        rw = me._apt.runways[rw];
+       
+        var clr = SURFACECOLORS[rw.surface];
+        if (clr == nil) { clr = SURFACECOLORS[0]};
+       
+        var icon_rw =
+          me.grp_apt.createChild("path", "runway-" ~ rw.id)
+                    .setStrokeLineWidth(0.5)
+                    .setColor(1.0,1.0,1.0)
+                    .setColorFill(clr.r, clr.g, clr.b);
+
+        var rwy = Runway.new(rw);
+        var beg_thr  = rwy.pointOffCenterline(rw.threshold);
+        var beg_thr1 = rwy.pointOffCenterline(rw.threshold,  0.5 * rw.width);
+        var beg_thr2 = rwy.pointOffCenterline(rw.threshold, -0.5 * rw.width);
+        var beg1 = rwy.pointOffCenterline(0,  0.5 * rw.width);
+        var beg2 = rwy.pointOffCenterline(0, -0.5 * rw.width);
+
+        var end_thr  = rwy.pointOffCenterline(rw.length - thresh_rec);
+        var end_thr1 = rwy.pointOffCenterline(rw.length - thresh_rec,  0.5 * rw.width);
+        var end_thr2 = rwy.pointOffCenterline(rw.length - thresh_rec, -0.5 * rw.width);
+        var end1 = rwy.pointOffCenterline(rw.length,  0.5 * rw.width);
+        var end2 = rwy.pointOffCenterline(rw.length, -0.5 * rw.width);
+
+        icon_rw.setDataGeo
+        (
+          [ canvas.Path.VG_MOVE_TO,
+            canvas.Path.VG_LINE_TO,
+            canvas.Path.VG_LINE_TO,
+            canvas.Path.VG_LINE_TO,
+            canvas.Path.VG_CLOSE_PATH ],
+          [ beg1[0], beg1[1],
+            beg2[0], beg2[1],
+            end2[0], end2[1],
+            end1[0], end1[1] ]
+        );
+
+        if( rw.length / rw.width > 3 and !is_heli )
+        {
+          # only runways which are much longer than wide are
+          # real runways, otherwise it's probably a heliport.
+          var icon_cl =
+            me.grp_apt.createChild("path", "centerline")
+                      .setStrokeLineWidth(0.5)
+                      .setColor(1,1,1)
+                      .setStrokeDashArray([15, 10]);
+
+          icon_cl.setDataGeo
+          (
+            [ canvas.Path.VG_MOVE_TO,
+              canvas.Path.VG_LINE_TO ],
+            [ beg_thr[0], beg_thr[1],
+              end_thr[0], end_thr[1] ]
+          );
+
+          var icon_thr =
+            me.grp_apt.createChild("path", "threshold")
+                      .setStrokeLineWidth(1.5)
+                      .setColor(1,1,1);
+
+          icon_thr.setDataGeo
+          (
+            [ canvas.Path.VG_MOVE_TO,
+              canvas.Path.VG_LINE_TO,
+              canvas.Path.VG_MOVE_TO,
+              canvas.Path.VG_LINE_TO ],
+            [ beg_thr1[0], beg_thr1[1],
+              beg_thr2[0], beg_thr2[1],
+              end_thr1[0], end_thr1[1],
+              end_thr2[0], end_thr2[1] ]
+          );
+        }
+      }
+    }
+
+}
+
diff --git a/Nasal/canvas/map_styling.nas b/Nasal/canvas/map_styling.nas
new file mode 100644
index 0000000..5d08171
--- /dev/null
+++ b/Nasal/canvas/map_styling.nas
@@ -0,0 +1,19 @@
+######
+## styling related stuff
+##
+
+# Mapping from surface codes to
+var SURFACECOLORS = {
+  1 : { type: "asphalt",  r:0.2,  g:0.2, b:0.2 },
+  2 : { type: "concrete", r:0.3,  g:0.3, b:0.3 },
+  3 : { type: "turf",     r:0.2,  g:0.5, b:0.2 },
+  4 : { type: "dirt",     r:0.4,  g:0.3, b:0.3 },
+  5 : { type: "gravel",   r:0.35, g:0.3, b:0.3 },
+#  Helipads 
+  6 : { type: "asphalt",  r:0.2,  g:0.2, b:0.2 },
+  7 : { type: "concrete", r:0.3,  g:0.3, b:0.3 },
+  8 : { type: "turf",     r:0.2,  g:0.5, b:0.2 },
+  9 : { type: "dirt",     r:0.4,  g:0.3, b:0.3 },
+  0 : { type: "gravel",   r:0.35, g:0.3, b:0.3 },
+};
+
diff --git a/gui/dialogs/airports.xml b/gui/dialogs/airports.xml
index 623c93d..392149b 100644
--- a/gui/dialogs/airports.xml
+++ b/gui/dialogs/airports.xml
@@ -455,7 +455,7 @@
       <layout>vbox</layout>
 
       <canvas>
-        <name>map-dialog</name>
+        <name>airport-selection</name>
         <valign>fill</valign>
         <halign>fill</halign>
         <stretch>true</stretch>
@@ -466,54 +466,29 @@
         
         <nasal>
       
-     
         <load><![CDATA[
+     var cleanup = func {
+      foreach (var listener; listeners)
+                    removelistener(listener);     
+      }
+
           var my_canvas = canvas.get(cmdarg());
           my_canvas.setColorBackground(0.2, 0.5, 0.2, 0.5);
 
           var root = my_canvas.createGroup();
+     # the top level map element
+          var map = canvas.Map.new2(parent:root, name:"airport-selection")
+                        .setTranslation(300, 200) # FIXME: move to Map class ctor
+         .setupZoom( ranges: [0.1, 0.25, 0.5, 1, 2.5, 5], zoom_property:"/sim/gui/dialogs/airports/zoom" );
           
-          var map = root.createChild("map", "map-test")
-                        .setTranslation(300, 200);
-         
-          var layer_runways = map.createChild("group", "runways");
+     # a layer for airports
+     var layer_airports = map.addLayer("airports");
 
-          var updateMap = func() {
-            var id = getprop("/sim/gui/dialogs/airports/selected-airport/id");
-         
-            if (id != "") {
-              var apt = airportinfo(id);
-             
-              map.removeAllChildren();
-              layer_runways = map.createChild("group", "runways");
-             
-              var display_taxiways = getprop("/sim/gui/dialogs/airports/display-taxiways");
-              var display_parking  = getprop("/sim/gui/dialogs/airports/display-parking");
-              var display_tower    = getprop("/sim/gui/dialogs/airports/display-tower");
-             
-              var airport = canvas.AirportMap.new(apt, 1, display_taxiways, display_parking, display_tower);
-              airport.build(layer_runways);
-
-              map._node.getNode("ref-lat", 1).setDoubleValue(apt.lat);
-              map._node.getNode("ref-lon", 1).setDoubleValue(apt.lon);
-              map._node.getNode("hdg", 1).setDoubleValue(0.0);
-             
-              updateZoom();
-            }
-          }
-               
-          var ranges = [0.1, 0.25, 0.5, 1, 2.5, 5];
-         
-          var updateZoom = func()
-          {
-            var z = getprop("/sim/gui/dialogs/airports/zoom");
-            if( z == nil )
-              z = 0;
-            var zoom = ranges[4 - z];
-            map._node.getNode("range", 1).setDoubleValue(zoom);
-                   
-          };
-         
+       var updateMap = func canvas.updateAirportMap(map);
+
+
+     ##
+     ## TODO: should be also moved to the layer module, so that individual elements can be picked and "marked"               
           var updateRunwayHighlight = func()
           {
             var selected_rwy = getprop("/sim/gui/dialogs/airports/selected-airport/rwy");
@@ -535,7 +510,7 @@
               }
             }
             
-            foreach (var apt; layer_runways.getChildren()) {
+            foreach (var apt; layer_airports.getChildren()) {
               if (apt.get("id") == "apt-" ~ selected_apt) {
                 foreach (var rwy; apt.getChildren()) {
                   if ((rwy.get("id") == "runway-" ~ selected_rwy) or
@@ -555,7 +530,7 @@
             var selected_parkpos = getprop("/sim/gui/dialogs/airports/selected-airport/parkpos");         
             var selected_apt = getprop("/sim/gui/dialogs/airports/selected-airport/id");
             
-            foreach (var apt; layer_runways.getChildren()) {
+            foreach (var apt; layer_airports.getChildren()) {
               if (apt.get("id") == "apt-" ~ selected_apt) {
                 foreach (var rwy; apt.getChildren()) {
                   if (rwy.get("id") == "parking-" ~ selected_parkpos) {
@@ -576,17 +551,13 @@
           append(listeners, setlistener("/sim/gui/dialogs/airports/display-taxiways", updateMap));
           append(listeners, setlistener("/sim/gui/dialogs/airports/display-parking", updateMap));
           append(listeners, setlistener("/sim/gui/dialogs/airports/display-tower", updateMap));
-          append(listeners, setlistener("/sim/gui/dialogs/airports/zoom", updateZoom));
           
           update_info();
           ]]>
           </load>
           <close><![CDATA[
-            foreach (var listener, listeners)
-            {
-              removelistener(listener);     
-           
-            }         
+      cleanup();
+      map.del();
           ]]>
           </close>
         </nasal>

Please don't send support requests by PM, instead post your questions on the forum so that all users can contribute and benefit
Thanks & all the best,
Hooray
Help write next month's newsletter !
pui2canvas | MapStructure | Canvas Development | Programming resources
Hooray
 
Posts: 11340
Joined: Tue Mar 25, 2008 8:40 am

Re: Using a canvas map in the GUI

Postby stuart » Fri Sep 21, 2012 7:02 pm

Hi Hooray,

I tried applying this patch without success:

Code: Select all
stuart@needle:~/FlightGear/data$ patch -p0 < /tmp/hooray.patch
patching file b/Nasal/canvas/api.nas
patch: **** malformed patch at line 7: Group._element_factories = {



I can't see anything wrong with the patch by inspection.

Can you email me the original files please, or submit a merge request via git.

-Stuart
G-MWLX
User avatar
stuart
Moderator
 
Posts: 1469
Joined: Wed Nov 29, 2006 9:56 am
Location: Edinburgh
Callsign: G-MWLX

Re: Using a canvas map in the GUI

Postby Hooray » Fri Sep 21, 2012 7:41 pm

I talked to Tom, and he suggested that you should merge your own work first, so that I can rebase things afterwards.
In the meantime, I have made some more progress - so that the original patch is already "outdated".
Thus, I'd suggest you just push your own changes, and I'll port/rebase things as necessary and then ask Tom to review/merge and push things to git (I cannot currently use gitorious unfortunately).

That said, I'd ask James to please wait with his own work related to porting the MapWidget/NavDisplay stuff, I have now about 60% of the OOP infrastructure in place to make things much easier with regards to supporting maps with layers and "elements", while some stuff is still empty/boilerplate, the essential stuff is there already.
Please don't send support requests by PM, instead post your questions on the forum so that all users can contribute and benefit
Thanks & all the best,
Hooray
Help write next month's newsletter !
pui2canvas | MapStructure | Canvas Development | Programming resources
Hooray
 
Posts: 11340
Joined: Tue Mar 25, 2008 8:40 am

Re: Using a canvas map in the GUI

Postby stuart » Fri Sep 21, 2012 7:47 pm

Actually, I had to rebuild FG from scratch, so I applied the patch manually. If (and that's a big IF given I had to do this by hand) it works, I'll commit it.

-Stuart
G-MWLX
User avatar
stuart
Moderator
 
Posts: 1469
Joined: Wed Nov 29, 2006 9:56 am
Location: Edinburgh
Callsign: G-MWLX

Re: Using a canvas map in the GUI

Postby Hooray » Fri Sep 21, 2012 8:04 pm

If you have already finished your own work, you should probably just push it.
If you haven't completed it yet, you could just as well base your own work on my patch.
Overall, if this is "only" about optimizing the way taxiways are drawn, then, you'll really just need to touch a single file/function with the patch.
Which would it make a little easier for me, when rebasing my own work.

Note that some of it may look weird at first, but that's because I tried to automate refactoring a little by using sed/awk.
In the meantime, things have been edited manually and look slightly less confusing now :-)
Please don't send support requests by PM, instead post your questions on the forum so that all users can contribute and benefit
Thanks & all the best,
Hooray
Help write next month's newsletter !
pui2canvas | MapStructure | Canvas Development | Programming resources
Hooray
 
Posts: 11340
Joined: Tue Mar 25, 2008 8:40 am

Re: Using a canvas map in the GUI

Postby Hooray » Sat Sep 22, 2012 5:05 am

stuart wrote in Thu Sep 20, 2012 12:35 pm:- Speeding up taxiway rendering by displaying them all as a single object.

With the latest changes in git, the performance is already pretty good for "static" maps. Zooming is a different matter, and so is panning.
But that can probably be addressed during the next weeks, I think we have discussed some pretty good ideas already.

- By default displaying the airports within 100nm, so this dialog can be used to find some nice airports to fly to. That was one of my original motivations for doing this work - I like to do short VFR flights from a random airport without having to spend lots of time planning, so knowing the distance, course and airport information in-sim makes that easier. Not exactly realistic flight-planning, but fun enough.


Supporting multiple airports didn't seem too difficult after all the refactoring that I had done already, and it will also be needed for all the other efforts (mapwidget, navdisplay, route manager) so I gave it a quick try:
Image
This uses now multiple canvas groups to emulate layers, so that groups can be individually toggled, in other words toggling the tower view, no longer updates the the whole tree and doesn't redraw taxiways/parkings etc anymore.
OOP-wise it's still not quite there yet, because I didn't want to rewrite all the original code, so I just put some wrappers around the original code.


- Indicating the selected runway direction in the highlight.
- Displaying the highlighted parking position irrespective of the checkbox state


My original refactorings sort of "broke" this ... I have now fixed the runway highlighting, but I am not sure if we should even use this approach at all?

The canvas system is prepared for multiple addressing modes, currently "by-index" is the default. But like you say, without a proper data model, by-index addressing is pretty poor.
Personally, I'd suggest to use "by-name (by-id)" - and then just look up "/by-name/map/ksfo/28R" or something like that - we previously talked about that, and I don't that it would be complicated to support this, Tom?


Just let me know how you want to proceed to merge my changes, if anybody else has worked on the Nasal, just post your patches here, so that I can merge them beforehand - because I have touched tons of places ...
Also, I'll probably need to clean up the code quite a bit - otherwise, just keep in mind that we want the API to evolve like this, so nothing is set in stone, and we are only just coming up with the requirements, so many things I added are probably like to be changed or even removed anyhow, so please don't worry about the amount of code. In the long run, I hope to provide a full OOP interface (like the rest of the canvas), but with a MVC design for different types of "layered maps".

That should make it very easy to add new layers (e.g. multiplayer, ai traffic, navaids etc) , so that arbitrary maps can be scripted with just a handful of Nasal code.
Thus, I am hoping that we can collect all code and clean up/refine things later on.

Ideally, all of us could now work on different dialogs (i.e. airport selection, route manager, map widget, navdisplay) - so that the backend code gets improved for different use cases.
Please don't send support requests by PM, instead post your questions on the forum so that all users can contribute and benefit
Thanks & all the best,
Hooray
Help write next month's newsletter !
pui2canvas | MapStructure | Canvas Development | Programming resources
Hooray
 
Posts: 11340
Joined: Tue Mar 25, 2008 8:40 am

Re: Using a canvas map in the GUI

Postby stuart » Sat Sep 22, 2012 6:58 am

I had to upgrade to Ubuntu 12.04 last night so didn't manage to test the new code or commit anything. If you could either provide the modified files or a Git merge request that would before reliable than applying a patch.

Regarding highlighting, I think a better approach will simply be to display an aircraft icon at the appropriate location. That will allow us to indicate runway direction and neatly sidesteps the whole issue of modifying an existing layer. We will want an aircraft layer for the moving map anyway.

-Stuart
G-MWLX
User avatar
stuart
Moderator
 
Posts: 1469
Joined: Wed Nov 29, 2006 9:56 am
Location: Edinburgh
Callsign: G-MWLX

Next

Return to Canvas

Who is online

Users browsing this forum: No registered users and 0 guests