1
0
Fork 0

Add Canvas Map Support for Slippy Map

- Include OSM and OpenAIP in the canvas-map dialog.
This commit is contained in:
Stuart Buchanan 2017-09-28 15:38:03 +01:00
parent cfa967db1d
commit 85b7665c19
7 changed files with 472 additions and 27 deletions

View file

@ -16,7 +16,7 @@
## - parents
## - __self__
## - del (managing all listeners and timers)
## - searchCmd -> filtering
## - searchCmd -> filtering
##
## APIs to be wrapped for each layer:
## printlog(), die(), debug.bt(), benchmark()
@ -79,10 +79,16 @@ var MapStructure_selfTest = func() {
# TODO: we'll need some z-indexing here, right now it's just random
# TODO: use foreach/keys to show all layers in this case by traversing SymbolLayer.registry direclty ?
# maybe encode implicit z-indexing for each lcontroller ctor call ? - i.e. preferred above/below order ?
foreach(var type; [r('TFC',0),r('APT'),r('DME'),r('VOR'),r('NDB'),r('FIX',0),r('RTE'),r('WPT'),r('FLT'),r('WXR'),r('APS'), ] )
foreach(var type; [r('TFC',0),r('APT'),r('DME'),r('VOR'),r('NDB'),r('FIX',0),r('RTE'),r('WPT'),r('FLT'),r('WXR'),r('APS'), ] )
TestMap.addLayer(factory: canvas.SymbolLayer, type_arg: type.name,
visible: type.vis, priority: type.zindex,
);
foreach(var type; [ r('OSM'), r('OpenAIP') ]) {
TestMap.addLayer(factory: canvas.OverlayLayer, type_arg: type.name,
visible: type.vis, priority: type.zindex,
style: Styles.get(type.name),
options: Options.get(type.name) );
}
}; # MapStructure_selfTest
@ -854,7 +860,7 @@ var LineSymbol = {
var path = me.model;
if(typeof(path) == 'hash'){
path = me.model.path;
if(path == nil)
if(path == nil)
__die("LineSymbol model requires a 'path' member (vector)");
}
foreach (var m; path) {
@ -862,7 +868,7 @@ var LineSymbol = {
var (lat,lon) = me.controller.getpos(m);
append(coords,"N"~lat);
append(coords,"E"~lon);
append(cmds,cmd);
append(cmds,cmd);
cmd = canvas.Path.VG_LINE_TO;
} else {
cmd = canvas.Path.VG_MOVE_TO;
@ -934,7 +940,7 @@ var SymbolLayer = {
# print("SymbolLayer setup options:", m.options!=nil);
m.style = default_hash(style, m.df_style);
m.options = default_hash(options, m.df_options);
if (controller == nil)
controller = m.df_controller;
assert_m(controller, "parents");
@ -1074,15 +1080,15 @@ var MultiSymbolLayer = {
}
return 0;
},
searchCmd: func() {
if (me.map.getPosCoord() == nil or me.map.getRange() == nil) {
searchCmd: func() {
if (me.map.getPosCoord() == nil or me.map.getRange() == nil) {
print("Map not yet initialized, returning empty result set!");
return []; # handle maps not yet fully initialized
}
var result = me.controller.searchCmd();
# some hardening
var type=typeof(result);
if(type != 'nil' and type != 'vector')
if(type != 'nil' and type != 'vector')
__die("MultiSymbolLayer: searchCmd() method MUST return a vector of valid positioned ghosts/Geo.Coord objects or nil! (was:"~type~")");
return result;
},
@ -1127,7 +1133,7 @@ var NavaidSymbolLayer = {
###
## TODO: wrappers for Horizontal vs. Vertical layers ?
##
##
var SingleSymbolLayer = {
parents: [SymbolLayer],
@ -1177,6 +1183,306 @@ var SingleSymbolLayer = {
},
}; # of SingleSymbolLayer
##
# Base class for a OverlayLayer, e.g. a TileLayer
#
var OverlayLayer = {
# Default implementations/values:
df_controller: nil, # default controller
df_priority: nil, # default priority for display sorting
df_style: nil,
df_options: nil,
type: nil, # type of #Symbol to add (MANDATORY)
id: nil, # id of the group #canvas.Element (OPTIONAL)
# Static/singleton:
registry: {},
add: func(type, class) {
me.registry[type] = class;
},
get: func(type) {
foreach(var invalid; var invalid_types = [nil,'vector','hash'])
if ( (var t=typeof(type)) == invalid) __die(" invalid OverlayLayer type (non-scalar) of type:"~t);
if ((var class = me.registry[type]) == nil)
__die("OverlayLayer.get(): unknown type '"~type~"'");
else return class;
},
# Calls corresonding layer constructor
# @param group #Canvas.Group to place this on.
# @param map The #Canvas.Map this is a member of.
# @param style An alternate style.
# @param options Extra options/configurations.
# @param visible Initially set it up as visible or invisible.
new: func(type, group, map, controller=nil, style=nil, options=nil, visible=1, arg...) {
# XXX: Extra named arguments are (obviously) not preserved well...
if (me == nil) __die("OverlaySymbolLayer constructor needs to know its parent class");
var ret = call((var class = me.get(type)).new, [group, map, controller, style, options, visible], class);
ret.type = type;
ret.group.set("layer-type", type);
return ret;
},
# Private constructor:
_new: func(m, style, controller, options) {
m.style = default_hash(style, m.df_style);
m.options = default_hash(options, m.df_options);
if (controller == nil) {
if (m.df_controller == nil) {
controller = OverlayLayer.Controller;
} else {
controller = m.df_controller;
}
}
assert_m(controller, "parents");
if (controller.parents[0] == OverlayLayer.Controller)
controller = controller.new(m);
assert_m(controller, "parents");
assert_m(controller.parents[0], "parents");
if(options != nil){
var listeners = opt_member(controller, 'listeners');
var listen = opt_member(options, 'listen');
if (listen != nil and listeners != nil){
var listen_tp = typeof(listen);
if(listen_tp != 'vector' and listen_tp != 'scalar')
__die("Options 'listen' cannot be a "~ listen_tp);
if(typeof(listen) == 'scalar')
listen = [listen];
foreach(var node_name; listen){
var node = opt_member(options, node_name);
if(node == nil)
node = node_name;
append(controller.listeners,
setlistener(node, func call(m.update,[],m),0,0));
}
}
}
m.controller = controller;
},
# For instances:
del: func() if (me.controller != nil) { me.controller.del(); me.controller = nil },
update: func() { _die("Abstract OverlayLayer.update() not implemented for this Layer"); },
};
var TileLayer = {
parents: [OverlayLayer],
# Default implementations/values:
# @param group A group to place this on.
# @param map The #Canvas.Map this is a member of.
# @param controller A controller object (parents=[OverlayLayer.Controller])
# or implementation (parents[0].parents=[OverlayLayer.Controller]).
# @param style An alternate style.
# @param options Extra options/configurations.
# @param visible Initially set it up as visible or invisible.
new: func(group, map, controller=nil, style=nil, options=nil, visible=1) {
if (me == nil) __die("TileLayer constructor needs to know its parent class");
var m = {
parents: [me],
map: map,
group: group.createChild("group", me.type),
maps_base: "",
num_tiles: [5,5],
makeURL: nil,
makePath: nil,
center_tile_offset : [],
tile_size: 256,
zoom: 9,
tile_type: "map",
last_tile_type: "map",
last_tile : [-1,-1],
tiles: [],
};
# Determine the number of tiles dynamically based on the canvas size
#var width = map.getCanvas().get("size[0]");
#var height = map.getCanvas().get("size[1]");
#m.num_tiles= [ math.ceil(width / m.tile_size),
# math.ceil(height / m.tile_size) ];
m.maps_base = getprop("/sim/fg-home") ~ '/cache/maps';
m.tiles = setsize([], m.num_tiles[0]);
m.center_tile_offset = [
(m.num_tiles[0] - 1.0) / 2.0,
(m.num_tiles[1] - 1.0) / 2.0
];
append(m.parents, m.group);
m.setVisible(visible);
OverlayLayer._new(m, style, controller, options);
#m.group.setCenter(0,0);
for(var x = 0; x < m.num_tiles[0]; x += 1)
{
m.tiles[x] = setsize([], m.num_tiles[1]);
for(var y = 0; y < m.num_tiles[1]; y += 1) {
m.tiles[x][y] = m.group.createChild("image", "map-tile");
}
}
m.update();
return m;
},
updateLayer: func()
{
# get current position
var lat = me.map.getLat();
var lon = me.map.getLon();
var range_nm = me.map.getRange();
var screen_range = me.map.getScreenRange();
if (screen_range == nil) screen_range = 200;
# Screen resolution m/pixel is range/screen_range
var screen_resolution = range_nm * globals.NM2M / screen_range;
# Slippy map resolution is
# 156543.03 meters/pixel * cos(latitude) / (2 ^ zoomlevel)
# Determine the closest zoom level and scaling ratio. Each increase in zoom level doubles resolution.
var ideal_zoom = math.ln(156543.03 * math.cos(lat * math.pi/180.0) / screen_resolution) / math.ln(2);
me.zoom = math.ceil(ideal_zoom);
var ratio = 1 / math.pow(2,me.zoom - ideal_zoom);
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((x - me.center_tile_offset[0]) * me.tile_size * ratio + 0.5),
int((y - me.center_tile_offset[1]) * me.tile_size * ratio + 0.5));
me.tiles[x][y].setScale(ratio);
me.tiles[x][y].scale_factor = ratio;
}
}
#var heading = me.map.getHdg();
#me.group.setRotation(heading);
var ymax = math.pow(2, me.zoom);
# Slippy map location of center point
var slippy_center = [
math.floor(ymax * ((lon + 180.0) / 360.0)),
math.floor((1 - math.ln(math.tan(lat * math.pi/180.0) + 1 / math.cos(lat * math.pi/180.0)) / math.pi) / 2.0 * ymax)
];
# This is the Slippy Map location of the 0,0 tile
var offset = [slippy_center[0] - me.center_tile_offset[0],
slippy_center[1] - me.center_tile_offset[1]];
var tile_index = [math.floor(offset[0]), math.floor(offset[1])];
# Find the lon, lat of the center tile
var center_tile_lon = slippy_center[0]/ymax * 360.0 - 180.0;
var nn = math.pi - 2.0 * math.pi * slippy_center[1]/ ymax;
var center_tile_lat = 180.0 / math.pi * math.atan(0.5 * (math.exp(nn) - math.exp(-nn)));
me.group.setGeoPosition(center_tile_lat, center_tile_lon);
if( tile_index[0] != me.last_tile[0]
or tile_index[1] != me.last_tile[1]
or me.tile_type != me.last_tile_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.zoom,
x: int(tile_index[0] + x),
y: int(tile_index[1] + y),
tms_y: ymax - int(tile_index[1] + y) - 1,
type: me.tile_type
};
(func {
var img_path = me.makePath(pos);
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);
#print('requesting ' ~ img_url);
http.save(img_url, img_path)
.done(func { tile.set("src", img_path);})
.fail(func (r) print('Failed to get image ' ~ img_path ~ ' ' ~ r.status ~ ': ' ~ r.reason));
} else {
# Re-use cached image
#print('loading ' ~ img_path);
tile.set("src", img_path)
}
})();
}
}
me.last_tile = tile_index;
me.last_type = me.type;
}
},
update: func() {
if (!me.getVisible())
return;
#debug.warn("update traceback for "~me.type);
if (me.options != nil and me.options['update_wrapper'] !=nil) {
me.options.update_wrapper( me, me.updateLayer ); # call external wrapper (usually for profiling purposes)
} else {
me.updateLayer();
}
},
del: func() {
printlog(_MP_dbg_lvl, "SymbolLayer.del()");
call(OverlayLayer.del, nil, me);
},
}; # of TileLayer
# Class to manage controlling a OverlayLayer.
# Currently handles:
# * Simple update() call
OverlayLayer.Controller = {
# Static/singleton:
registry: {},
add: func(type, class)
me.registry[type] = class,
get: func(type)
if ((var class = me.registry[type]) == nil)
__die("unknown type '"~type~"'");
else return class,
# Calls corresponding controller constructor
# @param layer The #OverlayLayer this controller is responsible for.
new: func(type, layer, arg...)
return call((var class = me.get(type)).new, [layer]~arg, class),
# Default implementations for derived classes:
# @return List of positioned objects.
updateLayer: func()
__die("Abstract method updateLayer() not implemented for this OverlayLayer.Controller type!"),
addVisibilityListener: func() {
var m = me;
append(m.listeners, setlistener(
m.layer._node.getNode("visible"),
func m.layer.update(),
#compile("m.layer.update()", "<layer visibility on node "~m.layer._node.getNode("visible").getPath()~" for layer "~m.layer.type~">"),
0,0
));
},
addRangeListener: func() {
var m = me;
append(m.listeners, setlistener(
m.layer._node.getNode("range",1),
func m.layer.update(),
#compile("m.layer.update()", "<layer visibility on node "~m.layer._node.getNode("visible").getPath()~" for layer "~m.layer.type~">"),
0,0
));
},
addScreenRangeListener: func() {
var m = me;
append(m.listeners, setlistener(
m.layer._node.getNode("screen-range",1),
func m.layer.update(),
#compile("m.layer.update()", "<layer visibility on node "~m.layer._node.getNode("visible").getPath()~" for layer "~m.layer.type~">"),
0,0
));
},
}; # of OverlayLayer.Controller
###
# set up a cache for 32x32 symbols (initialized below in load_MapStructure)
var SymbolCache32x32 = nil;
@ -1307,6 +1613,7 @@ var load_MapStructure = func {
"symbol",
"scontroller",
"controller",
"overlay"
];
var deps = {};
foreach (var d; dep_names) deps[d] = [];

