1 contributor
# vim: set foldmethod=marker foldmarker={{{,}}} :
var mapClass = {
# main class for maps {{{
new : func (device) {
var m = { parents : [ mapClass ] };
m.device = device;
m.device.data.mapview = [
m.device.display.display.get('view[0]'),
m.device.display.display.get('view[1]')
];
m.device.data.mapsize = [
m.device.display.display.get('size[0]'),
m.device.display.display.get('size[1]')
];
m.device.data.zoom = 10;
m.device.data.orientation = {
text: 'NORTH UP',
map: 0,
airplane: data.hdg,
};
m.changeZoom();
m.group = m.device.display.display.createGroup()
.set('clip',
'rect('
~ m.device.data.mapclip.top ~','
~ m.device.data.mapclip.right ~','
~ m.device.data.mapclip.bottom ~','
~ m.device.data.mapclip.left ~')')
.setCenter(
(m.device.data.mapclip.right + m.device.data.mapclip.left) / 2,
(m.device.data.mapclip.bottom + m.device.data.mapclip.top) / 2);
m.tiles = MapTiles.new(m.device, m.group);
m.navaids = MapNavaids.new(m.device, m.group);
# m.device.display.display.createGroup().createChild('path')
# .setColor(1,0,0)
# .setColorFill(1,0,0)
# .setStrokeLineWidth(2)
# .moveTo(
# m.device.data.mapclip.left + (m.device.data.mapclip.right-m.device.data.mapclip.left)/2,
# m.device.data.mapclip.top +50)
# .vertTo(
# m.device.data.mapclip.bottom -50)
# .close()
# .moveTo(
# m.device.data.mapclip.left +50,
# m.device.data.mapclip.top + (m.device.data.mapclip.bottom-m.device.data.mapclip.top)/2)
# .horizTo(
# m.device.data.mapclip.right-50)
# .close();
return m;
},
changeZoom : func (d = 0) {
me.device.data.zoom = math.max(2, math.min(19, me.device.data.zoom + d));
me.device.data['range-nm'] = me.device.display.display.get('view[1]') / 2 * 84.53 * math.cos(data.lat) / math.pow(2, me.device.data.zoom);
me.device.data['range-factor'] = math.pow(2,13 - me.device.data.zoom) * 1.6875;
},
update : func {
if (me.device.data.orientation.text == 'NORTH UP') {
me.device.data.orientation.map = 0;
me.device.data.orientation.airplane = data.hdg;
}
elsif (me.device.data.orientation.text == 'TRK UP') {
if (data.wow) {
me.device.data.orientation.map = -data.hdg;
me.device.data.orientation.airplane = data.hdg;
}
else {
var track = getprop('/orientation/track-deg');
me.device.data.orientation.map = -track;
me.device.data.orientation.airplane = data.hdg;
}
}
elsif (me.device.data.orientation.text == 'DTK UP') {
me.device.data.orientation.map = 0;
me.device.data.orientation.airplane = 0;
}
elsif (me.device.data.orientation.text == 'HDG UP') {
me.device.data.orientation.map = -data.hdg;
me.device.data.orientation.airplane = data.hdg;
}
me.group.setRotation(me.device.data.orientation.map * D2R);
me.tiles.update();
me.navaids.update();
},
setVisible : func (v) {
me.tiles.setVisible(v);
me.navaids.setVisible(v);
},
};
# }}}
var MapTiles = {
# displays maps background from web tiles {{{
# code from http://wiki.flightgear.org/Canvas_Snippets#A_simple_tile_map
new : func (device, group) {
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.visibility = m.device.role == 'MFD';
m.group = group.createChild('group', 'tiles')
.setTranslation(
m.device.role == 'MFD' ? (m.device.data.mapview[0] - m.device.data.mapsize[0] + m.device.data.mapclip.left)/2 : -520,
m.device.role == 'MFD' ? -250 : -45)
.setVisible(m.visibility);
m.tiles = setsize([], m.num_tiles[0]);
m.last_tile = [-1,-1];
m.last_type = data['tiles-type'];
m.initialize_grid();
return m;
},
setVisible : func (v) {
if (v != me.visibility) {
me.visibility = v;
me.group.setVisible(v);
}
},
# 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
update : func {
if (! me.visibility)
return;
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 > 32 ? 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 <= 32) { #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)
.setRotation(-mapOptions.orientation * D2R);
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 <= 32) {
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 <= 32)
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, mapOptions) {
mapIconCache.boundIconToImage('Navaid_' ~ type, me._can.image);
me._can.label
.setText(navaid.id)
.setRotation(-mapOptions.orientation * D2R);
me._can.group.setGeoPosition(navaid.lat, navaid.lon);
},
setVisible : func (visibility) {
me._can.group.setVisible(visibility);
},
};
# }}}
var MapAirplaneItem = {
# set airplane on ND {{{
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 (orientation) {
mapIconCache.boundIconToImage('airplane', me._can.image);
me._can.group
.setGeoPosition(data.lat, data.lon)
.setRotation(orientation * 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 MapNavaids = {
# the layer to show navaids, airports and airplane symbol {{{
new : func (device, group) {
var m = {parents : [MapNavaids]};
m._model = nil;
m.device = device;
m._visibility = m.device.role == 'MFD';
m._group = group.createChild('map', 'MFD map')
.setTranslation(
m.device.role == 'MFD' ? (m.device.data.mapview[0] + m.device.data.mapclip.left)/2 : 120,
m.device.role == 'MFD' ? 400 : 600)
.setVisible(m._visibility);
m._group._node
.getNode('range', 1).setDoubleValue(m.device.data['range-factor']);
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 : m.device.data.orientation.map,
};
m._results = nil;
return m;
},
update : func {
me._group._node.getNode('ref-lat', 1).setDoubleValue(data.lat);
me._group._node.getNode('ref-lon', 1).setDoubleValue(data.lon);
me._group._node
.getNode('range', 1).setDoubleValue(me.device.data['range-factor']);
me._mapOptions.orientation = me.device.data.orientation.map;
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, me._mapOptions);
}
else {
item = MapNavaidItem.new(me._cache[type].index, type);
item.create(me._can[type]);
item.setData(n, type, me._mapOptions);
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.device.data.orientation.airplane);
me._cache.airplane.item.setVisible(1);
},
};
# }}}