zkv1000 / Nasal / map.nas /
ac86bc6 7 years ago
1 contributor
630 lines | 22.911kb
# vim: set foldmethod=marker foldmarker={{{,}}} :
var MapTiles = {
# displays maps background from web tiles {{{
# code from http://wiki.flightgear.org/Canvas_Snippets#A_simple_tile_map
    new : func (device) {
        var m = { parents: [MapTiles] };
        m.device = device;
        m.display = m.device.display.display;
        m.tile_size = 256;
        m.maps_base = getprop("/sim/fg-home") ~ '/cache/maps';
        m.makeUrl = string.compileTemplate('https://{server}/{type}/{z}/{x}/{y}.png{apikey}');
        m.makePath = string.compileTemplate(m.maps_base ~ '/{server}/{type}/{z}/{x}/{y}.png');
        m.num_tiles = [
            math.ceil( m.device.data.mapsize[0] / m.tile_size ) + 1,
            math.ceil( m.device.data.mapsize[1] / m.tile_size ) + 1
        ];
        m.center_tile_offset = [
            (m.num_tiles[0] - 1) / 2,
            (m.num_tiles[1] - 1) / 2
        ];
        m.group = m.display.createGroup()
            .setCenter(
                    m.device.data.mapsize[0] / 2 + m.device.data.mapclip.left,
                    m.device.data.mapsize[1] / 2 + m.device.data.mapclip.top
                )
            .setTranslation(
                    m.device.data.mapclip.left / 2,
                    m.device.data.mapclip.top / 2
                )
            .set('clip',
                    'rect('
                        ~ m.device.data.mapclip.top ~','
                        ~ m.device.data.mapclip.right ~','
                        ~ m.device.data.mapclip.bottom ~','
                        ~ m.device.data.mapclip.left ~')'
                );
        m.tiles = setsize([], m.num_tiles[0]);
        m.last_tile = [-1,-1];
        m.last_type = data['tiles-type'];
        return m;
    },

# Simple user interface (Buttons for zoom and label for displaying it)
    changeZoom : func (d) {
        me.device.data.zoom = math.max(2, math.min(19, me.device.data.zoom + d));
        me.device.data['range-nm'] = me.display.get('view[1]') / 2 * 84.53 * math.cos(data.lat) / math.pow(2, me.device.data.zoom);
    },

# initialize the map by setting up a grid of raster images
    initialize_grid : func {
        for(var x = 0; x < me.num_tiles[0]; x += 1) {
            me.tiles[x] = setsize([], me.num_tiles[1]);
            for(var y = 0; y < me.num_tiles[1]; y += 1)
                me.tiles[x][y] = me.group.createChild('image', 'tile ' ~ x ~ ',' ~ y);
        }
    },

# this is the callback that will be regularly called by the timer to update the map
    updateTiles : func {
#        me.group.setRotation(-data.hdg * D2R);

        var n = math.pow(2, me.device.data.zoom);
        var offset = [
            n * ((data.lon + 180) / 360) - me.center_tile_offset[0],
            (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]
        ];
        var tile_index = [int(offset[0]), int(offset[1])];

        var ox = tile_index[0] - offset[0];
        var oy = tile_index[1] - offset[1];

        for(var x = 0; x < me.num_tiles[0]; x += 1)
            for(var y = 0; y < me.num_tiles[1]; y += 1)
                me.tiles[x][y]
                    .setTranslation(int((ox + x) * me.tile_size + 0.5), int((oy + y) * me.tile_size + 0.5));

        if(    tile_index[0] != me.last_tile[0]
                or tile_index[1] != me.last_tile[1]
                or data['tiles-type'] != me.last_type ) {
            for(var x = 0; x < me.num_tiles[0]; x += 1)
                for(var y = 0; y < me.num_tiles[1]; y += 1) {
                    var pos = {
                        z: me.device.data.zoom,
                        x: int(offset[0] + x),
                        y: int(offset[1] + y),
                        type: data['tiles-type'],
                        server : data['tiles-server'],
                        apikey: data['tiles-apikey'],
                    };

                    (func {
                        var img_path = me.makePath(pos);
                        printlog('debug', 'img_path: ', img_path);
                        var tile = me.tiles[x][y];

                        if( io.stat(img_path) == nil ) { # image not found, save in $FG_HOME
                            var img_url = me.makeUrl(pos);
                            printlog('debug', 'requesting ' ~ img_url);
                            http.save(img_url, img_path)
                                .done(func {printlog('info', 'received image ' ~ img_path); tile.set("src", img_path);})
                                .fail(func (r) printlog('warn', 'Failed to get image ' ~ img_path ~ ' ' ~ r.status ~ ': ' ~ r.reason));
                        }
                        else { # cached image found, reusing
                            printlog('debug', 'loading ' ~ img_path);
                            tile.set("src", img_path);
                        }
                    })();
                }
            me.last_tile = tile_index;
            me.last_type = data['tiles-type'];
        }
    },
};
# }}}

