Files
paparazzi/sw/logalizer/logplotter.ml
T
Freek van Tienen 1d39c29bb4 [ground_segment] Fix log plotter KML output
The KML output was parsing the GPS messages as radians, but it was degrees instead. This means the values were converted to degrees once more afterwards.
2016-08-15 15:45:45 +02:00

923 lines
31 KiB
OCaml

(*
* Basic log plotter
*
* Copyright (C) 2007- ENAC, Pascal Brisset, Antoine Drouin
*
* This file is part of paparazzi.
*
* paparazzi is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* paparazzi is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with paparazzi; see the file COPYING. If not, write to
* the Free Software Foundation, 59 Temple Place - Suite 330,
* Boston, MA 02111-1307, USA.
*
*)
open Latlong
open Printf
let (//) = Filename.concat
let logs_dir = Env.paparazzi_home // "var" // "logs"
let sample_kml = Env.paparazzi_home // "data/maps/sample_path.kml"
let verbose = ref false
class type text_value = object method text : string end
let double__ =
let underscore = Str.regexp "_" in
fun s -> Str.global_replace underscore "__" s
let remove_same_t = fun l ->
let rec loop prev = function
(t1,y1)::(((t2,y2)::l) as l')->
if t1 = t2 then
let y = if (y2-y1)*(y1-prev) > 0 then y2 else y1 in
loop y ((t1,y)::l)
else
(t1,y1)::loop y1 l'
| l -> l in
loop 0 l
let compute_ticks = fun min_y max_y ->
let delta = max_y -. min_y in
let scale = log delta /. log 10. in
let d = 10. ** floor scale in
let u =
if delta < 2.*.d then d/.5.
else if delta < 5.*.d then d/.2.
else d in
let tick_min = min_y -. mod_float min_y u in
(delta, scale, u, tick_min)
let colors = [|(255,0,0);(0,255,0); (0,0,255); (245,215,20); (245,20,245); (30,225,225); (110,30,230)|]
type curve = { values: (float*float) array; color : int*int*int }
let labelled_entry = fun ?width_chars text value (h:GPack.box) ->
let label = GMisc.label ~text ~packing:h#pack () in
label, GEdit.entry ?width_chars ~text:value ~packing:h#pack ()
let logs_menus = ref []
let screenshot_hint_name =
let n = ref 0 in
fun extension ->
let basename =
match !logs_menus with
(_, menu_name, _, _)::_ -> begin
match Str.split (Str.regexp ":") menu_name with
menu_prefix::_ -> sprintf "%s" menu_prefix
| _ -> sprintf "%s" menu_name
end
| _ -> incr n; sprintf "pprz_log-%d" !n in
sprintf "%s.%s" basename extension
let save_dialog = fun extension callback ->
let title = "Save snapshot" in
let dialog = GWindow.file_chooser_dialog ~action:`SAVE ~title () in
ignore (dialog#set_current_folder logs_dir);
dialog#add_filter (GFile.filter ~name:extension ~patterns:["*."^extension] ());
dialog#add_button_stock `CANCEL `CANCEL ;
dialog#add_select_button_stock `SAVE `SAVE ;
let name = screenshot_hint_name extension in
let _ = dialog#set_current_name name in
begin match dialog#run (), dialog#filename with
`SAVE, Some name ->
dialog#destroy ();
callback name
| _ -> dialog#destroy ()
end
let fig_renderer = fun (width, height) ->
let scale = 12 in (* 1200ppi (for xfig) / 100ppi (for screen) *)
let width = width * scale
and height = height * scale in
object (self)
val mutable pen_color = Fig.black
val mutable text = ""
val mutable objects = []
method unit = fun x -> scale * x
method size = (width, height)
method init = fun () ->
self#rectangle 0 0 width height ()
method set_color = fun (r, g, b) ->
let (id, obj) = Fig.color r g b in
pen_color <- id;
objects <- obj :: objects
method lines = fun points ->
objects <- Fig.polyline ~pen_color points :: objects
method rectangle = fun x y width height ?(filled=false) () ->
let area_fill = if filled then Fig.filled else -1
and p1 = (x,y)
and p2 = (x+width,y+height) in
let points = [p1; p2] in
objects <- Fig.polyline ~pen_color ~fill_color:pen_color ~sub_type:Fig.Box ~area_fill points :: objects
val font_size = 10
method create_text = fun s ->
text <- s;
((3*font_size*scale)/4*String.length s, font_size*scale)
method put_text = fun x y ->
let font = Fig.Postscript Fig.Helvetica in
let obj = Fig.text ~font ~font_size ~color:pen_color (x,y+font_size*scale) text in
objects <- obj :: objects
method draw = fun () ->
let fig = Fig.create objects in
save_dialog "fig" (fun name -> Fig.write name fig)
end
class plot = fun ~width ~height ~packing () ->
let curves = Hashtbl.create 3
and left_margin = 50
and bottom_margin = 25
and top_margin = 20 in
object (self)
val mutable min_x = max_float
val mutable max_x = -.max_float
val mutable min_y = max_float
val mutable max_y = -.max_float
val mutable scale_events = []
val mutable color_index = 0
val mutable csts = ([] : float list)
val mutable auto_scale = true
val mutable press_x = 0.
val mutable press_y = 0.
val mutable motion_x = 0.
val mutable motion_y = 0.
val mutable pressed_button = None
inherit Gtk_tools.pixmap_in_drawin_area ~width ~height ~packing () as pida
method unscale_x = fun width x ->
min_x +. (x -. float left_margin) *. (max_x -. min_x) /. float (width-left_margin)
method unscale_y = fun height y ->
let graph_height = height - bottom_margin - top_margin in
min_y -. (y -. float (top_margin+graph_height)) *. (max_y -. min_y) /. float graph_height
method private update_scale = fun ?(update_x=true) values ->
let n = Array.length values in
if update_x then begin
min_x <- min min_x (fst values.(0));
max_x <- max max_x (fst values.(n-1))
end;
for i = 0 to n - 1 do
let x, y = values.(i) in
if min_x <= x && x <= max_x then begin
min_y <- min min_y y;
max_y <- max max_y y
end
done
method reset_scale = fun ?(update_x = true) () ->
if auto_scale || not update_x then begin
(* Recomputes the min and max *)
if update_x then begin
min_x <- max_float;
max_x <- -.max_float
end;
min_y <- max_float;
max_y <- -.max_float;
Hashtbl.iter
(fun _ curve -> self#update_scale ~update_x curve.values)
curves;
self#wake ()
end
method auto_scale = auto_scale
method set_auto_scale = fun x ->
auto_scale <- x;
self#reset_scale ();
self#recompute ()
method min_x () = min_x
method min_y () = min_y
method set_min_x = fun x -> if not self#auto_scale then begin min_x <- x; self#recompute () end
method set_min_y = fun x -> if not self#auto_scale then begin min_y <- x; self#recompute () end
method max_x () = max_x
method max_y () = max_y
method set_max_x = fun x -> if not self#auto_scale then begin max_x <- x; self#recompute () end
method set_max_y = fun x -> if not self#auto_scale then begin max_y <- x; self#recompute () end
method scale_event = fun cb -> scale_events <- cb :: scale_events
method wake = fun () -> List.iter (fun cb -> cb ()) scale_events;
method destroy = fun () -> ()
method add_cst = fun v ->
csts <- v :: csts;
self#recompute ()
method delete_cst = fun v ->
csts <- List.filter (fun x -> x <> v) csts;
self#recompute ()
method add_curve = fun (name:string) (values:(float*float) array) ->
let curve = { values = values; color = colors.(color_index) } in
Hashtbl.add curves name curve;
color_index <- (color_index + 1) mod Array.length colors;
if auto_scale then begin
self#update_scale values;
self#wake ()
end;
self#wake ();
self#recompute ();
curve
method delete_curve = fun (name:string) ->
Hashtbl.remove curves name;
self#reset_scale ();
self#recompute ()
method da_renderer = fun () ->
let da = self#drawing_area in
let context = da#misc#create_pango_context in
let () = context#set_font_by_name "sans 8 " in
let layout = context#create_layout in
let dr = self#get_pixmap () in
object (self)
method size =
let s = da#misc#allocation in
(s.Gtk.width, s.Gtk.height)
method init = fun () ->
dr#set_foreground (`NAME "white");
let (width, height) = self# size in
dr#rectangle ~x:0 ~y:0 ~width ~height ~filled:true ()
method set_color = fun (r, g, b) ->
dr#set_foreground (`RGB (256*r, 256*g, 256*b))
method lines = dr#lines
method rectangle = fun x y width height ?(filled=false) () ->
dr#rectangle ~x ~y ~width ~height ~filled ()
method create_text = fun s ->
Pango.Layout.set_text layout s;
Pango.Layout.get_pixel_size layout
method put_text = fun x y ->
dr#put_layout ~x ~y layout
method draw = fun () ->
pida#redraw ()
method unit = fun x -> x
end
method export_fig = fun () ->
let renderer = fig_renderer (self#da_renderer ())#size in
self#recompute ~renderer ()
method recompute = fun ?(renderer=self#da_renderer ()) ?(low_res=false) () ->
let (width, height) = renderer#size in
renderer#init ();
let tick_len = renderer#unit 5
and margin = renderer#unit 3
and left_margin = renderer#unit left_margin
and bottom_margin = renderer#unit bottom_margin
and top_margin = renderer#unit top_margin in
let black = (0,0,0) in
let graph_height = height - bottom_margin - top_margin in
let scale_x = fun x -> left_margin + truncate ((x-.min_x)*. float (width-left_margin) /. (max_x -. min_x))
and scale_y = fun y -> top_margin+graph_height - truncate ((y-.min_y)*. float graph_height /. (max_y -. min_y)) in
(* Constants *)
List.iter
(fun v ->
renderer#set_color black;
renderer#lines [(left_margin, scale_y v); (width, scale_y v)])
csts;
(* Curves *)
let title_y = ref margin in
Hashtbl.iter (fun title curve ->
let step = if low_res then 10 else 1 in
let points = ref [] in
Array.iteri (fun i (t, v) -> if t > min_x && t <= max_x && (i mod step = 0) then points := List.rev_append [(scale_x t, scale_y v)] !points) curve.values;
renderer#set_color curve.color;
if List.length !points > 0 then renderer#lines !points;
(* Title *)
let (w, h) = renderer#create_text title in
renderer#rectangle (width-h-margin) !title_y h h ~filled:true ();
renderer#set_color black;
renderer#put_text (width-2*margin-w-h) (!title_y);
title_y := !title_y + h + margin)
curves;
(* Graduations *)
if Hashtbl.length curves > 0 then begin
renderer#set_color black;
(* Y *)
let (min_y, max_y) =
if max_y > min_y then (min_y, max_y)
else let d = abs_float max_y /. 10. in (max_y -. d, max_y +. d) in
let delta, scale, u, tick_min = compute_ticks min_y max_y in
for i = 0 to truncate (delta/.u) + 1 do
let tick = tick_min +. float i *. u in
let y = scale_y tick in
if y < height - bottom_margin then
let s = Printf.sprintf "%.*f" (Pervasives.max 0 (2-truncate scale)) tick in
let (w, h) = renderer#create_text s in
renderer#put_text (left_margin-margin-w) (y-h/2);
renderer#lines [(left_margin,y);(left_margin+tick_len,y)]
done;
(* Time *)
let delta, scale, u, tick_min = compute_ticks min_x max_x in
let y = height in
for i = 0 to truncate (delta/.u) + 1 do
let tick = tick_min +. float i *. u in
let x = scale_x tick in
if left_margin < x && x < width then
let s = Printf.sprintf "%.*f" (Pervasives.max 0 (2-truncate scale)) tick in
let (w, h) = renderer#create_text s in
let y = y-margin-h in
renderer#put_text (x-w/2) y;
renderer#lines [(x,y);(x,y-tick_len)]
done
end;
(* Draw a rectangle of the current selection *)
begin
match pressed_button with
Some 1 ->
let width = abs (truncate (motion_x -. press_x))
and height = abs (truncate (motion_y -. press_y)) in
if width > 5 && height > 5 then
let x = truncate (min press_x motion_x)
and y = truncate (min press_y motion_y) in
renderer#set_color black;
renderer#rectangle x y width height ();
| _ -> ()
end;
(* Actually draw *)
renderer#draw ()
method scroll = fun dpx dpy ->
let scale_x = (max_x -. min_x) /. float (width-left_margin)
and scale_y = (max_y -. min_y) /. float (height - bottom_margin - top_margin) in
let dx = scale_x *. float dpx
and dy = scale_y *. float dpy in
min_x <- min_x +. dx;
max_x <- max_x +. dx;
min_y <- min_y +. dy;
max_y <- max_y +. dy;
self#recompute ~low_res:true ()
method button_press = fun ev ->
pressed_button <- Some (GdkEvent.Button.button ev);
match GdkEvent.Button.button ev with
1 | 2 -> (* left button, store position *)
press_x <- GdkEvent.Button.x ev;
press_y <- GdkEvent.Button.y ev;
true
| 3 when not auto_scale -> (* right button, reset scale *)
self#reset_scale ~update_x:false ();
self#recompute ();
true
| _ -> false
method motion_notify = fun ev ->
let x = GdkEvent.Motion.x ev
and y = GdkEvent.Motion.y ev in
match pressed_button with
Some 1 ->
motion_x <- x;
motion_y <- y;
self#recompute ~low_res:true ();
true
| Some 2 -> (* middle button, scroll *)
self#scroll (truncate (press_x-.x)) (truncate (y-.press_y));
press_x <- x;
press_y <- y;
true
| _ -> false
method button_release = fun ev ->
pressed_button <- None;
match GdkEvent.Button.button ev with
1 -> (* left button, change scale *)
let release_x = GdkEvent.Button.x ev
and release_y = GdkEvent.Button.y ev in
if abs_float (release_x -. press_x) > 25.
&& abs_float (release_y -. press_y) > 25. then begin
let {Gtk.width=width;height=height}= self#drawing_area#misc#allocation in
let new_max_x = self#unscale_x width (max press_x release_x) in
min_x <- self#unscale_x width (min press_x release_x);
max_x <- new_max_x;
let new_max_y = self#unscale_y height (min press_y release_y)in
min_y <- self#unscale_y height (max press_y release_y);
max_y <- new_max_y;
auto_scale <- false;
self#recompute ()
end;
true
| 2 -> self#recompute (); (* recompute at full resolution after a scroll *)
true
| _ -> false
method zoom = fun ev ->
let {Gtk.width=width;height=height}= self#drawing_area#misc#allocation
and dx = (max_x -. min_x) and dy = (max_y -. min_y) in
let alpha_x =
(GdkEvent.Scroll.x ev -. float left_margin)
/. float (width-left_margin)
and alpha_y =
(float height-.GdkEvent.Scroll.y ev-. float (top_margin+bottom_margin))
/. float (height-top_margin-bottom_margin)
and ev_state = GdkEvent.Scroll.state ev in
let ctrl_mod = Gdk.Convert.test_modifier `CONTROL ev_state
and shift_mod = Gdk.Convert.test_modifier `SHIFT ev_state in
match GdkEvent.Scroll.direction ev with
`UP -> (* Zoom factor 2 *)
if not shift_mod then begin
min_x <- min_x +. dx *. alpha_x /. 2.;
max_x <- min_x +. dx /. 2.
end;
if not ctrl_mod then begin
min_y <- min_y +. dy *. alpha_y /. 2.;
max_y <- min_y +. dy /. 2.
end;
auto_scale <- false;
self#recompute ();
true
| `DOWN -> (* Unzoom factor 2 *)
if not shift_mod then begin
min_x <- min_x -. dx *. alpha_x;
max_x <- min_x +. dx *. 2.
end;
if not ctrl_mod then begin
min_y <- min_y -. dy *. alpha_y;
max_y <- min_y +. dy *. 2.
end;
auto_scale <- false;
self#recompute ();
true
| _ -> false
initializer ignore (self#drawing_area#event#connect#expose ~callback:(fun _ -> self#recompute (); false))
initializer ignore (self#drawing_area#event#add [`BUTTON_PRESS; `BUTTON_MOTION; `BUTTON_RELEASE; `SCROLL])
initializer ignore (self#drawing_area#event#connect#button_press ~callback:self#button_press);
initializer ignore (self#drawing_area#event#connect#motion_notify ~callback:self#motion_notify);
initializer ignore (self#drawing_area#event#connect#button_release ~callback:self#button_release);
initializer ignore (self#drawing_area#event#connect#scroll ~callback:self#zoom);
end
let pprz_float = function
PprzLink.Int i -> float i
| PprzLink.Float f -> f
| PprzLink.Int32 i -> Int32.to_float i
| PprzLink.Int64 i -> Int64.to_float i
| PprzLink.String s -> let v = try float_of_string s with _ -> 0. in v
| PprzLink.Char c -> let v = try float_of_string (String.make 1 c) with _ -> 0. in v
| PprzLink.Array _ -> 0.
let rec select_gps_values = function
[] -> []
| (m, values)::_ when m.PprzLink.name = "GPS" ->
let xs = List.assoc "utm_east" values
and ys = List.assoc "utm_north" values
and zs = List.assoc "utm_zone" values
and alts = List.assoc "alt" values in
let l = ref [] in
for i = 0 to Array.length xs - 1 do
let z = truncate (snd zs.(i))
and a = snd alts.(i) /. 1000. in
if z <> 0 && a > 0. then
let t = fst xs.(i)
and x = snd xs.(i) /. 100.
and y = snd ys.(i) /. 100. in
let utm = { utm_x = x; utm_y = y; utm_zone = z } in
l := try (t, of_utm WGS84 utm, a) :: !l with _ -> !l
done;
List.rev !l
| (m, values)::_ when m.PprzLink.name = "GPS_INT" ->
let lats = List.assoc "lat" values
and lons = List.assoc "lon" values
and alts = List.assoc "hmsl" values in
let l = ref [] in
for i = 0 to Array.length lats - 1 do
let a = snd alts.(i) /. 1000. in
if a > 0. then
let t = fst lats.(i)
and lat = snd lats.(i) /. 1e7
and lon = snd lons.(i) /. 1e7 in
let wgs84 = make_geo_deg lat lon in
l := (t, wgs84, a) :: !l
done;
List.rev !l
| _ :: rest ->
select_gps_values rest
let write_kml = fun plot log_name values ->
let t_min = plot#min_x ()
and t_max = plot#max_x () in
let t_min = if t_min = max_float then -. max_float else t_min in
let t_max = if t_max = -. max_float then max_float else t_max in
let l = List.filter (fun (t,_,_) -> t_min <= t && t < t_max) values in
let xml = ExtXml.parse_file sample_kml in
let doc = ExtXml.child xml "Document" in
let place = ExtXml.child doc "Placemark" in
let line = ExtXml.child place "LineString" in
let coords =
String.concat " " (List.map (fun (_,p, a) -> sprintf "%.6f,%.6f,%f" ((Rad>>Deg)p.posn_long) ((Rad>>Deg)p.posn_lat) a) l) in
let coordinates = Xml.Element ("coordinates", [], [Xml.PCData coords]) in
let line = ExtXml.subst_child "coordinates" coordinates line in
let place = ExtXml.subst_child "LineString" line place in
let name = Xml.Element ("name", [], [Xml.PCData log_name]) in
let place = ExtXml.subst_child "name" name place in
let doc = ExtXml.subst_child "Placemark" place doc in
let doc = ExtXml.subst_child "name" (Xml.Element ("name", [], [Xml.PCData log_name])) doc in
let xml = ExtXml.subst_child "Document" doc xml in
let title = "Save KML" in
let dialog = GWindow.file_chooser_dialog ~action:`SAVE ~title () in
ignore (dialog#set_current_folder logs_dir);
dialog#add_filter (GFile.filter ~name:"kml" ~patterns:["*.kml"] ());
dialog#add_button_stock `CANCEL `CANCEL ;
dialog#add_select_button_stock `SAVE `SAVE ;
let _ = dialog#set_current_name (log_name^".kml") in
begin match dialog#run (), dialog#filename with
`SAVE, Some name ->
dialog#destroy ();
let f = open_out name in
fprintf f "%s\n" (Xml.to_string_fmt xml);
close_out f
| _ -> dialog#destroy ()
end
let bracket_regexp = Str.regexp "\\["
let add_ac_submenu = fun ?(export=false) protocol ?(factor=object method text="1" end) plot menubar (curves_menu_fact: GMenu.menu GMenu.factory) ac menu_name l raw_msgs ->
let menu = GMenu.menu () in
let menuitem = GMenu.menu_item ~label:menu_name () in
menuitem#set_submenu menu;
menubar#menu#append menuitem;
let menu_fact = new GMenu.factory menu in
(* Build the msg menus *)
List.iter
(fun (msg, l) ->
let msg_name = msg.PprzLink.name in
let menu = menu_fact#add_submenu (double__ msg_name) in
let menu_fact = new GMenu.factory menu in
(* Build the field menus *)
List.iter (* forall fields *)
(fun (f, values) ->
let callback = fun _ ->
(* Remove the . for an array field name *)
let f' = List.hd (Str.split bracket_regexp f) in
let alt_unit_coef = (List.assoc f' msg.PprzLink.fields).PprzLink.alt_unit_coef in
let name = sprintf "%s:%s:%s:%s" menu_name msg_name f factor#text
and (a, b) = Ocaml_tools.affine_transform factor#text
and (a', b') = Ocaml_tools.affine_transform alt_unit_coef in
let a = a *. a' and b = a*.b' +. b in
let values = Array.map (fun (t,v) -> (t, v*.a+.b)) values in
let curve = plot#add_curve name values in
let eb = GBin.event_box ~width:10 ~height:10 () in
let (r, g, b) = curve.color in
eb#coerce#misc#modify_bg [`NORMAL, `RGB (256*r,256*g,256*b)];
let item = curves_menu_fact#add_image_item ~image:eb#coerce ~label:name () in
let delete = fun () ->
plot#delete_curve name;
curves_menu_fact#menu#remove (item :> GMenu.menu_item) in
ignore (item#connect#activate ~callback:delete)
in
ignore (menu_fact#add_item ~callback (double__ f)))
l
)
l;
ignore (menu_fact#add_separator ());
let callback = fun () ->
write_kml plot menu_name (select_gps_values l) in
ignore (menu_fact#add_item ~callback "Export KML path");
let callback = fun ?no_gui () ->
Export.popup ?no_gui protocol menu_name raw_msgs in
ignore (menu_fact#add_item ~callback "Export CSV");
if export then
callback ~no_gui:true ()
let load_log = fun ?export ?factor (plot:plot) (menubar:GMenu.menu_shell GMenu.factory) curves_fact xml_file ->
Debug.call 'p' (fun f -> fprintf f "load_log: %s\n" xml_file);
let xml = ExtXml.parse_file xml_file in
let data_file = ExtXml.attrib xml "data_file" in
Debug.call 'p' (fun f -> fprintf f "data_file: %s\n" data_file);
let protocol = ExtXml.child xml "protocol" in
(* In the old days, telemetry class was named telemetry_ap ... *)
let class_name =
try
let name = "telemetry_ap" in
let _ = begin try ExtXml.child protocol ~select:(fun x -> Xml.attrib x "name" = name) "msg_class"
with Not_found -> ExtXml.child protocol ~select:(fun x -> Xml.attrib x "name" = name) "class"
end in
name
with _ -> "telemetry" in
Debug.call 'p' (fun f -> fprintf f "class_name: %s\n" class_name);
let module M = struct let name = class_name let xml = protocol end in
let module P = PprzLink.MessagesOfXml(M) in
let f =
try
Ocaml_tools.find_file [Filename.dirname xml_file] data_file
with
Not_found ->
fprintf stderr "File '%s' not found\n%!" data_file;
let data_file = Filename.chop_extension (Filename.basename xml_file) ^ ".data" in
fprintf stderr "Trying with '%s'\n%!" data_file;
try
Ocaml_tools.find_file [Filename.dirname xml_file] data_file
with Not_found ->
fprintf stderr "File '%s' not found\n%!" data_file;
failwith "Data file not found" in
let f = Ocaml_tools.open_compress f in
let acs = Hashtbl.create 3 in (* indexed by A/C *)
try
while true do
let l = input_line f in
try
Scanf.sscanf l "%f %s %[^\n]"
(fun t ac m ->
if not (Hashtbl.mem acs ac) then
Hashtbl.add acs ac (Hashtbl.create 97, ref []);
let msgs, raw_msgs = Hashtbl.find acs ac in
(*Elements of [acs] are assoc lists of [fields] indexed by msg id*)
let msg_id, vs = P.values_of_string m in
if not (Hashtbl.mem msgs msg_id) then
Hashtbl.add msgs msg_id (Hashtbl.create 97);
let fields = Hashtbl.find msgs msg_id in
(* Elements of [fields] are values indexed by field name *)
List.iter
(fun (f, value) ->
match value with
PprzLink.Array array ->
Array.iteri
(fun i scalar ->
let f = sprintf "%s[%d]" f i in
Hashtbl.add fields f (t, scalar))
array
| scalar ->
Hashtbl.add fields f (t, scalar))
vs;
let msg_name = (P.message_of_id msg_id).PprzLink.name in
raw_msgs := (t, msg_name, vs) :: !raw_msgs
)
with
exc ->
if !verbose then
prerr_endline (Printexc.to_string exc)
done
with
End_of_file ->
close_in f;
(* Compile the data to ease the menu building *)
Hashtbl.iter (* For all A/Cs *)
(fun ac (msgs, raw_msgs) ->
let raw_msgs = List.rev !raw_msgs in
let menu_name = sprintf "%s:%s" (Filename.chop_extension (Filename.basename xml_file)) ac in
(* First sort by message id *)
let l = ref [] in
Hashtbl.iter (fun msg fields -> l := (P.message_of_id msg, fields):: !l) msgs;
let msgs = List.sort (fun (a,_) (b,_) -> compare a b) !l in
let msgs =
List.map (fun (msg, fields) ->
let l = ref [] in
Hashtbl.iter
(fun f v -> if not (List.mem f !l) then l := f :: !l)
fields;
let sorted_fields = List.sort compare !l in
let field_values_assoc =
List.map
(fun f ->
let values = Hashtbl.find_all fields f in
let values = List.map (fun (t, v) -> (t, pprz_float v)) values in
let values = Array.of_list values in
Array.sort compare values;
(f, values))
sorted_fields in
(msg, field_values_assoc))
msgs in
(* Store data for other windows *)
logs_menus := !logs_menus @ [(ac, menu_name, (msgs, raw_msgs), protocol)];
add_ac_submenu ?export protocol ?factor plot menubar curves_fact ac menu_name msgs raw_msgs;
)
acs
let open_log = fun ?factor plot menubar curves_fact () ->
ignore (Log_file.chooser ~callback:(fun name -> load_log ?factor plot menubar curves_fact name) ())
let screenshot = fun frame ->
let width, height = Gdk.Drawable.get_size frame#misc#window in
let dest = GdkPixbuf.create width height () in
GdkPixbuf.get_from_drawable ~dest ~width ~height frame#misc#window;
save_dialog
"png"
(fun name -> GdkPixbuf.save name "png" dest)
(** Table of current windows, to be able to quit when the last one is closed
FIXME: should be shared with plotter.ml *)
let windows = Hashtbl.create 3
(*****************************************************************************)
let rec plot_window = fun ?export init ->
let plotter = GWindow.window ~allow_shrink:true ~title:"Log Plotter" () in
(* Register the window *)
let oid = plotter#get_oid in
Hashtbl.add windows oid ();
plotter#set_icon (Some (GdkPixbuf.from_file Env.icon_log_file));
let vbox = GPack.vbox ~packing:plotter#add () in
let quit = fun () -> GMain.Main.quit (); exit 0 in
let close = fun () ->
plotter#destroy ();
Hashtbl.remove windows oid;
if Hashtbl.length windows = 0 then
quit () in
let tooltips = GData.tooltips () in
let menubar = GMenu.menu_bar ~packing:vbox#pack () in
let factory = new GMenu.factory menubar in
let accel_group = factory#accel_group in
let file_menu = factory#add_submenu "File" in
let file_menu_fact = new GMenu.factory file_menu ~accel_group in
let width = 900 and height = 200 in
let h = GPack.hbox ~packing:vbox#pack () in
let plot = new plot ~width ~height ~packing:(vbox#pack ~expand:true) () in
let open_log_item = file_menu_fact#add_item "Open Log" ~key:GdkKeysyms._O in
ignore (file_menu_fact#add_item "New" ~key:GdkKeysyms._N ~callback:(fun () -> plot_window []));
let delayed_screenshot = fun () ->
ignore (GMain.Idle.add (fun () -> screenshot plot#drawing_area; false)) in
ignore (file_menu_fact#add_item "Save screenshot" ~key:GdkKeysyms._S ~callback:delayed_screenshot);
let delayed_export = fun () ->
ignore (GMain.Idle.add (fun () -> (try plot#export_fig () with exc -> prerr_endline (Printexc.to_string exc)); false)) in
ignore (file_menu_fact#add_item "Export fig" ~key:GdkKeysyms._X ~callback:delayed_export);
ignore (file_menu_fact#add_separator ());
ignore (file_menu_fact#add_item "Close" ~key:GdkKeysyms._W ~callback:close);
ignore (file_menu_fact#add_item "Quit" ~key:GdkKeysyms._Q ~callback:quit);
let curves_menu = factory#add_submenu "Curves" in
let curves_menu_fact = new GMenu.factory curves_menu in
tooltips#set_tip curves_menu#coerce ~text:"Delete";
ignore (plotter#connect#destroy ~callback:close);
(* Auto Scale *)
let auto_scale = GButton.check_button ~label:"Auto Scale" ~active:true ~packing:h#pack () in
ignore (auto_scale#connect#toggled (fun () -> plot#set_auto_scale auto_scale#active));
let bounds = [
("Tmin", plot#min_x, plot#set_min_x);
("Tmax", plot#max_x, plot#set_max_x);
("Ymin", plot#min_y, plot#set_min_y);
("Ymax", plot#max_y, plot#set_max_y)] in
let entries =
List.map (fun (label, value, action) ->
let _, entry= labelled_entry ~width_chars:8 label "" h in
plot#scale_event (fun () -> entry#set_text (string_of_float (value ())));
ignore (entry#connect#activate ~callback:(fun () -> action (float_of_string entry#text)));
entry)
bounds in
let active_min_maxs = fun () ->
let b = not auto_scale#active in
List.iter (fun entry -> entry#misc#set_sensitive b) entries in
ignore (auto_scale#connect#toggled active_min_maxs);
active_min_maxs ();
(* Constants *)
let _, cst = labelled_entry ~width_chars:5 "Constant:" "" h in
let add_cst = fun s ->
let v = float_of_string s in
plot#add_cst v;
let eb = GBin.event_box ~width:10 ~height:10 () in
eb#coerce#misc#modify_bg [`NORMAL, `NAME "black"];
let item = curves_menu_fact#add_image_item ~image:eb#coerce ~label:s () in
let delete = fun () ->
plot#delete_cst v;
curves_menu#remove (item :> GMenu.menu_item) in
ignore (item#connect#activate ~callback:delete);
in
ignore (cst#connect#activate ~callback:(fun () ->add_cst cst#text));
tooltips#set_tip cst#coerce ~text:"Enter value for a constant curve";
(* Factor *)
let factor_label, factor = labelled_entry ~width_chars:5 "Scale next by" "1+0" h in
tooltips#set_tip factor#coerce ~text:"Scale next curve (e.g. 0.0174 to convert deg in rad, 57.3 to convert rad in deg, 1.8+32 to convert Celsius into Fahrenheit)";
List.iter
(fun (ac, menu_name, (msgs, raw_msgs), protocol) ->
add_ac_submenu protocol ~factor:(factor:>text_value) plot factory curves_menu_fact ac menu_name msgs raw_msgs)
!logs_menus;
ignore(open_log_item#connect#activate ~callback:(fun () -> let factor = (factor:>text_value) in open_log ~factor plot factory curves_menu_fact ()));
List.iter (fun f -> load_log ?export ~factor:(factor:>text_value) plot factory curves_menu_fact f) init;
plotter#add_accel_group accel_group;
plotter#show ()
(***************************** Main ****************************************)
let () =
let logs = ref []
and export = ref false in
Arg.parse
[ ("-export_csv", Arg.Set export, "Export in CSV in batch mode according to saved preferences (conf/%gconf.xml)");
("-v", Arg.Set verbose, "Verbose")]
(fun x -> logs := x :: !logs)
"Usage: logplotter <log files>";
plot_window ~export: !export !logs;
if not !export then
let loop = Glib.Main.create true in
while Glib.Main.is_running loop do
ignore (Glib.Main.iteration true)
done