Showing 5 changed files with 639 additions and 642 deletions
+10 -636
Nasal/map.nas
... ...
@@ -1,6 +1,4 @@
1
-# vim: set foldmethod=marker foldmarker={{{,}}} :
2 1
 var mapClass = {
3
-# main class for maps {{{
4 2
     new : func (device) {
5 3
         var m = { parents : [ mapClass ] };
6 4
         m.device = device;
... ...
@@ -20,6 +18,8 @@ var mapClass = {
20 18
             airplane: data.hdg,
21 19
         };
22 20
         m.changeZoom();
21
+
22
+        m.visibility = m.device.role == 'MFD';
23 23
         
24 24
         m.group = m.device.display.display.createGroup()
25 25
             .set('clip',
... ...
@@ -32,8 +32,9 @@ var mapClass = {
32 32
                 (m.device.data.mapclip.right + m.device.data.mapclip.left) / 2,
33 33
                 (m.device.data.mapclip.bottom + m.device.data.mapclip.top) / 2);
34 34
 
35
-        m.tiles = MapTiles.new(m.device, m.group);
36
-        m.navaids = MapNavaids.new(m.device, m.group);
35
+        m.layers = {};
36
+        m.layers.tiles = MapTiles.new(m.device, m.group);
37
+        m.layers.navaids = MapNavaids.new(m.device, m.group);
37 38
 
38 39
 #        m.device.display.display.createGroup().createChild('path')
39 40
 #            .setColor(1,0,0)
... ...
@@ -86,639 +87,12 @@ var mapClass = {
86 87
 
87 88
         me.group.setRotation(me.device.data.orientation.map * D2R);
88 89
 
89
-        me.tiles.update();
90
-        me.navaids.update();
91
-    },
92
-    setVisible : func (v) {
93
-        me.tiles.setVisible(v);
94
-        me.navaids.setVisible(v);
95
-    },
96
-};
97
-# }}}
98
-
99
-var MapTiles = {
100
-# displays maps background from web tiles {{{
101
-# code from http://wiki.flightgear.org/Canvas_Snippets#A_simple_tile_map
102
-    new : func (device, group) {
103
-        var m = { parents: [MapTiles] };
104
-        m.device = device;
105
-        m.display = m.device.display.display;
106
-        m.tile_size = 256;
107
-        m.maps_base = getprop("/sim/fg-home") ~ '/cache/maps';
108
-        m.makeUrl = string.compileTemplate('https://{server}/{type}/{z}/{x}/{y}.png{apikey}');
109
-        m.makePath = string.compileTemplate(m.maps_base ~ '/{server}/{type}/{z}/{x}/{y}.png');
110
-        m.num_tiles = [
111
-            math.ceil( m.device.data.mapsize[0] / m.tile_size ) + 1,
112
-            math.ceil( m.device.data.mapsize[1] / m.tile_size ) + 1
113
-        ];
114
-        m.center_tile_offset = [
115
-            (m.num_tiles[0] - 1) / 2,
116
-            (m.num_tiles[1] - 1) / 2
117
-        ];
118
-        m.visibility = m.device.role == 'MFD';
119
-        m.group = group.createChild('group', 'tiles')
120
-            .setTranslation(
121
-                    m.device.role == 'MFD' ? (m.device.data.mapview[0] - m.device.data.mapsize[0] + m.device.data.mapclip.left)/2 : -520,
122
-                    m.device.role == 'MFD' ? -250 : -45)
123
-            .setVisible(m.visibility);
124
-        m.tiles = setsize([], m.num_tiles[0]);
125
-        m.last_tile = [-1,-1];
126
-        m.last_type = data['tiles-type'];
127
-        m.initialize_grid();
128
-        return m;
129
-    },
130
-
131
-    setVisible : func (v) {
132
-        if (v != me.visibility) {
133
-            me.visibility = v;
134
-            me.group.setVisible(v);
135
-        }
136
-    },
137
-
138
-# initialize the map by setting up a grid of raster images
139
-    initialize_grid : func {
140
-        for(var x = 0; x < me.num_tiles[0]; x += 1) {
141
-            me.tiles[x] = setsize([], me.num_tiles[1]);
142
-            for(var y = 0; y < me.num_tiles[1]; y += 1)
143
-                me.tiles[x][y] = me.group.createChild('image', 'tile ' ~ x ~ ',' ~ y);
144
-        }
145
-    },
146
-
147
-# this is the callback that will be regularly called by the timer to update the map
148
-    update : func {
149
-         if (! me.visibility)
150
-             return;
151
-
152
-        var n = math.pow(2, me.device.data.zoom);
153
-        var offset = [
154
-            n * ((data.lon + 180) / 360) - me.center_tile_offset[0],
155
-            (1 - math.ln(math.tan(data.lat * math.pi/180) + 1 / math.cos(data.lat * math.pi/180)) / math.pi) / 2 * n - me.center_tile_offset[1]
156
-        ];
157
-        var tile_index = [int(offset[0]), int(offset[1])];
158
-
159
-        var ox = tile_index[0] - offset[0];
160
-        var oy = tile_index[1] - offset[1];
161
-
162
-        for (var x = 0; x < me.num_tiles[0]; x += 1)
163
-            for(var y = 0; y < me.num_tiles[1]; y += 1)
164
-                me.tiles[x][y]
165
-                    .setTranslation(
166
-                        int((ox + x) * me.tile_size + 0.5),
167
-                        int((oy + y) * me.tile_size + 0.5));
168
-
169
-        if (tile_index[0] != me.last_tile[0]
170
-         or tile_index[1] != me.last_tile[1]
171
-         or data['tiles-type'] != me.last_type) {
172
-            for(var x = 0; x < me.num_tiles[0]; x += 1)
173
-                for(var y = 0; y < me.num_tiles[1]; y += 1) {
174
-                    var pos = {
175
-                        z: me.device.data.zoom,
176
-                        x: int(offset[0] + x),
177
-                        y: int(offset[1] + y),
178
-                        type: data['tiles-type'],
179
-                        server : data['tiles-server'],
180
-                        apikey: data['tiles-apikey'],
181
-                    };
182
-
183
-                    (func {
184
-                        var img_path = me.makePath(pos);
185
-                        printlog('debug', 'img_path: ', img_path);
186
-                        var tile = me.tiles[x][y];
187
-
188
-                        if (io.stat(img_path) == nil) { # image not found, save in $FG_HOME
189
-                            var img_url = me.makeUrl(pos);
190
-                            printlog('debug', 'requesting ' ~ img_url);
191
-                            http.save(img_url, img_path)
192
-                                .done(func {printlog('info', 'received image ' ~ img_path); tile.set("src", img_path);})
193
-                                .fail(func (r) printlog('warn', 'Failed to get image ' ~ img_path ~ ' ' ~ r.status ~ ': ' ~ r.reason));
194
-                        }
195
-                        else { # cached image found, reusing
196
-                            printlog('debug', 'loading ' ~ img_path);
197
-                            tile.set("src", img_path);
198
-                        }
199
-                    })();
200
-                }
201
-            me.last_tile = tile_index;
202
-            me.last_type = data['tiles-type'];
203
-        }
204
-    },
205
-};
206
-# }}}
207
-
208
-# The following is largely inspired from the Extra500 Avidyne Entegra 9 
209
-# https://gitlab.com/extra500/extra500.git
210
-# Many thanks to authors: Dirk Dittmann and Eric van den Berg
211
-
212
-var MapIconCache = {
213
-# creates at init an icons cache for navaids, airports and airplane {{{
214
-    new : func (svgFile) {
215
-        var m = { parents:[MapIconCache] };
216
-
217
-        m._canvas = canvas.new( {
218
-            'name': 'MapIconCache',
219
-            'size': [512, 512],
220
-            'view': [512, 512],
221
-            'mipmapping': 1
222
-        });
223
-        m._canvas.addPlacement( {'type': 'ref'} );
224
-        m._canvas.setColorBackground(1,1,1,0);
225
-        m._group = m._canvas.createGroup('MapIcons');
226
-
227
-        canvas.parsesvg(m._group, data.zkv1000_reldir ~ svgFile);
228
-
229
-        m._sourceRectMap = {};
230
-
231
-        var icons = [ 'airplane' ];
232
-
233
-        foreach (var near; [0, 1])
234
-            foreach (var surface; [0, 1])
235
-                foreach (var tower; [0, 1])
236
-                    foreach (var center; tower ? [0, 1] : [ 0 ])
237
-                        append(icons, 'Airport_' ~ near ~ surface ~ tower ~ center);
238
-
239
-        foreach (var type; ['VOR', 'DME', 'TACAN', 'NDB'])
240
-            append(icons, 'Navaid_' ~ type);
241
-
242
-        foreach (var i; icons)
243
-            m.registerIcon(i);
244
-
245
-        return m;
246
-    },
247
-    registerIcon : func (id) {
248
-        me._sourceRectMap[id] = {
249
-            'bound' : [],
250
-            'size'  : [],
251
-        };
252
-        var element = me._group.getElementById(id);
253
-        if (element != nil) {
254
-            me._sourceRectMap[id].bound = element.getTransformedBounds();
255
-            # TODO ugly hack ? check for reason!
256
-            var top     = 512 - me._sourceRectMap[id].bound[3];
257
-            var bottom  = 512 - me._sourceRectMap[id].bound[1];
258
-            me._sourceRectMap[id].bound[1] = top;
259
-            me._sourceRectMap[id].bound[3] = bottom;
260
-
261
-            me._sourceRectMap[id].size = [
262
-                me._sourceRectMap[id].bound[2] - me._sourceRectMap[id].bound[0],
263
-                me._sourceRectMap[id].bound[3] - me._sourceRectMap[id].bound[1]
264
-            ];
265
-        }
266
-        else {
267
-            print('MapIconCache.registerIcon(' ~ id ~ ') fail');
268
-        }
269
-    },
270
-    getBounds : func (id) {
271
-        return me._sourceRectMap[id].bound;
272
-    },
273
-    getSize : func (id) {
274
-        return me._sourceRectMap[id].size;
275
-    },
276
-    boundIconToImage : func (id, image, center=1) {
277
-        if (!contains(me._sourceRectMap, id)) {
278
-            print('MapIconCache.boundIconToImage('~id~') ... no available.');
279
-            id = 'Airport_0001';
280
-        }
281
-        image.setSourceRect(
282
-                me._sourceRectMap[id].bound[0],
283
-                me._sourceRectMap[id].bound[1],
284
-                me._sourceRectMap[id].bound[2],
285
-                me._sourceRectMap[id].bound[3],
286
-                0);
287
-        image.setSize(
288
-                me._sourceRectMap[id].size[0],
289
-                me._sourceRectMap[id].size[1]);
290
-        if (center) {
291
-            image.setTranslation(
292
-                    -me._sourceRectMap[id].size[0]/2,
293
-                    -me._sourceRectMap[id].size[1]/2);
294
-        }
295
-    },
296
-};
297
-
298
-var mapIconCache = MapIconCache.new('Models/MapIcons.svg');
299
-# }}}
300
-
301
-var MapAirportItem = {
302
-# manage airports items by adding the ID and runways on associated icon {{{
303
-    new : func (id) {
304
-        var m = {parents:[MapAirportItem]};
305
-        m._id = id;
306
-        m._can = {
307
-            'group' : nil,
308
-            'label' : nil,
309
-            'image' : nil,
310
-            'layout': nil,
311
-            'runway': [],
312
-        };
313
-        m._mapAirportIcon = {
314
-            'near'      : 0,
315
-            'surface'   : 0,
316
-            'tower'     : 0,
317
-            'center'    : 0,
318
-            'displayed' : 0,
319
-            'icon'      : '',
320
-        };
321
-        return m;
322
-    },
323
-    create : func (group) {
324
-        me._can.group = group
325
-            .createChild('group', 'airport_' ~ me._id);
326
-
327
-        me._can.image = me._can.group.createChild('image', 'airport-image_' ~ me._id)
328
-            .setFile(mapIconCache._canvas.getPath())
329
-            .setSourceRect(0,0,0,0,0);
330
-
331
-        me._can.label = me._can.group.createChild('text', 'airport-label_' ~ me._id)
332
-            .setDrawMode( canvas.Text.TEXT )
333
-            .setTranslation(0, 37)
334
-            .setAlignment('center-bottom-baseline')
335
-            .setFont('LiberationFonts/LiberationSans-Regular.ttf')
336
-            .setFontSize(24);
337
-
338
-        me._can.label.set('fill','#BACBFB');
339
-        me._can.label.set('stroke','#000000');
340
-
341
-        me._can.layout = group.createChild('group','airport_layout' ~ me._id);
342
-        me._can.layoutIcon = group.createChild('group','airport_layout_Icon' ~ me._id);
343
-        return me._can.group;
344
-    },
345
-    draw : func (apt, mapOptions) {
346
-        me._mapAirportIcon.near = mapOptions.range > 32 ? 0 : 1;
347
-        me._mapAirportIcon.surface  = 0;
348
-        me._mapAirportIcon.tower    = 0;
349
-        me._mapAirportIcon.center   = 0;
350
-        me._mapAirportIcon.displayed    = 0;
351
-
352
-        # TODO make departure and destination airports specific
353
-        var aptInfo = airportinfo(apt.id);
354
-
355
-        me._can.layout.removeAllChildren();
356
-        me._can.layoutIcon.removeAllChildren();
357
-
358
-        me._mapAirportIcon.tower = (size(aptInfo.comms('tower')) > 0);
359
-        me._mapAirportIcon.center = me._mapAirportIcon.tower and (size(aptInfo.comms('approach')) > 0);
360
-
361
-        foreach (var rwy; keys(aptInfo.runways)) {
362
-            var runway = aptInfo.runways[rwy];
363
-            me._mapAirportIcon.surface = MAP_RUNWAY_SURFACE[runway.surface] ? 1 : me._mapAirportIcon.surface;
364
-            me._mapAirportIcon.displayed = runway.length > mapOptions.runwayLength ? 1 : me._mapAirportIcon.displayed;
365
-
366
-            if (mapOptions.range <= 10) {    # drawing real runways
367
-                me._can.layout.createChild('path', 'airport-runway-' ~ me._id ~ '-' ~ runway.id)
368
-                    .setStrokeLineWidth(7)
369
-                    .setColor(1,1,1)
370
-                    .setColorFill(1,1,1)
371
-                    .setDataGeo([
372
-                        canvas.Path.VG_MOVE_TO,
373
-                        canvas.Path.VG_LINE_TO,
374
-                        canvas.Path.VG_CLOSE_PATH
375
-                    ],[
376
-                        'N' ~ runway.lat, 'E' ~ runway.lon,
377
-                        'N' ~ runway.reciprocal.lat, 'E' ~ runway.reciprocal.lon,
378
-                    ]);
379
-            }
380
-            elsif (mapOptions.range <= 32) {     #draw icon runways
381
-                me._can.layoutIcon.setGeoPosition(apt.lat, apt.lon);
382
-                me._can.layoutIcon.createChild('path', 'airport-runway-' ~ me._id ~ '-' ~ runway.id)
383
-                    .setStrokeLineWidth(7)
384
-                    .setColor(1,1,1)
385
-                    .setColorFill(1,1,1)
386
-                    .setData([
387
-                        canvas.Path.VG_MOVE_TO,
388
-                        canvas.Path.VG_LINE_TO,
389
-                        canvas.Path.VG_CLOSE_PATH
390
-                    ],[
391
-                        0, -20,
392
-                        0, 20,
393
-                    ])
394
-                    .setRotation((runway.heading)* D2R);
395
-            }
396
-        }
397
-        me._mapAirportIcon.icon = 'Airport_'
398
-            ~ me._mapAirportIcon.near
399
-            ~ me._mapAirportIcon.surface
400
-            ~ me._mapAirportIcon.tower
401
-            ~ me._mapAirportIcon.center;
402
-
403
-        if (me._mapAirportIcon.displayed) {
404
-            me._can.label
405
-                .setText(apt.id)
406
-                .setRotation(-mapOptions.orientation * D2R);
407
-            me._can.group.setGeoPosition(apt.lat, apt.lon);
408
-            if (mapOptions.range <= 10) {
409
-                me._can.image.setVisible(0);
410
-                me._can.layout.setVisible(1);
411
-            }
412
-            elsif (mapOptions.range <= 32) {
413
-                mapIconCache.boundIconToImage(me._mapAirportIcon.icon, me._can.image);
414
-                me._can.image.setVisible(1);
415
-                me._can.layout.setVisible(1);
416
-            }
417
-            else {
418
-                mapIconCache.boundIconToImage(me._mapAirportIcon.icon, me._can.image);
419
-                me._can.layout.setVisible(0);
420
-                me._can.image.setVisible(1);
421
-            }
422
-            me._can.group.setVisible(1);
423
-        }
424
-        return me._mapAirportIcon.displayed;
425
-    },
426
-    update : func (mapOptions) {
427
-        if (mapOptions.range <= 10) { }
428
-        elsif (mapOptions.range <= 32)
429
-            me._can.layoutIcon.setRotation(-mapOptions.orientation * D2R);
430
-        else { }
431
-    },
432
-    setVisible : func (visibility) {
433
-        me._can.group.setVisible(visibility);
434
-        me._can.layout.setVisible(visibility);
435
-        me._can.image.setVisible(visibility);
436
-        me._can.layoutIcon.setVisible(visibility);
437
-    },
438
-};
439
-# }}}
440
-
441
-var MapNavaidItem = {
442
-# manage navaids items by adding ID in the icon {{{
443
-    new : func (id, type) {
444
-        var m = {parents:[MapNavaidItem]};
445
-        m._id = id;
446
-        m._type = type;
447
-        m._can = {
448
-            'group' : nil,
449
-            'label' : nil,
450
-            'image' : nil,
451
-        };
452
-        return m;
453
-    },
454
-    create : func (group) {
455
-        me._can.group = group
456
-            .createChild('group', me._type ~ '_' ~ me._id);
457
-
458
-        me._can.image = me._can.group.createChild('image', me._type ~ '-image_' ~ me._id)
459
-            .setFile(mapIconCache._canvas.getPath())
460
-            .setSourceRect(0,0,0,0,0);
461
-
462
-        me._can.label = me._can.group.createChild('text', me._type ~ '-label_' ~ me._id)
463
-            .setDrawMode( canvas.Text.TEXT )
464
-            .setTranslation(0,42)
465
-            .setAlignment('center-bottom-baseline')
466
-            .setFont('LiberationFonts/LiberationSans-Regular.ttf')
467
-            .setFontSize(24);
468
-
469
-        me._can.label.set('fill','#BACBFB');
470
-        me._can.label.set('stroke','#000000');
471
-
472
-        return me._can.group;
473
-    },
474
-    setData : func (navaid, type, mapOptions) {
475
-        mapIconCache.boundIconToImage('Navaid_' ~ type, me._can.image);
476
-        me._can.label
477
-            .setText(navaid.id)
478
-            .setRotation(-mapOptions.orientation * D2R);
479
-        me._can.group.setGeoPosition(navaid.lat, navaid.lon);
480
-    },
481
-    setVisible : func (visibility) {
482
-        me._can.group.setVisible(visibility);
483
-    },
484
-};
485
-# }}}
486
-
487
-var MapAirplaneItem = {
488
-# set airplane on ND {{{
489
-    new : func {
490
-        var m = {parents:[MapAirplaneItem]};
491
-        m._can = {
492
-            'group' : nil,
493
-            'image' : nil,
494
-        };
495
-        return m;
496
-    },
497
-    create : func (group) {
498
-        me._can.group = group
499
-            .createChild('group', 'airplane');
500
-        me._can.image = me._can.group.createChild('image', 'airplane-image')
501
-            .setFile(mapIconCache._canvas.getPath())
502
-            .setSourceRect(0,0,0,0,0);
503
-        return me._can.group;
504
-    },
505
-    setData : func (orientation) {
506
-        mapIconCache.boundIconToImage('airplane', me._can.image);
507
-        me._can.group
508
-            .setGeoPosition(data.lat, data.lon)
509
-            .setRotation(orientation * D2R);
510
-    },
511
-    setVisible : func (visibility) {
512
-        me._can.group.setVisible(visibility);
513
-    },
514
-};
515
-# }}}
516
-
517
-var MAP_RUNWAY_SURFACE =  {0:0, 1:1, 2:1, 3:0, 4:0, 5:0, 6:1, 7:1, 8:0, 9:0, 10:0, 11:0, 12:0};
518
-var MAP_RUNWAY_AT_RANGE = func (range) {
519
-    if (range < 40) return 0;
520
-    if (range < 50) return 250;
521
-    if (range < 80) return 500;
522
-    if (range < 160) return 1000;
523
-    if (range < 240) return 3000;
524
-    return 3000;
525
-}
526
-var MAP_TXRANGE_VOR = func (range) {
527
-    if (range < 40) return 0;
528
-    if (range < 50) return 20;
529
-    if (range < 80) return 25;
530
-    if (range < 160) return 30;
531
-    if (range < 240) return 50;
532
-    return 100;
533
-}
534
-####
535
-# Declutter
536
-#   land
537
-#       0 : 'Terrain'
538
-#       1 : 'Political boundaries'
539
-#       2 : 'River/Lakes/Oceans'
540
-#       3 : 'Roads'
541
-#   Nav
542
-#       0 : 'Airspace'
543
-#       1 : 'Victor/Jet airways'
544
-#       2 : 'Obstacles'
545
-#       3 : 'Navaids'
546
-
547
-var MapNavaids = {
548
-# the layer to show navaids, airports and airplane symbol {{{
549
-    new : func (device, group) {
550
-        var m = {parents : [MapNavaids]};
551
-
552
-        m._model = nil;
553
-        m.device = device;
554
-        m._visibility = m.device.role == 'MFD';
555
-
556
-        m._group = group.createChild('map', 'MFD map')
557
-            .setTranslation(
558
-                m.device.role == 'MFD' ? (m.device.data.mapview[0] + m.device.data.mapclip.left)/2 : 120,
559
-                m.device.role == 'MFD' ? 400 : 600)
560
-            .setVisible(m._visibility);
561
-            m._group._node
562
-                .getNode('range', 1).setDoubleValue(m.device.data['range-factor']);
563
-
564
-        m._can = {};
565
-        m._cache = {};
566
-        foreach (var n; ['airport', 'VOR', 'TACAN', 'NDB', 'DME']) {
567
-            m._can[n] = m._group.createChild('group', n);
568
-            m._cache[n] = {
569
-                'data' : [],
570
-                'index' : 0,
571
-                'max' : 100,
572
-            };
573
-        }
574
-        m._can['airplane'] = m._group.createChild('group', 'airplane');
575
-        m._cache['airplane'] = {
576
-            displayed : 0,
577
-            item : nil,
578
-        };
579
-
580
-        m._mapOptions = {
581
-            declutterLand : 3,
582
-            declutterNAV  : 3,
583
-            lightning     : 0,
584
-            reports       : 0,
585
-            overlay       : 0,
586
-            range         : m.device.data['range-nm'],
587
-            rangeLow      : m.device.data['range-nm'] / 2,
588
-            runwayLength  : -1,
589
-            orientation   : m.device.data.orientation.map,
590
-        };
591
-
592
-        m._results = nil;
593
-
594
-        return m;
595
-    },
596
-    update : func {
597
-        me._group._node.getNode('ref-lat', 1).setDoubleValue(data.lat);
598
-        me._group._node.getNode('ref-lon', 1).setDoubleValue(data.lon);
599
-
600
-        me._group._node
601
-            .getNode('range', 1).setDoubleValue(me.device.data['range-factor']);
602
-        me._mapOptions.orientation = me.device.data.orientation.map;
603
-
604
-        if (me._visibility == 1) {
605
-            me.loadAirport();
606
-            foreach (var n; ['VOR', 'TACAN', 'NDB', 'DME'])
607
-                me.loadNavaid(n);
608
-            me.loadAirplane();
609
-        }
610
-    },
611
-    _onVisibilityChange : func {
612
-        me._group.setVisible(me._visibility);
613
-    },
614
-    setMapOptions : func (mapOptions) {
615
-        me._mapOptions = mapOptions;
616
-        me.update();
617
-    },
618
-    updateOrientation : func (value) {
619
-        me._mapOptions.orientation = value;
620
-        for (var i = 0 ; i < me._cache.airport.index ; i +=1) {
621
-            item = me._cache.airport.data[i];
622
-            item.update(me._mapOptions);
623
-        }
624
-    },
625
-    setRange : func (range=100) {
626
-        me._mapOptions.range = me.device.data['range-nm'];
627
-        me._mapOptions.rangeLow = me._mapOptions.range/2;
628
-        me.update();
629
-    },
630
-    setRotation : func (deg) {
631
-        me._group.setRotation(deg * D2R);
90
+        foreach (var l; keys(me.layers))
91
+            me.layers[l].update();
632 92
     },
633 93
     setVisible : func (v) {
634
-        if (me._visibility != v) {
635
-            me._visibility = v;
636
-            me._onVisibilityChange();
637
-        }
638
-    },
639
-    _onVisibilityChange : func {
640
-        me._group.setVisible(me._visibility);
641
-    },
642
-    # positioned.findWithinRange : any, fix, vor, ndb, ils, dme, tacan
643
-
644
-    loadAirport : func {
645
-        me._cache.airport.index = 0;
646
-        var results = positioned.findWithinRange(me._mapOptions.range * 2.5, 'airport');
647
-        var item = nil;
648
-
649
-        if (me._mapOptions.declutterNAV >= 2)
650
-            me._mapOptions.runwayLength = MAP_RUNWAY_AT_RANGE(me._mapOptions.range);
651
-        elsif (me._mapOptions.declutterNAV >= 1)
652
-            me._mapOptions.runwayLength = 2000;
653
-        else
654
-            me._mapOptions.runwayLength = 3000;
655
-
656
-        if (me._mapOptions.runwayLength >= 0) {
657
-            foreach (var apt; results) {
658
-                if (me._cache.airport.index >= me._cache.airport.max )
659
-                    break;
660
-
661
-                if (size(me._cache.airport.data) > me._cache.airport.index)
662
-                    item = me._cache.airport.data[me._cache.airport.index];
663
-                else {
664
-                    item = MapAirportItem.new(me._cache.airport.index);
665
-                    item.create(me._can.airport);
666
-                    append(me._cache.airport.data, item);
667
-                }
668
-
669
-                if (item.draw(apt, me._mapOptions)) {
670
-                    item.setVisible(1);
671
-                    me._cache.airport.index += 1;
672
-                }
673
-            }
674
-        }
675
-
676
-        for (var i = me._cache.airport.index ; i < size(me._cache.airport.data) ; i +=1) {
677
-            item = me._cache.airport.data[i];
678
-            item.setVisible(0);
679
-        }
680
-    },
681
-    loadNavaid : func (type) {
682
-        me._cache[type].index = 0;
683
-        if (me._mapOptions.declutterNAV >= 3) { # TODO test for DME and NDB range < 100nm
684
-            var range = me._mapOptions.range * 2.5;
685
-            var txRange = MAP_TXRANGE_VOR(me._mapOptions.range);
686
-            var results = positioned.findWithinRange(range, type);
687
-            var item = nil;
688
-            foreach (var n; results) {
689
-                if (n.range_nm < txRange)
690
-                    break;
691
-
692
-                if (me._cache[type].index >= me._cache[type].max )
693
-                    break;
694
-
695
-                if (size(me._cache[type].data) > me._cache[type].index) {
696
-                    item = me._cache[type].data[me._cache[type].index];
697
-                    item.setData(n, type, me._mapOptions);
698
-                }
699
-                else {
700
-                    item = MapNavaidItem.new(me._cache[type].index, type);
701
-                    item.create(me._can[type]);
702
-                    item.setData(n, type, me._mapOptions);
703
-                    append(me._cache[type].data, item);
704
-                }
705
-                item.setVisible(1);
706
-                me._cache[type].index += 1;
707
-            }
708
-        }
709
-        for (var i = me._cache[type].index ; i < size(me._cache[type].data) ; i +=1) {
710
-            item = me._cache[type].data[i];
711
-            item.setVisible(0);
712
-        }
713
-    },
714
-    loadAirplane : func {
715
-        if (!me._cache.airplane.displayed) {
716
-            me._cache.airplane.item = MapAirplaneItem.new();
717
-            me._cache.airplane.item.create(me._can['airplane']);
718
-            me._cache.airplane.displayed = 1;
719
-        }
720
-        me._cache.airplane.item.setData(me.device.data.orientation.airplane);
721
-        me._cache.airplane.item.setVisible(1);
94
+        me.visibility = v;
95
+        foreach (var l; keys(me.layers))
96
+            me.layers[l].setVisible(v);
722 97
     },
723 98
 };
724
-# }}}
+517
Nasal/maps/navaids.nas
... ...
@@ -0,0 +1,517 @@
1
+# The following is largely inspired from the Extra500 Avidyne Entegra 9 
2
+# https://gitlab.com/extra500/extra500.git
3
+# Many thanks to authors: Dirk Dittmann and Eric van den Berg
4
+var MapIconCache = {
5
+# creates at init an icons cache for navaids, airports and airplane {{{
6
+    new : func (svgFile) {
7
+        var m = { parents:[MapIconCache] };
8
+
9
+        m._canvas = canvas.new( {
10
+            'name': 'MapIconCache',
11
+            'size': [512, 512],
12
+            'view': [512, 512],
13
+            'mipmapping': 1
14
+        });
15
+        m._canvas.addPlacement( {'type': 'ref'} );
16
+        m._canvas.setColorBackground(1,1,1,0);
17
+        m._group = m._canvas.createGroup('MapIcons');
18
+
19
+        canvas.parsesvg(m._group, data.zkv1000_reldir ~ svgFile);
20
+
21
+        m._sourceRectMap = {};
22
+
23
+        var icons = [ 'airplane' ];
24
+
25
+        foreach (var near; [0, 1])
26
+            foreach (var surface; [0, 1])
27
+                foreach (var tower; [0, 1])
28
+                    foreach (var center; tower ? [0, 1] : [ 0 ])
29
+                        append(icons, 'Airport_' ~ near ~ surface ~ tower ~ center);
30
+
31
+        foreach (var type; ['VOR', 'DME', 'TACAN', 'NDB'])
32
+            append(icons, 'Navaid_' ~ type);
33
+
34
+        foreach (var i; icons)
35
+            m.registerIcon(i);
36
+
37
+        return m;
38
+    },
39
+    registerIcon : func (id) {
40
+        me._sourceRectMap[id] = {
41
+            'bound' : [],
42
+            'size'  : [],
43
+        };
44
+        var element = me._group.getElementById(id);
45
+        if (element != nil) {
46
+            me._sourceRectMap[id].bound = element.getTransformedBounds();
47
+            # TODO ugly hack ? check for reason!
48
+            var top     = 512 - me._sourceRectMap[id].bound[3];
49
+            var bottom  = 512 - me._sourceRectMap[id].bound[1];
50
+            me._sourceRectMap[id].bound[1] = top;
51
+            me._sourceRectMap[id].bound[3] = bottom;
52
+
53
+            me._sourceRectMap[id].size = [
54
+                me._sourceRectMap[id].bound[2] - me._sourceRectMap[id].bound[0],
55
+                me._sourceRectMap[id].bound[3] - me._sourceRectMap[id].bound[1]
56
+            ];
57
+        }
58
+        else {
59
+            print('MapIconCache.registerIcon(' ~ id ~ ') fail');
60
+        }
61
+    },
62
+    getBounds : func (id) {
63
+        return me._sourceRectMap[id].bound;
64
+    },
65
+    getSize : func (id) {
66
+        return me._sourceRectMap[id].size;
67
+    },
68
+    boundIconToImage : func (id, image, center=1) {
69
+        if (!contains(me._sourceRectMap, id)) {
70
+            print('MapIconCache.boundIconToImage('~id~') ... no available.');
71
+            id = 'Airport_0001';
72
+        }
73
+        image.setSourceRect(
74
+                me._sourceRectMap[id].bound[0],
75
+                me._sourceRectMap[id].bound[1],
76
+                me._sourceRectMap[id].bound[2],
77
+                me._sourceRectMap[id].bound[3],
78
+                0);
79
+        image.setSize(
80
+                me._sourceRectMap[id].size[0],
81
+                me._sourceRectMap[id].size[1]);
82
+        if (center) {
83
+            image.setTranslation(
84
+                    -me._sourceRectMap[id].size[0]/2,
85
+                    -me._sourceRectMap[id].size[1]/2);
86
+        }
87
+    },
88
+};
89
+
90
+var mapIconCache = MapIconCache.new('Models/MapIcons.svg');
91
+# }}}
92
+
93
+var MapAirportItem = {
94
+# manage airports items by adding the ID and runways on associated icon {{{
95
+    new : func (id) {
96
+        var m = {parents:[MapAirportItem]};
97
+        m._id = id;
98
+        m._can = {
99
+            'group' : nil,
100
+            'label' : nil,
101
+            'image' : nil,
102
+            'layout': nil,
103
+            'runway': [],
104
+        };
105
+        m._mapAirportIcon = {
106
+            'near'      : 0,
107
+            'surface'   : 0,
108
+            'tower'     : 0,
109
+            'center'    : 0,
110
+            'displayed' : 0,
111
+            'icon'      : '',
112
+        };
113
+        return m;
114
+    },
115
+    create : func (group) {
116
+        me._can.group = group
117
+            .createChild('group', 'airport_' ~ me._id);
118
+
119
+        me._can.image = me._can.group.createChild('image', 'airport-image_' ~ me._id)
120
+            .setFile(mapIconCache._canvas.getPath())
121
+            .setSourceRect(0,0,0,0,0);
122
+
123
+        me._can.label = me._can.group.createChild('text', 'airport-label_' ~ me._id)
124
+            .setDrawMode( canvas.Text.TEXT )
125
+            .setTranslation(0, 37)
126
+            .setAlignment('center-bottom-baseline')
127
+            .setFont('LiberationFonts/LiberationSans-Regular.ttf')
128
+            .setFontSize(24);
129
+
130
+        me._can.label.set('fill','#BACBFB');
131
+        me._can.label.set('stroke','#000000');
132
+
133
+        me._can.layout = group.createChild('group','airport_layout' ~ me._id);
134
+        me._can.layoutIcon = group.createChild('group','airport_layout_Icon' ~ me._id);
135
+        return me._can.group;
136
+    },
137
+    draw : func (apt, mapOptions) {
138
+        me._mapAirportIcon.near = mapOptions.range > 32 ? 0 : 1;
139
+        me._mapAirportIcon.surface  = 0;
140
+        me._mapAirportIcon.tower    = 0;
141
+        me._mapAirportIcon.center   = 0;
142
+        me._mapAirportIcon.displayed    = 0;
143
+
144
+        # TODO make departure and destination airports specific
145
+        var aptInfo = airportinfo(apt.id);
146
+
147
+        me._can.layout.removeAllChildren();
148
+        me._can.layoutIcon.removeAllChildren();
149
+
150
+        me._mapAirportIcon.tower = (size(aptInfo.comms('tower')) > 0);
151
+        me._mapAirportIcon.center = me._mapAirportIcon.tower and (size(aptInfo.comms('approach')) > 0);
152
+
153
+        foreach (var rwy; keys(aptInfo.runways)) {
154
+            var runway = aptInfo.runways[rwy];
155
+            me._mapAirportIcon.surface = MAP_RUNWAY_SURFACE[runway.surface] ? 1 : me._mapAirportIcon.surface;
156
+            me._mapAirportIcon.displayed = runway.length > mapOptions.runwayLength ? 1 : me._mapAirportIcon.displayed;
157
+
158
+            if (mapOptions.range <= 10) {    # drawing real runways
159
+                me._can.layout.createChild('path', 'airport-runway-' ~ me._id ~ '-' ~ runway.id)
160
+                    .setStrokeLineWidth(7)
161
+                    .setColor(1,1,1)
162
+                    .setColorFill(1,1,1)
163
+                    .setDataGeo([
164
+                        canvas.Path.VG_MOVE_TO,
165
+                        canvas.Path.VG_LINE_TO,
166
+                        canvas.Path.VG_CLOSE_PATH
167
+                    ],[
168
+                        'N' ~ runway.lat, 'E' ~ runway.lon,
169
+                        'N' ~ runway.reciprocal.lat, 'E' ~ runway.reciprocal.lon,
170
+                    ]);
171
+            }
172
+            elsif (mapOptions.range <= 32) {     #draw icon runways
173
+                me._can.layoutIcon.setGeoPosition(apt.lat, apt.lon);
174
+                me._can.layoutIcon.createChild('path', 'airport-runway-' ~ me._id ~ '-' ~ runway.id)
175
+                    .setStrokeLineWidth(7)
176
+                    .setColor(1,1,1)
177
+                    .setColorFill(1,1,1)
178
+                    .setData([
179
+                        canvas.Path.VG_MOVE_TO,
180
+                        canvas.Path.VG_LINE_TO,
181
+                        canvas.Path.VG_CLOSE_PATH
182
+                    ],[
183
+                        0, -20,
184
+                        0, 20,
185
+                    ])
186
+                    .setRotation((runway.heading)* D2R);
187
+            }
188
+        }
189
+        me._mapAirportIcon.icon = 'Airport_'
190
+            ~ me._mapAirportIcon.near
191
+            ~ me._mapAirportIcon.surface
192
+            ~ me._mapAirportIcon.tower
193
+            ~ me._mapAirportIcon.center;
194
+
195
+        if (me._mapAirportIcon.displayed) {
196
+            me._can.label
197
+                .setText(apt.id)
198
+                .setRotation(-mapOptions.orientation * D2R);
199
+            me._can.group.setGeoPosition(apt.lat, apt.lon);
200
+            if (mapOptions.range <= 10) {
201
+                me._can.image.setVisible(0);
202
+                me._can.layout.setVisible(1);
203
+            }
204
+            elsif (mapOptions.range <= 32) {
205
+                mapIconCache.boundIconToImage(me._mapAirportIcon.icon, me._can.image);
206
+                me._can.image.setVisible(1);
207
+                me._can.layout.setVisible(1);
208
+            }
209
+            else {
210
+                mapIconCache.boundIconToImage(me._mapAirportIcon.icon, me._can.image);
211
+                me._can.layout.setVisible(0);
212
+                me._can.image.setVisible(1);
213
+            }
214
+            me._can.group.setVisible(1);
215
+        }
216
+        return me._mapAirportIcon.displayed;
217
+    },
218
+    update : func (mapOptions) {
219
+        if (mapOptions.range <= 10) { }
220
+        elsif (mapOptions.range <= 32)
221
+            me._can.layoutIcon.setRotation(-mapOptions.orientation * D2R);
222
+        else { }
223
+    },
224
+    setVisible : func (visibility) {
225
+        me._can.group.setVisible(visibility);
226
+        me._can.layout.setVisible(visibility);
227
+        me._can.image.setVisible(visibility);
228
+        me._can.layoutIcon.setVisible(visibility);
229
+    },
230
+};
231
+# }}}
232
+
233
+var MapNavaidItem = {
234
+# manage navaids items by adding ID in the icon {{{
235
+    new : func (id, type) {
236
+        var m = {parents:[MapNavaidItem]};
237
+        m._id = id;
238
+        m._type = type;
239
+        m._can = {
240
+            'group' : nil,
241
+            'label' : nil,
242
+            'image' : nil,
243
+        };
244
+        return m;
245
+    },
246
+    create : func (group) {
247
+        me._can.group = group
248
+            .createChild('group', me._type ~ '_' ~ me._id);
249
+
250
+        me._can.image = me._can.group.createChild('image', me._type ~ '-image_' ~ me._id)
251
+            .setFile(mapIconCache._canvas.getPath())
252
+            .setSourceRect(0,0,0,0,0);
253
+
254
+        me._can.label = me._can.group.createChild('text', me._type ~ '-label_' ~ me._id)
255
+            .setDrawMode( canvas.Text.TEXT )
256
+            .setTranslation(0,42)
257
+            .setAlignment('center-bottom-baseline')
258
+            .setFont('LiberationFonts/LiberationSans-Regular.ttf')
259
+            .setFontSize(24);
260
+
261
+        me._can.label.set('fill','#BACBFB');
262
+        me._can.label.set('stroke','#000000');
263
+
264
+        return me._can.group;
265
+    },
266
+    setData : func (navaid, type, mapOptions) {
267
+        mapIconCache.boundIconToImage('Navaid_' ~ type, me._can.image);
268
+        me._can.label
269
+            .setText(navaid.id)
270
+            .setRotation(-mapOptions.orientation * D2R);
271
+        me._can.group.setGeoPosition(navaid.lat, navaid.lon);
272
+    },
273
+    setVisible : func (visibility) {
274
+        me._can.group.setVisible(visibility);
275
+    },
276
+};
277
+# }}}
278
+
279
+var MapAirplaneItem = {
280
+# set airplane on ND {{{
281
+    new : func {
282
+        var m = {parents:[MapAirplaneItem]};
283
+        m._can = {
284
+            'group' : nil,
285
+            'image' : nil,
286
+        };
287
+        return m;
288
+    },
289
+    create : func (group) {
290
+        me._can.group = group
291
+            .createChild('group', 'airplane');
292
+        me._can.image = me._can.group.createChild('image', 'airplane-image')
293
+            .setFile(mapIconCache._canvas.getPath())
294
+            .setSourceRect(0,0,0,0,0);
295
+        return me._can.group;
296
+    },
297
+    setData : func (orientation) {
298
+        mapIconCache.boundIconToImage('airplane', me._can.image);
299
+        me._can.group
300
+            .setGeoPosition(data.lat, data.lon)
301
+            .setRotation(orientation * D2R);
302
+    },
303
+    setVisible : func (visibility) {
304
+        me._can.group.setVisible(visibility);
305
+    },
306
+};
307
+# }}}
308
+
309
+var MAP_RUNWAY_SURFACE =  {0:0, 1:1, 2:1, 3:0, 4:0, 5:0, 6:1, 7:1, 8:0, 9:0, 10:0, 11:0, 12:0};
310
+var MAP_RUNWAY_AT_RANGE = func (range) {
311
+    if (range < 40) return 0;
312
+    if (range < 50) return 250;
313
+    if (range < 80) return 500;
314
+    if (range < 160) return 1000;
315
+    if (range < 240) return 3000;
316
+    return 3000;
317
+}
318
+var MAP_TXRANGE_VOR = func (range) {
319
+    if (range < 40) return 0;
320
+    if (range < 50) return 20;
321
+    if (range < 80) return 25;
322
+    if (range < 160) return 30;
323
+    if (range < 240) return 50;
324
+    return 100;
325
+}
326
+####
327
+# Declutter
328
+#   land
329
+#       0 : 'Terrain'
330
+#       1 : 'Political boundaries'
331
+#       2 : 'River/Lakes/Oceans'
332
+#       3 : 'Roads'
333
+#   Nav
334
+#       0 : 'Airspace'
335
+#       1 : 'Victor/Jet airways'
336
+#       2 : 'Obstacles'
337
+#       3 : 'Navaids'
338
+
339
+var MapNavaids = {
340
+# the layer to show navaids, airports and airplane symbol {{{
341
+    new : func (device, group) {
342
+        var m = {parents : [MapNavaids]};
343
+
344
+        m._model = nil;
345
+        m.device = device;
346
+        m._visibility = m.device.role == 'MFD';
347
+
348
+        m._group = group.createChild('map', 'MFD map')
349
+            .setTranslation(
350
+                m.device.role == 'MFD' ? (m.device.data.mapview[0] + m.device.data.mapclip.left)/2 : 120,
351
+                m.device.role == 'MFD' ? 400 : 600)
352
+            .setVisible(m._visibility);
353
+            m._group._node
354
+                .getNode('range', 1).setDoubleValue(m.device.data['range-factor']);
355
+
356
+        m._can = {};
357
+        m._cache = {};
358
+        foreach (var n; ['airport', 'VOR', 'TACAN', 'NDB', 'DME']) {
359
+            m._can[n] = m._group.createChild('group', n);
360
+            m._cache[n] = {
361
+                'data' : [],
362
+                'index' : 0,
363
+                'max' : 100,
364
+            };
365
+        }
366
+        m._can['airplane'] = m._group.createChild('group', 'airplane');
367
+        m._cache['airplane'] = {
368
+            displayed : 0,
369
+            item : nil,
370
+        };
371
+
372
+        m._mapOptions = {
373
+            declutterLand : 3,
374
+            declutterNAV  : 3,
375
+            lightning     : 0,
376
+            reports       : 0,
377
+            overlay       : 0,
378
+            range         : m.device.data['range-nm'],
379
+            rangeLow      : m.device.data['range-nm'] / 2,
380
+            runwayLength  : -1,
381
+            orientation   : m.device.data.orientation.map,
382
+        };
383
+
384
+        m._results = nil;
385
+
386
+        return m;
387
+    },
388
+    update : func {
389
+        me._group._node.getNode('ref-lat', 1).setDoubleValue(data.lat);
390
+        me._group._node.getNode('ref-lon', 1).setDoubleValue(data.lon);
391
+
392
+        me._group._node
393
+            .getNode('range', 1).setDoubleValue(me.device.data['range-factor']);
394
+        me._mapOptions.orientation = me.device.data.orientation.map;
395
+
396
+        if (me._visibility == 1) {
397
+            me.loadAirport();
398
+            foreach (var n; ['VOR', 'TACAN', 'NDB', 'DME'])
399
+                me.loadNavaid(n);
400
+            me.loadAirplane();
401
+        }
402
+    },
403
+    _onVisibilityChange : func {
404
+        me._group.setVisible(me._visibility);
405
+    },
406
+    setMapOptions : func (mapOptions) {
407
+        me._mapOptions = mapOptions;
408
+        me.update();
409
+    },
410
+    updateOrientation : func (value) {
411
+        me._mapOptions.orientation = value;
412
+        for (var i = 0 ; i < me._cache.airport.index ; i +=1) {
413
+            item = me._cache.airport.data[i];
414
+            item.update(me._mapOptions);
415
+        }
416
+    },
417
+    setRange : func (range=100) {
418
+        me._mapOptions.range = me.device.data['range-nm'];
419
+        me._mapOptions.rangeLow = me._mapOptions.range/2;
420
+        me.update();
421
+    },
422
+    setRotation : func (deg) {
423
+        me._group.setRotation(deg * D2R);
424
+    },
425
+    setVisible : func (v) {
426
+        if (me._visibility != v) {
427
+            me._visibility = v;
428
+            me._onVisibilityChange();
429
+        }
430
+    },
431
+    _onVisibilityChange : func {
432
+        me._group.setVisible(me._visibility);
433
+    },
434
+    # positioned.findWithinRange : any, fix, vor, ndb, ils, dme, tacan
435
+
436
+    loadAirport : func {
437
+        me._cache.airport.index = 0;
438
+        var results = positioned.findWithinRange(me._mapOptions.range * 2.5, 'airport');
439
+        var item = nil;
440
+
441
+        if (me._mapOptions.declutterNAV >= 2)
442
+            me._mapOptions.runwayLength = MAP_RUNWAY_AT_RANGE(me._mapOptions.range);
443
+        elsif (me._mapOptions.declutterNAV >= 1)
444
+            me._mapOptions.runwayLength = 2000;
445
+        else
446
+            me._mapOptions.runwayLength = 3000;
447
+
448
+        if (me._mapOptions.runwayLength >= 0) {
449
+            foreach (var apt; results) {
450
+                if (me._cache.airport.index >= me._cache.airport.max )
451
+                    break;
452
+
453
+                if (size(me._cache.airport.data) > me._cache.airport.index)
454
+                    item = me._cache.airport.data[me._cache.airport.index];
455
+                else {
456
+                    item = MapAirportItem.new(me._cache.airport.index);
457
+                    item.create(me._can.airport);
458
+                    append(me._cache.airport.data, item);
459
+                }
460
+
461
+                if (item.draw(apt, me._mapOptions)) {
462
+                    item.setVisible(1);
463
+                    me._cache.airport.index += 1;
464
+                }
465
+            }
466
+        }
467
+
468
+        for (var i = me._cache.airport.index ; i < size(me._cache.airport.data) ; i +=1) {
469
+            item = me._cache.airport.data[i];
470
+            item.setVisible(0);
471
+        }
472
+    },
473
+    loadNavaid : func (type) {
474
+        me._cache[type].index = 0;
475
+        if (me._mapOptions.declutterNAV >= 3) { # TODO test for DME and NDB range < 100nm
476
+            var range = me._mapOptions.range * 2.5;
477
+            var txRange = MAP_TXRANGE_VOR(me._mapOptions.range);
478
+            var results = positioned.findWithinRange(range, type);
479
+            var item = nil;
480
+            foreach (var n; results) {
481
+                if (n.range_nm < txRange)
482
+                    break;
483
+
484
+                if (me._cache[type].index >= me._cache[type].max )
485
+                    break;
486
+
487
+                if (size(me._cache[type].data) > me._cache[type].index) {
488
+                    item = me._cache[type].data[me._cache[type].index];
489
+                    item.setData(n, type, me._mapOptions);
490
+                }
491
+                else {
492
+                    item = MapNavaidItem.new(me._cache[type].index, type);
493
+                    item.create(me._can[type]);
494
+                    item.setData(n, type, me._mapOptions);
495
+                    append(me._cache[type].data, item);
496
+                }
497
+                item.setVisible(1);
498
+                me._cache[type].index += 1;
499
+            }
500
+        }
501
+        for (var i = me._cache[type].index ; i < size(me._cache[type].data) ; i +=1) {
502
+            item = me._cache[type].data[i];
503
+            item.setVisible(0);
504
+        }
505
+    },
506
+    loadAirplane : func {
507
+        if (!me._cache.airplane.displayed) {
508
+            me._cache.airplane.item = MapAirplaneItem.new();
509
+            me._cache.airplane.item.create(me._can['airplane']);
510
+            me._cache.airplane.displayed = 1;
511
+        }
512
+        me._cache.airplane.item.setData(me.device.data.orientation.airplane);
513
+        me._cache.airplane.item.setVisible(1);
514
+    },
515
+};
516
+# }}}
517
+
+107
Nasal/maps/tiles.nas
... ...
@@ -0,0 +1,107 @@
1
+var MapTiles = {
2
+# displays maps background from web tiles
3
+# code from http://wiki.flightgear.org/Canvas_Snippets#A_simple_tile_map
4
+    new : func (device, group) {
5
+        var m = { parents: [MapTiles] };
6
+        m.device = device;
7
+        m.display = m.device.display.display;
8
+        m.tile_size = 256;
9
+        m.maps_base = getprop("/sim/fg-home") ~ '/cache/maps';
10
+        m.makeUrl = string.compileTemplate('https://{server}/{type}/{z}/{x}/{y}.png{apikey}');
11
+        m.makePath = string.compileTemplate(m.maps_base ~ '/{server}/{type}/{z}/{x}/{y}.png');
12
+        m.num_tiles = [
13
+            math.ceil( m.device.data.mapsize[0] / m.tile_size ) + 1,
14
+            math.ceil( m.device.data.mapsize[1] / m.tile_size ) + 1
15
+        ];
16
+        m.center_tile_offset = [
17
+            (m.num_tiles[0] - 1) / 2,
18
+            (m.num_tiles[1] - 1) / 2
19
+        ];
20
+        m.visibility = m.device.role == 'MFD';
21
+        m.group = group.createChild('group', 'tiles')
22
+            .setTranslation(
23
+                    m.device.role == 'MFD' ? (m.device.data.mapview[0] - m.device.data.mapsize[0] + m.device.data.mapclip.left)/2 : -520,
24
+                    m.device.role == 'MFD' ? -250 : -45)
25
+            .setVisible(m.visibility);
26
+        m.tiles = setsize([], m.num_tiles[0]);
27
+        m.last_tile = [-1,-1];
28
+        m.last_type = data['tiles-type'];
29
+        m.initialize_grid();
30
+        return m;
31
+    },
32
+
33
+    setVisible : func (v) {
34
+        if (v != me.visibility) {
35
+            me.visibility = v;
36
+            me.group.setVisible(v);
37
+        }
38
+    },
39
+
40
+# initialize the map by setting up a grid of raster images
41
+    initialize_grid : func {
42
+        for(var x = 0; x < me.num_tiles[0]; x += 1) {
43
+            me.tiles[x] = setsize([], me.num_tiles[1]);
44
+            for(var y = 0; y < me.num_tiles[1]; y += 1)
45
+                me.tiles[x][y] = me.group.createChild('image', 'tile ' ~ x ~ ',' ~ y);
46
+        }
47
+    },
48
+
49
+# this is the callback that will be regularly called by the timer to update the map
50
+    update : func {
51
+         if (! me.visibility)
52
+             return;
53
+
54
+        var n = math.pow(2, me.device.data.zoom);
55
+        var offset = [
56
+            n * ((data.lon + 180) / 360) - me.center_tile_offset[0],
57
+            (1 - math.ln(math.tan(data.lat * math.pi/180) + 1 / math.cos(data.lat * math.pi/180)) / math.pi) / 2 * n - me.center_tile_offset[1]
58
+        ];
59
+        var tile_index = [int(offset[0]), int(offset[1])];
60
+
61
+        var ox = tile_index[0] - offset[0];
62
+        var oy = tile_index[1] - offset[1];
63
+
64
+        for (var x = 0; x < me.num_tiles[0]; x += 1)
65
+            for(var y = 0; y < me.num_tiles[1]; y += 1)
66
+                me.tiles[x][y]
67
+                    .setTranslation(
68
+                        int((ox + x) * me.tile_size + 0.5),
69
+                        int((oy + y) * me.tile_size + 0.5));
70
+
71
+        if (tile_index[0] != me.last_tile[0]
72
+         or tile_index[1] != me.last_tile[1]
73
+         or data['tiles-type'] != me.last_type) {
74
+            for(var x = 0; x < me.num_tiles[0]; x += 1)
75
+                for(var y = 0; y < me.num_tiles[1]; y += 1) {
76
+                    var pos = {
77
+                        z: me.device.data.zoom,
78
+                        x: int(offset[0] + x),
79
+                        y: int(offset[1] + y),
80
+                        type: data['tiles-type'],
81
+                        server : data['tiles-server'],
82
+                        apikey: data['tiles-apikey'],
83
+                    };
84
+
85
+                    (func {
86
+                        var img_path = me.makePath(pos);
87
+                        printlog('debug', 'img_path: ', img_path);
88
+                        var tile = me.tiles[x][y];
89
+
90
+                        if (io.stat(img_path) == nil) { # image not found, save in $FG_HOME
91
+                            var img_url = me.makeUrl(pos);
92
+                            printlog('debug', 'requesting ' ~ img_url);
93
+                            http.save(img_url, img_path)
94
+                                .done(func {printlog('info', 'received image ' ~ img_path); tile.set("src", img_path);})
95
+                                .fail(func (r) printlog('warn', 'Failed to get image ' ~ img_path ~ ' ' ~ r.status ~ ': ' ~ r.reason));
96
+                        }
97
+                        else { # cached image found, reusing
98
+                            printlog('debug', 'loading ' ~ img_path);
99
+                            tile.set("src", img_path);
100
+                        }
101
+                    })();
102
+                }
103
+            me.last_tile = tile_index;
104
+            me.last_type = data['tiles-type'];
105
+        }
106
+    },
107
+};
+3 -6
Nasal/softkeys.nas
... ...
@@ -58,16 +58,13 @@ var softkeysClass = {
58 58
         PFD : {
59 59
             INSET: {
60 60
                 OFF: func {
61
-                    me.device.map.tiles.setVisible(0);
62
-                    me.device.map.navaids.setVisible(0);
61
+                    me.device.map.setVisible(0);
63 62
                     me.device.display.screenElements['PFD-Map-bg'].hide();
64 63
                 },
65 64
                 hook : func {
66 65
                     me.device.display.screenElements['PFD-Map-bg'].show();
67
-                    me.device.map.tiles.setVisible(1);
68
-                    me.device.map.navaids.setVisible(1);
69
-                    me.device.map.tiles.update();
70
-                    me.device.map.navaids.update();
66
+                    me.device.map.setVisible(1);
67
+                    me.device.map.update();
71 68
                 },
72 69
             },
73 70
             PFD: {
+2
zkv1000.nas
... ...
@@ -6,6 +6,8 @@ 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
+    'maps/navaids.nas',
10
+    'maps/tiles.nas',
9 11
     'map.nas',     # moves the maps
10 12
     'display.nas',
11 13
     'menu.nas',    # manage windows