# The following is largely inspired from the Extra500 Avidyne Entegra 9 
# https://gitlab.com/extra500/extra500.git
# Many thanks to authors: Dirk Dittmann and Eric van den Berg

var MapIconCache = {
# creates at init an icons cache for navaids, airports and airplane {{{
    new : func (svgFile) {
        var m = { parents:[MapIconCache] };

        m._canvas = canvas.new( {
            'name': 'MapIconCache',
            'size': [512, 512],
            'view': [512, 512],
            'mipmapping': 1
        });
        m._canvas.addPlacement( {'type': 'ref'} );
        m._canvas.setColorBackground(1,1,1,0);
        m._group = m._canvas.createGroup('MapIcons');

        canvas.parsesvg(m._group, data.zkv1000_reldir ~ svgFile);

        m._sourceRectMap = {};

        var icons = [ 'airplane' ];

        foreach (var near; [0, 1])
            foreach (var surface; [0, 1])
                foreach (var tower; [0, 1])
                    foreach (var center; tower ? [0, 1] : [ 0 ])
                        append(icons, 'Airport_' ~ near ~ surface ~ tower ~ center);

        foreach (var type; ['VOR', 'DME', 'TACAN', 'NDB'])
            append(icons, 'Navaid_' ~ type);

        foreach (var i; icons)
            m.registerIcon(i);

        return m;
    },
    registerIcon : func (id) {
        me._sourceRectMap[id] = {
            'bound' : [],
            'size'  : [],
        };
        var element = me._group.getElementById(id);
        if (element != nil) {
            me._sourceRectMap[id].bound = element.getTransformedBounds();
            # TODO ugly hack ? check for reason!
            var top     = 512 - me._sourceRectMap[id].bound[3];
            var bottom  = 512 - me._sourceRectMap[id].bound[1];
            me._sourceRectMap[id].bound[1] = top;
            me._sourceRectMap[id].bound[3] = bottom;

            me._sourceRectMap[id].size = [
                me._sourceRectMap[id].bound[2] - me._sourceRectMap[id].bound[0],
                me._sourceRectMap[id].bound[3] - me._sourceRectMap[id].bound[1]
            ];
        }
        else {
            print('MapIconCache.registerIcon(' ~ id ~ ') fail');
        }
    },
    getBounds : func (id) {
        return me._sourceRectMap[id].bound;
    },
    getSize : func (id) {
        return me._sourceRectMap[id].size;
    },
    boundIconToImage : func (id, image, center=1) {
        if (!contains(me._sourceRectMap, id)) {
            print('MapIconCache.boundIconToImage('~id~') ... no available.');
            id = 'Airport_0001';
        }
        image.setSourceRect(
                me._sourceRectMap[id].bound[0],
                me._sourceRectMap[id].bound[1],
                me._sourceRectMap[id].bound[2],
                me._sourceRectMap[id].bound[3],
                0);
        image.setSize(
                me._sourceRectMap[id].size[0],
                me._sourceRectMap[id].size[1]);
        if (center) {
            image.setTranslation(
                    -me._sourceRectMap[id].size[0]/2,
                    -me._sourceRectMap[id].size[1]/2);
        }
    },
};

var mapIconCache = MapIconCache.new('Models/MapIcons.svg');
# }}}

