Showing 3 changed files with 263 additions and 127 deletions
+8
Nasal/display.nas
... ...
@@ -19,6 +19,11 @@ var displayClass = {
19 19
         m.device = device;
20 20
         m.screenElements = {};
21 21
 
22
+        if (role == 'MFD') {
23
+            m.MFDMapTiles = MapTiles.new(m.display);
24
+            m.MFDMapNavDisplay = MapNavDisplay.new(m.display);
25
+        }
26
+
22 27
         return m;
23 28
     },
24 29
 #}}}
... ...
@@ -193,6 +198,9 @@ var displayClass = {
193 198
             }
194 199
             else {
195 200
                 me.updateEIS();
201
+                me.MFDMapNavDisplay.showMap();
202
+                me.MFDMapTiles.initialize_grid();
203
+                me.MFDMapTiles.update_timer.start();
196 204
             }
197 205
             me._updateRadio({auto:'nav'});
198 206
             me._updateRadio({auto:'comm'});
+236 -126
Nasal/map.nas
... ...
@@ -1,128 +1,238 @@
1
-var mapsRelativePath = '';
2
-var mapsAbsolutePath = '';
3
-var textureExtension = '.png';
4
-var actual_map = '';
5
-var mapRanges = [0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.0];
6
-
7
-var init_map = func {
8
-    var maps = '/zkv1000/maps';
9
-    var root_l = common_l = common_c = 0;
10
-
11
-    var home = string.normpath(getprop('/sim/fg-home'));
12
-    var home_s = size(home);
13
-    
14
-    var root = string.normpath(getprop('/sim/fg-root'));
15
-    var root_s = size(root);
16
-    
17
-    for (var i = 0; i < root_s; i += 1) 
18
-        if (root[i] == `/`)
19
-            root_l += 1;
20
-    
21
-    for (var i = 0; i < root_s and i < home_s; i += 1) {
22
-        (home[i] == root[i]) or break;
23
-        common_c += 1;
24
-        if (home[i] == `/`) common_l += 1;
25
-    }
26
-    
27
-    mapsRelativePath = '../../../../';
28
-    for (var i = 0; i < root_l - common_l; i += 1)
29
-        mapsRelativePath ~= '../';
30
-    
31
-    mapsRelativePath ~= substr(home, common_c, home_s) ~ maps;
32
-    mapsAbsolutePath = home ~ maps;
33
-}
34
-
35
-var moveMap = func (d) {
36
-    atlas_map(d);
37
-    terrain_map(d);
38
-}
39
-
40
-var atlas_map = func (d) {
41
-    if (device[d].status != 2 and device[d].status != 3) return;
42
-
43
-    var mapnode = '/instrumentation/zkv1000/device[' ~ d ~ ']/map/';
44
-    var lat = getprop(mapnode ~ 'latitude-deg');
45
-    var lon = getprop(mapnode ~ 'longitude-deg');
46
-    var map = sprintf('%s%03i%s%02i',
47
-        (lon > 0)? 'e' : 'w',
48
-        (lon > 0)? lon : (abs(lon) + 1),
49
-        (lat > 0)? 'n' : 's',
50
-        (lat > 0)? lat : (abs(lat) + 1)
51
-    );
52
-    if (actual_map == map) {
53
-        setprop(mapnode ~ 'moving-x', (lon > 0)? frac(lon) : 1 - abs(frac(lon)));
54
-        setprop(mapnode ~ 'moving-y', (lat > 0)? frac(lat) : 1 - abs(frac(lat)));
55
-    }
56
-    elsif (io.stat(mapsAbsolutePath ~ '/terrain/' ~ map ~ textureExtension) != nil) {
57
-        actual_map = map;
58
-        setprop(mapnode ~ 'terrain-path', mapsRelativePath ~ '/terrain/' ~ map ~ textureExtension);
59
-        setprop(mapnode ~ 'moving-x', (lon > 0)? frac(lon) : 1 - abs(frac(lon)));
60
-        setprop(mapnode ~ 'moving-y', (lat > 0)? frac(lat) : 1 - abs(frac(lat)));
61
-    }
62
-    else {
63
-        actual_map = '';
64
-        setprop(mapnode ~ 'terrain-path', '');
65
-        setprop(mapnode ~ 'moving-x', 0);
66
-        setprop(mapnode ~ 'moving-y', 0);
67
-    }
68
-    setprop(mapnode ~ 'alt', computeCursorPosition(alt));
69
-    setprop(mapnode ~ 'alt-selected', computeCursorPosition(getprop('/instrumentation/zkv1000/afcs/selected-alt-ft')));
70
-    setprop(mapnode ~ 'alt-1-min', computeCursorPosition(alt + vs));
71
-    setprop(mapnode ~ 'longitude-string', device[d].status == 3 ? 
72
-            sprintf('%04.2f LON', lon) : DMS(lon, lon > 0 ? 'E' : 'W'));
73
-    setprop(mapnode ~ 'latitude-string', device[d].status == 3 ? 
74
-            sprintf('%04.2f LAT', lat) : DMS(lat, lat > 0 ? 'N' : 'S'));
75
-}
76
-
77
-var terrain_map_pos = nil;
78
-var terrain_map_row = 16; # number of rows for terrain radar, equals to number of colums
79
-var terrain_map_hdg = 0;
80
-var terrain_map_alt = 0;
81
-
82
-var terrain_map = func (d) {
83
-# inspired from Ryan M's generic moving map
84
-    if (device[d].status != 4) return;
85
-    var path = '/instrumentation/zkv1000/device[' ~ d ~ ']/map/terrain-elevation/';
86
-    var offset = 0;
87
-    if (terrain_map_row < 15)
88
-        terrain_map_row += 1;
89
-    else {
90
-        terrain_map_pos = geo.aircraft_position();
91
-        terrain_map_hdg = hdg;
92
-        terrain_map_alt = alt;
93
-        terrain_map_row = 0;
94
-    }
95
-    terrain_map_pos.apply_course_distance(terrain_map_hdg - 90, range / 2 * 1852);
96
-    for (var col = 0; col < 16; col += 1) {
97
-        var info = geodinfo(terrain_map_pos.lat(), terrain_map_pos.lon());
98
-        if (info != nil) {
99
-            var d = terrain_map_alt - info[0] * 3.28;
100
-            if (d > 1000) {
101
-                offset = 0 ##black
102
-            }
103
-            elsif (d > -100) {
104
-                offset = 0.25 ##yellow
105
-            }
106
-            else {
107
-                offset = 1.5 ##red
108
-            }
1
+# vim: set foldmethod=marker foldmarker={{{,}}} :
2
+var MapTiles = {
3
+# code from http://wiki.flightgear.org/Canvas_Snippets#A_simple_tile_map
4
+    new : func (display) {
5
+        var m = { parents: [MapTiles] };
6
+        m.display = display;
7
+        m.group = m.display.createGroup();
8
+        m.tile_size = 256;
9
+        m.zoom = 10;
10
+        m.tile_server = getprop('/instrumentation/zkv1000/mfd/online-tiles-server');
11
+        m.type = getprop('/instrumentation/zkv1000/mfd/online-tiles-type');
12
+        m.maps_base = getprop("/sim/fg-home") ~ '/cache/maps';
13
+        m.makeUrl = string.compileTemplate('https://{tile_server}/{type}/{z}/{x}/{y}.png');
14
+        m.makePath = string.compileTemplate(m.maps_base ~ '/{tile_server}/{type}/{z}/{x}/{y}.png');
15
+        m.num_tiles = [
16
+            math.ceil( m.display.get('view[0]') / m.tile_size ) + 1, 
17
+            math.ceil( m.display.get('view[1]') / m.tile_size ) + 1
18
+        ];
19
+        m.center_tile_offset = [
20
+            (m.num_tiles[0] - 1) / 2,
21
+            (m.num_tiles[1] - 1) / 2
22
+        ];
23
+        m.tiles = setsize([], m.num_tiles[0]);
24
+        m.last_tile = [-1,-1];
25
+        m.last_type = m.type;
26
+        m.update_timer = maketimer(2, m, m.updateTiles);
27
+        return m;
28
+    },
29
+
30
+# Simple user interface (Buttons for zoom and label for displaying it)
31
+    changeZoom : func(d) {
32
+        me.zoom = math.max(2, math.min(19, me.zoom + d));
33
+        call(me.updateTiles, [], me);
34
+    },
35
+
36
+# initialize the map by setting up a grid of raster images  
37
+    initialize_grid : func {
38
+        for(var x = 0; x < me.num_tiles[0]; x += 1) {
39
+            me.tiles[x] = setsize([], me.num_tiles[1]);
40
+            for(var y = 0; y < me.num_tiles[1]; y += 1)
41
+                me.tiles[x][y] = me.group.createChild("image", "map-tile");
42
+        }
43
+    },
44
+
45
+# this is the callback that will be regularly called by the timer to update the map
46
+    updateTiles : func() {
47
+        var lat = getprop('/position/latitude-deg');
48
+        var lon = getprop('/position/longitude-deg');
49
+
50
+        var n = math.pow(2, me.zoom);
51
+        var offset = [
52
+            n * ((lon + 180) / 360) - me.center_tile_offset[0],
53
+            (1 - math.ln(math.tan(lat * math.pi/180) + 1 / math.cos(lat * math.pi/180)) / math.pi) / 2 * n - me.center_tile_offset[1]
54
+        ];
55
+        var tile_index = [int(offset[0]), int(offset[1])];
56
+
57
+        var ox = tile_index[0] - offset[0];
58
+        var oy = tile_index[1] - offset[1];
59
+
60
+        for(var x = 0; x < me.num_tiles[0]; x += 1)
61
+            for(var y = 0; y < me.num_tiles[1]; y += 1)
62
+                me.tiles[x][y]
63
+                    .setTranslation(int((ox + x) * me.tile_size + 0.5), int((oy + y) * me.tile_size + 0.5));
64
+
65
+        if(    tile_index[0] != me.last_tile[0]
66
+                or tile_index[1] != me.last_tile[1]
67
+                or me.type != me.last_type ) {
68
+            for(var x = 0; x < me.num_tiles[0]; x += 1)
69
+                for(var y = 0; y < me.num_tiles[1]; y += 1) {
70
+                    var pos = {
71
+                        z: me.zoom,
72
+                        x: int(offset[0] + x),
73
+                        y: int(offset[1] + y),
74
+                        type: me.type,
75
+                        tile_server : me.tile_server,
76
+                    };
77
+
78
+                    (func {
79
+                        var img_path = me.makePath(pos);
80
+                        printlog('debug', 'img_path: ', img_path);
81
+                        var tile = me.tiles[x][y];
82
+
83
+                        if( io.stat(img_path) == nil ) { # image not found, save in $FG_HOME
84
+                            var img_url = me.makeUrl(pos);
85
+                            printlog('debug', 'requesting ' ~ img_url);
86
+                            http.save(img_url, img_path)
87
+                                .done(func {printlog('info', 'received image ' ~ img_path); tile.set("src", img_path);})
88
+                                .fail(func (r) printlog('warn', 'Failed to get image ' ~ img_path ~ ' ' ~ r.status ~ ': ' ~ r.reason));
89
+                        }
90
+                        else { # cached image found, reusing
91
+                            printlog('debug', 'loading ' ~ img_path);
92
+                            tile.set("src", img_path);
93
+                        }
94
+                    })();
95
+                }
96
+            me.last_tile = tile_index;
97
+            me.last_type = me.type;
98
+        }
99
+    },
100
+
101
+    del : func() {
102
+        me.update_timer.stop();
103
+        call(canvas.Window.del, [], me);
104
+    },
105
+};
106
+
107
+var MapNavDisplay = {
108
+    new : func (display) {
109
+        var m = { parents: [MapNavDisplay] };
110
+        m.display = display;
111
+        m.map = m.display.createGroup().createChild('map');
112
+        m.ctrl_ns = canvas.Map.Controller.get("Aircraft position");
113
+        m.ctrl_ns.SOURCES["map-dialog"] = {
114
+            getPosition: func subvec(geo.aircraft_position().latlon(), 0, 2),
115
+            getAltitude: func getprop('/position/altitude-ft'),
116
+            getHeading:  func {
117
+                if (me.aircraft_heading)
118
+                    getprop('/orientation/heading-deg')
119
+                else 0
120
+            },
121
+            aircraft_heading: 1,
122
+        };
123
+
124
+        m.Styles = {
125
+            get : func(type) return m.Styles[type],
126
+        };
127
+
128
+        m.Options = {
129
+            get : func(type) return m.Options[type],
130
+        };
131
+
132
+        m.listeners = [];
133
+
134
+        return m;
135
+    },
136
+
137
+    showMap : func {
138
+        me.setMap();
139
+        me.setStyles();
140
+        me.setOptions();
141
+        me.refresh();
142
+    },
143
+
144
+    setMap : func {
145
+        var source = me.ctrl_ns.SOURCES["map-dialog"];
146
+        me.map.setController("Aircraft position", "map-dialog");
147
+        me.map.setRange(40);
148
+        me.map.setTranslation(
149
+            me.display.get("view[0]")/2,
150
+            me.display.get("view[1]")/2
151
+        );
152
+    },
153
+
154
+    setStyles : func {
155
+## set up a few keys supported by the DME.symbol file to customize appearance: {{{
156
+        me.Styles.DME = {};
157
+        me.Styles.DME.debug = 1; # HACK for benchmarking/debugging purposes
158
+        me.Styles.DME.animation_test = 0; # for prototyping animated symbols
159
+
160
+        me.Styles.DME.scale_factor = 0.4; # 40% (applied to whole group)
161
+        me.Styles.DME.line_width = 3.0;
162
+        me.Styles.DME.color_tuned = [0,1,0]; #rgb
163
+        me.Styles.DME.color_default = [1,1,0];  #rgb
164
+
165
+        me.Styles.APT = {};
166
+        me.Styles.APT.scale_factor = 0.4; # 40% (applied to whole group)
167
+        me.Styles.APT.line_width = 3.0;
168
+        me.Styles.APT.color_default = [0,0.6,0.85];  #rgb
169
+        me.Styles.APT.label_font_color = me.Styles.APT.color_default;
170
+        me.Styles.APT.label_font_size=16;
171
+
172
+        me.Styles.TFC = {};
173
+        me.Styles.TFC.scale_factor = 0.4; # 40% (applied to whole group)
174
+
175
+        me.Styles.WPT = {};
176
+        me.Styles.WPT.scale_factor = 0.5; # 50% (applied to whole group)
177
+
178
+        me.Styles.RTE = {};
179
+        me.Styles.RTE.line_width = 2;
180
+
181
+        me.Styles.FLT = {};
182
+        me.Styles.FLT.line_width = 3;
183
+
184
+        me.Styles.FIX = {};
185
+        me.Styles.FIX.color = [1,0,0];
186
+        me.Styles.FIX.scale_factor = 0.4; # 40%
187
+
188
+        me.Styles.VOR = {};
189
+        me.Styles.VOR.range_line_width = 2;
190
+        me.Styles.VOR.radial_line_width = 1;
191
+        me.Styles.VOR.scale_factor = 0.6; # 60%
192
+
193
+        me.Styles.APS = {};
194
+        me.Styles.APS.scale_factor = 0.5;
195
+    },
196
+#}}}
197
+
198
+    setOptions : func {
199
+        me.Options.FLT = {};
200
+    },
201
+
202
+    make_update_wrapper : func(name) {
203
+        if (!contains(me.Options, name)) me.Options[name] = {};
204
+        me.Options[name].update_wrapper = func(layer, fn) {
205
+            fn();
109 206
         }
110
-        else {
111
-            offset = 0.75 ##no available data
207
+    },
208
+
209
+    ToggleLayerVisible : func(name) {
210
+        (var l = me.map.getLayer(name)).setVisible(l.getVisible());
211
+    },
212
+
213
+    SetLayerVisible : func(name,n=1) {
214
+        me.map.getLayer(name).setVisible(n);
215
+    },
216
+
217
+    refresh : func {
218
+        var r = func(name,vis=1,zindex=nil) return caller(0)[0];
219
+        # TODO: we'll need some z-indexing here, right now it's just random
220
+        foreach(var type; [r('TFC',0),r('APT'),r('DME'),r('VOR'),r('NDB'),r('FIX',0),r('RTE'),r('WPT'),r('FLT'),r('WXR',0),r('APS'), ] ) {
221
+            if (1 and type.name != 'APS' and type.name != 'FLT') me.make_update_wrapper(type.name);
222
+            me.map.addLayer(factory: canvas.SymbolLayer, type_arg: type.name,
223
+                    visible: type.vis, priority: type.zindex,
224
+                    style: me.Styles.get(type.name),
225
+                    options: me.Options.get(type.name) 
226
+                    );
227
+            (func {
228
+                 # Notify MapStructure about layer visibility changes:
229
+                 var name = type.name;
230
+                 props.globals.initNode("/sim/gui/dialogs/map-canvas/draw-"~name, type.vis, "BOOL");
231
+                 append(me.listeners,
232
+                         setlistener("/sim/gui/dialogs/map-canvas/draw-"~name,
233
+                             func(n) me.SetLayerVisible(name,n.getValue()), 1, 2)
234
+                       );
235
+             })();
112 236
         }
113
-        setprop(path ~ terrain_map_row ~ '-' ~ col, offset);
114
-        terrain_map_pos.apply_course_distance(terrain_map_hdg + 90, range / 16 * 1852);
115
-    }
116
-    terrain_map_pos.apply_course_distance(terrain_map_hdg, range / 16 * 1852);
117
-}
118
-
119
-var computeCursorPosition = func (v) {
120
-    var ref = round_bis(v, (v < 3281)? 820 : 1640);
121
-    if (ref < 4921) ref /= 820;
122
-    elsif (ref > 21325) ref = 15;
123
-    else {
124
-        ref /= 1640;
125
-        ref += 2;
126
-    }
127
-    return ref;
128
-}
237
+    },
238
+};
+19 -1
zkv1000.nas
... ...
@@ -6,6 +6,7 @@ files_to_load = [
6 6
     'knobs.nas',   # handles knobs
7 7
     'buttons.nas', # handles knobs and buttons
8 8
     'softkeys.nas',# handles softkeys and menus items
9
+    'map.nas',     # moves the maps
9 10
     'display.nas',
10 11
     'core.nas',    # the scheduler and switch on button
11 12
 ];
... ...
@@ -14,7 +15,6 @@ files_to_load = [
14 15
 #    'routes.nas',  # manages flightplans and routes
15 16
 #    'display.nas', # display messages and popups
16 17
 #    'infos.nas',   # get informations from environment
17
-#    'map.nas',     # moves the maps
18 18
 #    'alerts.nas',  # in flight tests
19 19
 #    'mud.nas',     # displays simple embedded GUI (Multi-Use Display)
20 20
 
... ...
@@ -87,6 +87,24 @@ var init_props = func {
87 87
         getprop('/consumables/fuel/tank/level-gal_us')
88 88
       + getprop('/consumables/fuel/tank/level-gal_us'));
89 89
 
90
+    setprop('/sim/gui/dialogs/map-canvas/draw-APT', 1);
91
+    setprop('/sim/gui/dialogs/map-canvas/draw-FIX', 1);
92
+    setprop('/sim/gui/dialogs/map-canvas/draw-VOR', 1);
93
+    setprop('/sim/gui/dialogs/map-canvas/draw-DME', 1);
94
+    setprop('/sim/gui/dialogs/map-canvas/draw-NDB', 1);
95
+    setprop('/sim/gui/dialogs/map-canvas/draw-TFC', 1);
96
+    setprop('/sim/gui/dialogs/map-canvas/draw-data', 1);
97
+    setprop('/sim/gui/dialogs/map-canvas/draw-FLT', 1);
98
+    setprop('/sim/gui/dialogs/map-canvas/draw-WXR', 1);
99
+    setprop('/sim/gui/dialogs/map-canvas/draw-TUT', 1);
100
+    setprop('/sim/gui/dialogs/map-canvas/aircraft-heading-up', 0);
101
+    mfd = zkv.getNode('mfd', 1);
102
+    var tiles_server = getprop('/sim/online-tiles-server');
103
+    var tiles_type = getprop('/sim/online-tiles-type');
104
+    # see https://www.mediawiki.org/wiki/Maps
105
+    mfd.getNode('online-tiles-server', 1).setValue(tiles_server != nil ? tiles_server : 'maps.mediawiki.org');
106
+    mfd.getNode('online-tiles-type', 1).setValue(tiles_type != nil ? tiles_type : 'osm-intl');
107
+
90 108
     props.globals.getNode('/instrumentation/transponder/id-code',1).setIntValue(1200);
91 109
     props.globals.getNode('/instrumentation/transponder/serviceable',1).setBoolValue(1);
92 110
     props.globals.getNode('/autopilot/settings/heading-bug-deg', 1).alias('/instrumentation/zkv1000/afcs/heading-bug-deg');