View file

@ -476,11 +476,13 @@ var Map = {
setController: func(controller=nil, arg...)
{
if (me.controller != nil) me.controller.del(me);
if (controller == nil)
if (controller == nil) {
controller = Map.df_controller;
elsif (typeof(controller) != 'hash')
}
elsif (typeof(controller) != 'hash') {
controller = Map.Controller.get(controller);
}
if (controller == nil) {
me.controller = nil;
} else {
@ -527,8 +529,8 @@ var Map = {
},
getLayer: func(type_arg) me.layers[type_arg],
setRange: func(range) me.set("range",range),
getRange: func me.get('range'),
setRange: func(range) { me.set("range",range); },
setScreenRange: func(range) { me.set("screen-range",range); },
setPos: func(lat, lon, hdg=nil, range=nil, alt=nil)
{
@ -555,6 +557,7 @@ var Map = {
getHdg: func me.get("hdg"),
getAlt: func me.get("altitude"),
getRange: func me.get("range"),
getScreenRange: func me.get('screen-range'),
getLatLon: func [me.get("ref-lat"), me.get("ref-lon")],
# N.B.: This always returns the same geo.Coord object,
# so its values can and will change at any time (call
@ -626,7 +629,7 @@ var Text = {
{
die("updateText() requires enableUpdate() to be called first");
},
# enable fast setprop-based text writing
enableFast: func ()
{
@ -981,7 +984,7 @@ var Path = {
},
setColor: func me.setStroke(_getColor(arg)),
getColor: func me.getStroke(),
getColor: func me.getStroke(),
setColorFill: func me.setFill(_getColor(arg)),
getColorFill: func me.getColorFill(),

View file

@ -243,7 +243,7 @@ LayeredMap.updateZoom = func {
z = math.max(0, math.min(z, size(me.ranges) - 1));
me.zoom_property.setIntValue(z);
var zoom = me.ranges[size(me.ranges) - 1 - z];
# print("Setting zoom range to:", zoom);
print("Setting zoom range to: " ~ z ~ " " ~ zoom);
benchmark("Zooming map:"~zoom, func {
me._node.getNode("range", 1).setDoubleValue(zoom);
# TODO update center/limit translation to keep airport always visible
@ -274,7 +274,7 @@ LayeredMap.setupZoom = func(dialog) {
foreach(var r; ranges)
append(me.ranges, r.getValue() );
# print("Setting up Zoom Ranges:", size(ranges)-1);
print("Setting up Zoom Ranges:", size(ranges)-1);
me.listen(me.zoom_property, func me.updateZoom() );
me.updateZoom();
me; #chainable
@ -441,4 +441,3 @@ setlistener("/nasal/canvas/loaded", func {
# TODO: should be inside a separate subfolder, i.e. canvas/map/mfd
load_modules( files_with('.mfd'), 'canvas' );
});

View file

@ -0,0 +1,37 @@
# See: http://wiki.flightgear.org/MapStructure
# Class things:
var name = 'OSM';
var parents = [OverlayLayer.Controller];
var __self__ = caller(0)[0];
OverlayLayer.Controller.add(name, __self__);
TileLayer.add(name, {
parents: [TileLayer],
type: name, # Layer type
df_controller: __self__, # controller to use by default -- this one
});
var new = func(layer) {
var m = {
parents: [__self__],
layer: layer,
map: layer.map,
listeners: [],
};
layer.makeURL = string.compileTemplate('https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png');
layer.makePath = string.compileTemplate(layer.maps_base ~ '/osm-intl/{z}/{x}/{y}.png');
m.addVisibilityListener();
m.addRangeListener();
m.addScreenRangeListener();
return m;
};
var updateLayer = func() {
}
var del = func() {
#print(name~".lcontroller.del()");
foreach (var l; me.listeners)
removelistener(l);
};

View file

@ -0,0 +1,38 @@
# See: http://wiki.flightgear.org/MapStructure
# Class things:
var name = 'OpenAIP';
var parents = [OverlayLayer.Controller];
var __self__ = caller(0)[0];
OverlayLayer.Controller.add(name, __self__);
TileLayer.add(name, {
parents: [TileLayer],
type: name, # Layer type
df_controller: __self__, # controller to use by default -- this one
});
var new = func(layer) {
var m = {
parents: [__self__],
layer: layer,
map: layer.map,
listeners: [],
};
# http://1.tile.maps.openaip.net/geowebcache/service/tms/1.0.0/openaip_basemap@EPSG%3A900913@png/6/30/43.png
layer.makeURL = string.compileTemplate('http://1.tile.maps.openaip.net/geowebcache/service/tms/1.0.0/openaip_basemap@EPSG%3A900913@png/{z}/{x}/{tms_y}.png');
layer.makePath = string.compileTemplate(layer.maps_base ~ '/openaip_basemap/{z}/{x}/{tms_y}.png');
m.addVisibilityListener();
m.addRangeListener();
m.addScreenRangeListener();
return m;
};
var updateLayer = func() {
}
var del = func() {
#print(name~".lcontroller.del()");
foreach (var l; me.listeners)
removelistener(l);
};

View file

@ -164,12 +164,12 @@ var Coord = {
me._pupdate();
course *= D2R;
dist /= ERAD;
if (dist < 0.0) {
dist = abs(dist);
course = course - math.pi;
course = course - math.pi;
}
me._lat = math.asin(math.sin(me._lat) * math.cos(dist)
+ math.cos(me._lat) * math.sin(dist) * math.cos(course));
@ -398,6 +398,6 @@ var PositionedSearch = {
debug.benchmark('Toggle '~from~'nm/'~to~'nm', func {
s.update();
s.update( func positioned.findWithinRange(to, 'fix') );
}); # ~ takes
}); # ~ takes
}, # of test
};

View file

@ -25,6 +25,9 @@
}
}
setTransparency(0);
]]></open>
<close><![CDATA[
@ -213,6 +216,35 @@
</binding>
</checkbox>
<checkbox>
<label>OSM</label>
<halign>left</halign>
<property>/sim/gui/dialogs/map-canvas/draw-OSM</property>
<live>true</live>
<binding>
<command>dialog-apply</command>
</binding>
<binding>
<command>property-toggle</command>
</binding>
</checkbox>
<checkbox>
<label>OpenAIP</label>
<halign>left</halign>
<property>/sim/gui/dialogs/map-canvas/draw-OpenAIP</property>
<live>true</live>
<binding>
<command>dialog-apply</command>
</binding>
<binding>
<command>property-toggle</command>
</binding>
</checkbox>
<!-- layer only supported if tutorial system is active and targets specified-->
<!--
<checkbox>
@ -348,7 +380,9 @@
TestMap.setController("Aircraft position", "map-dialog"); # from aircraftpos.controller
# Initialize a range:
TestMap.setRange(20);
TestMap.setRange(60);
TestMap.setScreenRange(200);
var range_step = math.log10(TestMap.getRange());
# TODO: check if this is valid, IIRC DOM manipulation was fragile when done inside canvas/Nasal region (?)
gui.findElementByName(self, "zoomdisplay").setValue("property", TestMap._node.getNode("range").getPath());
@ -423,16 +457,25 @@
TestMap.getLayer(name).setVisible(n);
};
var SetProjection = func(projection) {
TestMap._node.setValue("projection", projection);
};
setlistener("/sim/gui/dialogs/map-canvas/projection",
func(n) SetProjection(n.getValue());
);
Styles.APS = {};
Styles.APS.scale_factor = 0.25;
# TODO: introduce some meta NAV layer that handles both VORs and NDBs, can we instantiate those layers directly ?
var r = func(name,vis=1,zindex=nil) return caller(0)[0];
# TODO: we'll need some z-indexing here, right now it's just random
foreach(var type; [r('TFC',0),r('APT'),r('DME'),r('VOR'),r('NDB'),r('FIX',0),r('RTE'),r('WPT'),r('FLT'),r('WXR',0),r('APS'), ] ) {
foreach(var type; [r('TFC',0),r('APT'),r('DME'),r('VOR'),r('NDB'),r('FIX',0),r('RTE'),r('WPT'),r('FLT'),r('WXR',0),r('APS')] ) {
if (1 and type.name != 'APS' and type.name != 'FLT') make_update_wrapper(type.name);
TestMap.addLayer(factory: canvas.SymbolLayer, type_arg: type.name,
visible: type.vis, priority: type.zindex,
visible: type.vis, priority: 4,
style: Styles.get(type.name),
options: Options.get(type.name) );
(func {
@ -445,6 +488,24 @@
);
})();
}
foreach(var type; [ r('OSM'), r('OpenAIP') ]) {
TestMap.addLayer(factory: canvas.OverlayLayer, type_arg: type.name,
visible: type.vis, priority: 1,
style: Styles.get(type.name),
options: Options.get(type.name) );
(func {
# Notify MapStructure about layer visibility changes:
var name = type.name;
props.globals.initNode("/sim/gui/dialogs/map-canvas/draw-"~name, type.vis, "BOOL");
append(listeners,
setlistener("/sim/gui/dialogs/map-canvas/draw-"~name,
func(n) SetLayerVisible(name,n.getValue()))
);
})();
}
]]></load></nasal>
</canvas>
<layout>hbox</layout>
@ -498,7 +559,7 @@
</binding>
</button>
</group>
</group>
</group>
</PropertyList>