var MapAirportItem = {
# manage airports items by adding the ID and runways on associated icon {{{
    new : func (id) {
        var m = {parents:[MapAirportItem]};
        m._id = id;
        m._can = {
            'group' : nil,
            'label' : nil,
            'image' : nil,
            'layout': nil,
            'runway': [],
        };
        m._mapAirportIcon = {
            'near'      : 0,
            'surface'   : 0,
            'tower'     : 0,
            'center'    : 0,
            'displayed' : 0,
            'icon'      : '',
        };
        return m;
    },
    create : func (group) {
        me._can.group = group
            .createChild('group', 'airport_' ~ me._id);

        me._can.image = me._can.group.createChild('image', 'airport-image_' ~ me._id)
            .setFile(mapIconCache._canvas.getPath())
            .setSourceRect(0,0,0,0,0);

        me._can.label = me._can.group.createChild('text', 'airport-label_' ~ me._id)
            .setDrawMode( canvas.Text.TEXT )
            .setTranslation(0, 37)
            .setAlignment('center-bottom-baseline')
            .setFont('LiberationFonts/LiberationSans-Regular.ttf')
            .setFontSize(24);

        me._can.label.set('fill','#BACBFB');
        me._can.label.set('stroke','#000000');

        me._can.layout = group.createChild('group','airport_layout' ~ me._id);
        me._can.layoutIcon = group.createChild('group','airport_layout_Icon' ~ me._id);
        return me._can.group;
    },
    draw : func (apt, mapOptions) {
        me._mapAirportIcon.near = mapOptions.range > 30 ? 0 : 1;
        me._mapAirportIcon.surface  = 0;
        me._mapAirportIcon.tower    = 0;
        me._mapAirportIcon.center   = 0;
        me._mapAirportIcon.displayed    = 0;

        # TODO make departure and destination airports specific
        var aptInfo = airportinfo(apt.id);

        me._can.layout.removeAllChildren();
        me._can.layoutIcon.removeAllChildren();

        me._mapAirportIcon.tower = (size(aptInfo.comms('tower')) > 0);
        me._mapAirportIcon.center = me._mapAirportIcon.tower and (size(aptInfo.comms('approach')) > 0);

        foreach (var rwy; keys(aptInfo.runways)) {
            var runway = aptInfo.runways[rwy];
            me._mapAirportIcon.surface = MAP_RUNWAY_SURFACE[runway.surface] ? 1 : me._mapAirportIcon.surface;
            me._mapAirportIcon.displayed = runway.length > mapOptions.runwayLength ? 1 : me._mapAirportIcon.displayed;

            if (mapOptions.range <= 10) {    # drawing real runways
                me._can.layout.createChild('path', 'airport-runway-' ~ me._id ~ '-' ~ runway.id)
                    .setStrokeLineWidth(7)
                    .setColor(1,1,1)
                    .setColorFill(1,1,1)
                    .setDataGeo([
                        canvas.Path.VG_MOVE_TO,
                        canvas.Path.VG_LINE_TO,
                        canvas.Path.VG_CLOSE_PATH
                    ],[
                        'N' ~ runway.lat, 'E' ~ runway.lon,
                        'N' ~ runway.reciprocal.lat, 'E' ~ runway.reciprocal.lon,
                    ]);
            }
            elsif (mapOptions.range <= 30) {     #draw icon runways
                me._can.layoutIcon.setGeoPosition(apt.lat, apt.lon);
                me._can.layoutIcon.createChild('path', 'airport-runway-' ~ me._id ~ '-' ~ runway.id)
                    .setStrokeLineWidth(7)
                    .setColor(1,1,1)
                    .setColorFill(1,1,1)
                    .setData([
                        canvas.Path.VG_MOVE_TO,
                        canvas.Path.VG_LINE_TO,
                        canvas.Path.VG_CLOSE_PATH
                    ],[
                        0, -20,
                        0, 20,
                    ])
                    .setRotation((runway.heading)* D2R);
            }
        }
        me._mapAirportIcon.icon = 'Airport_'
            ~ me._mapAirportIcon.near
            ~ me._mapAirportIcon.surface
            ~ me._mapAirportIcon.tower
            ~ me._mapAirportIcon.center;

        if (me._mapAirportIcon.displayed) {
            me._can.label.setText(apt.id);
            me._can.group.setGeoPosition(apt.lat, apt.lon);
            if (mapOptions.range <= 10) {
                me._can.image.setVisible(0);
                me._can.layout.setVisible(1);
            }
            elsif (mapOptions.range <= 30) {
                mapIconCache.boundIconToImage(me._mapAirportIcon.icon, me._can.image);
                me._can.image.setVisible(1);
                me._can.layout.setVisible(1);
            }
            else {
                mapIconCache.boundIconToImage(me._mapAirportIcon.icon, me._can.image);
                me._can.layout.setVisible(0);
                me._can.image.setVisible(1);
            }
            me._can.group.setVisible(1);
        }
        return me._mapAirportIcon.displayed;
    },
    update : func (mapOptions) {
        if (mapOptions.range <= 10) { }
        elsif (mapOptions.range <= 30)
            me._can.layoutIcon.setRotation(-mapOptions.orientation * D2R);
        else { }
    },
    setVisible : func (visibility) {
        me._can.group.setVisible(visibility);
        me._can.layout.setVisible(visibility);
        me._can.image.setVisible(visibility);
        me._can.layoutIcon.setVisible(visibility);
    },
};
# }}}

var MapNavaidItem = {
# manage navaids items by adding ID in the icon {{{
    new : func (id, type) {
        var m = {parents:[MapNavaidItem]};
        m._id = id;
        m._type = type;
        m._can = {
            'group' : nil,
            'label' : nil,
            'image' : nil,
        };
        return m;
    },
    create : func (group) {
        me._can.group = group
            .createChild('group', me._type ~ '_' ~ me._id);

        me._can.image = me._can.group.createChild('image', me._type ~ '-image_' ~ me._id)
            .setFile(mapIconCache._canvas.getPath())
            .setSourceRect(0,0,0,0,0);

        me._can.label = me._can.group.createChild('text', me._type ~ '-label_' ~ me._id)
            .setDrawMode( canvas.Text.TEXT )
            .setTranslation(0,42)
            .setAlignment('center-bottom-baseline')
            .setFont('LiberationFonts/LiberationSans-Regular.ttf')
            .setFontSize(24);

        me._can.label.set('fill','#BACBFB');
        me._can.label.set('stroke','#000000');

        return me._can.group;
    },
    setData : func (navaid, type) {
        mapIconCache.boundIconToImage('Navaid_' ~ type, me._can.image);
        me._can.label.setText(navaid.id);
        me._can.group.setGeoPosition(navaid.lat, navaid.lon);
    },
    setVisible : func (visibility) {
        me._can.group.setVisible(visibility);
    },
};
# }}}

var MapAirplaneItem = {
    new : func {
        var m = {parents:[MapAirplaneItem]};
        m._can = {
            'group' : nil,
            'image' : nil,
        };
        return m;
    },
    create : func (group) {
        me._can.group = group
            .createChild('group', 'airplane');
        me._can.image = me._can.group.createChild('image', 'airplane-image')
            .setFile(mapIconCache._canvas.getPath())
            .setSourceRect(0,0,0,0,0);
        return me._can.group;
    },
    setData : func {
        mapIconCache.boundIconToImage('airplane', me._can.image);
        me._can.group
            .setGeoPosition(data.lat, data.lon)
            .setRotation(data.hdg * D2R);
    },
    setVisible : func (visibility) {
        me._can.group.setVisible(visibility);
    },
};

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};
var MAP_RUNWAY_AT_RANGE = func (range) {
    if (range < 40) return 0;
    if (range < 50) return 250;
    if (range < 80) return 500;
    if (range < 160) return 1000;
    if (range < 240) return 3000;
    return 3000;
}
var MAP_TXRANGE_VOR = func (range) {
    if (range < 40) return 0;
    if (range < 50) return 20;
    if (range < 80) return 25;
    if (range < 160) return 30;
    if (range < 240) return 50;
    return 100;
}
####
# Declutter
#   land
#       0 : 'Terrain'
#       1 : 'Political boundaries'
#       2 : 'River/Lakes/Oceans'
#       3 : 'Roads'
#   Nav
#       0 : 'Airspace'
#       1 : 'Victor/Jet airways'
#       2 : 'Obstacles'
#       3 : 'Navaids'

var PositionedLayer = {
# the layer to show navaids, airports and airplane symbol {{{
    new : func (device) {
        var m = {parents : [PositionedLayer]};

        m._model = nil;
        m.device = device;
        var display = m.device.display.display;
        m._group = display.createGroup().createChild('map', 'MFD map')
            .set('clip',
                    'rect('
                        ~ m.device.data.mapclip.top ~','
                        ~ m.device.data.mapclip.right ~','
                        ~ m.device.data.mapclip.bottom ~','
                        ~ m.device.data.mapclip.left ~')');
        m._group
            .setTranslation(
                    m.device.data.mapsize[0] / 2 + m.device.data.mapclip.left,
                    m.device.data.mapsize[1] / 2 + m.device.data.mapclip.top
                );
        m._group._node.getNode('range', 1)
            .setDoubleValue(13.5); # TODO find a far less esoteric way to get range value

        m._can = {};
        m._cache = {};
        foreach (var n; ['airport', 'VOR', 'TACAN', 'NDB', 'DME']) {
            m._can[n] = m._group.createChild('group', n);
            m._cache[n] = {
                'data' : [],
                'index' : 0,
                'max' : 100,
            };
        }
        m._can['airplane'] = m._group.createChild('group', 'airplane');
        m._cache['airplane'] = {
            displayed : 0,
            item : nil,
        };

        m._mapOptions = {
            declutterLand : 3,
            declutterNAV  : 3,
            lightning     : 0,
            reports       : 0,
            overlay       : 0,
            range         : m.device.data['range-nm'],
            rangeLow      : m.device.data['range-nm'] / 2,
            runwayLength  : -1,
            orientation   : 0,
        };

        m._results = nil;

        m._visibility = 0;

        return m;
    },
    update : func {
        me._group._node.getNode('ref-lat', 1).setDoubleValue(data.lat);
        me._group._node.getNode('ref-lon', 1).setDoubleValue(data.lon);

        if (me._visibility == 1) {
            me.loadAirport();
            foreach (var n; ['VOR', 'TACAN', 'NDB', 'DME'])
                me.loadNavaid(n);
            me.loadAirplane();
        }
    },
    _onVisibilityChange : func {
        me._group.setVisible(me._visibility);
    },
    setMapOptions : func (mapOptions) {
        me._mapOptions = mapOptions;
        me.update();
    },
    updateOrientation : func (value) {
        me._mapOptions.orientation = value;
        for (var i = 0 ; i < me._cache.airport.index ; i +=1) {
            item = me._cache.airport.data[i];
            item.update(me._mapOptions);
        }
    },
    setRange : func (range=100) {
        me._mapOptions.range = me.device.data['range-nm'];
        me._mapOptions.rangeLow = me._mapOptions.range/2;
        me.update();
    },
    setRotation : func (deg) {
        me._group.setRotation(deg * D2R);
    },
    setVisible : func (v) {
        if (me._visibility != v) {
            me._visibility = v;
            me._onVisibilityChange();
        }
    },
    _onVisibilityChange : func {
        me._group.setVisible(me._visibility);
    },
    # positioned.findWithinRange : any, fix, vor, ndb, ils, dme, tacan

    loadAirport : func {
        me._cache.airport.index = 0;
        var results = positioned.findWithinRange(me._mapOptions.range * 2.5, 'airport');
        var item = nil;

        if (me._mapOptions.declutterNAV >= 2)
            me._mapOptions.runwayLength = MAP_RUNWAY_AT_RANGE(me._mapOptions.range);
        elsif (me._mapOptions.declutterNAV >= 1)
            me._mapOptions.runwayLength = 2000;
        else
            me._mapOptions.runwayLength = 3000;

        if (me._mapOptions.runwayLength >= 0) {
            foreach (var apt; results) {
                if (me._cache.airport.index >= me._cache.airport.max )
                    break;

                if (size(me._cache.airport.data) > me._cache.airport.index)
                    item = me._cache.airport.data[me._cache.airport.index];
                else {
                    item = MapAirportItem.new(me._cache.airport.index);
                    item.create(me._can.airport);
                    append(me._cache.airport.data, item);
                }

                if (item.draw(apt, me._mapOptions)) {
                    item.setVisible(1);
                    me._cache.airport.index += 1;
                }
            }
        }

        for (var i = me._cache.airport.index ; i < size(me._cache.airport.data) ; i +=1) {
            item = me._cache.airport.data[i];
            item.setVisible(0);
        }
    },
    loadNavaid : func (type) {
        me._cache[type].index = 0;
        if (me._mapOptions.declutterNAV >= 3) { # TODO test for DME and NDB range < 100nm
            var range = me._mapOptions.range * 2.5;
            var txRange = MAP_TXRANGE_VOR(me._mapOptions.range);
            var results = positioned.findWithinRange(range, type);
            var item = nil;
            foreach (var n; results) {
                if (n.range_nm < txRange)
                    break;

                if (me._cache[type].index >= me._cache[type].max )
                    break;

                if (size(me._cache[type].data) > me._cache[type].index) {
                    item = me._cache[type].data[me._cache[type].index];
                    item.setData(n, type);
                }
                else {
                    item = MapNavaidItem.new(me._cache[type].index, type);
                    item.create(me._can[type]);
                    item.setData(n, type);
                    append(me._cache[type].data, item);
                }
                item.setVisible(1);
                me._cache[type].index += 1;
            }
        }
        for (var i = me._cache[type].index ; i < size(me._cache[type].data) ; i +=1) {
            item = me._cache[type].data[i];
            item.setVisible(0);
        }
    },
    loadAirplane : func {
        if (!me._cache.airplane.displayed) {
            me._cache.airplane.item = MapAirplaneItem.new();
            me._cache.airplane.item.create(me._can['airplane']);
            me._cache.airplane.displayed = 1;
        }
        me._cache.airplane.item.setData();
        me._cache.airplane.item.setVisible(1);
    },
};
# }}}