]> git.mxchange.org Git - flightgear.git/blob - src/Cockpit/panel_io.cxx
ae127d185b6aed20e1a46864f307a7fe07f1fd3f
[flightgear.git] / src / Cockpit / panel_io.cxx
1 //  panel_io.cxx - I/O for 2D panel.
2 //
3 //  Written by David Megginson, started January 2000.
4 //
5 //  This program is free software; you can redistribute it and/or
6 //  modify it under the terms of the GNU General Public License as
7 //  published by the Free Software Foundation; either version 2 of the
8 //  License, or (at your option) any later version.
9 // 
10 //  This program is distributed in the hope that it will be useful, but
11 //  WITHOUT ANY WARRANTY; without even the implied warranty of
12 //  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 //  General Public License for more details.
14 // 
15 //  You should have received a copy of the GNU General Public License
16 //  along with this program; if not, write to the Free Software
17 //  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18 //
19 //  $Id$
20
21 #ifdef HAVE_CONFIG_H
22 #  include <config.h>
23 #endif
24
25 #ifdef HAVE_WINDOWS_H          
26 #  include <windows.h>
27 #endif
28
29 #include <simgear/compiler.h>
30 #include <simgear/debug/logstream.hxx>
31 #include <simgear/misc/props.hxx>
32
33 #include <iostream>
34 #include <string>
35
36 #include "panel.hxx"
37 #include "steam.hxx"
38 #include "panel_io.hxx"
39
40 using std::istream;
41 using std::string;
42
43
44 \f
45 ////////////////////////////////////////////////////////////////////////
46 // Built-in layer for the magnetic compass ribbon layer.
47 //
48 // TODO: move this out into a special directory for built-in
49 // layers of various sorts.
50 ////////////////////////////////////////////////////////////////////////
51
52 class FGMagRibbon : public FGTexturedLayer
53 {
54 public:
55   FGMagRibbon (int w, int h);
56   virtual ~FGMagRibbon () {}
57
58   virtual void draw ();
59 };
60
61 FGMagRibbon::FGMagRibbon (int w, int h)
62   : FGTexturedLayer(w, h)
63 {
64   CroppedTexture texture("Textures/Panel/compass-ribbon.rgb");
65   setTexture(texture);
66 }
67
68 void
69 FGMagRibbon::draw ()
70 {
71   double heading = FGSteam::get_MH_deg();
72   double xoffset, yoffset;
73
74   while (heading >= 360.0) {
75     heading -= 360.0;
76   }
77   while (heading < 0.0) {
78     heading += 360.0;
79   }
80
81   if (heading >= 60.0 && heading <= 180.0) {
82     xoffset = heading / 240.0;
83     yoffset = 0.75;
84   } else if (heading >= 150.0 && heading <= 270.0) {
85     xoffset = (heading - 90.0) / 240.0;
86     yoffset = 0.50;
87   } else if (heading >= 240.0 && heading <= 360.0) {
88     xoffset = (heading - 180.0) / 240.0;
89     yoffset = 0.25;
90   } else {
91     if (heading < 270.0)
92       heading += 360.0;
93     xoffset = (heading - 270.0) / 240.0;
94     yoffset = 0.0;
95   }
96
97   xoffset = 1.0 - xoffset;
98                                 // Adjust to put the number in the centre
99   xoffset -= 0.25;
100
101   CroppedTexture &t = getTexture();
102   t.minX = xoffset;
103   t.minY = yoffset;
104   t.maxX = xoffset + 0.5;
105   t.maxY = yoffset + 0.25;
106   FGTexturedLayer::draw();
107 }
108
109
110 \f
111 ////////////////////////////////////////////////////////////////////////
112 // Read and construct a panel.
113 //
114 // The panel is specified as a regular property list, and each of the
115 // instruments is its own, separate property list (and thus, a separate
116 // XML document).  The functions in this section read in the files
117 // as property lists, then extract properties to set up the panel
118 // itself.
119 //
120 // A panel contains zero or more instruments.
121 //
122 // An instrument contains one or more layers and zero or more actions.
123 //
124 // A layer contains zero or more transformations.
125 //
126 // Some special types of layers also contain other objects, such as 
127 // chunks of text or other layers.
128 //
129 // There are currently four types of layers:
130 //
131 // 1. Textured Layer (type="texture"), the default
132 // 2. Text Layer (type="text")
133 // 3. Switch Layer (type="switch")
134 // 4. Built-in Layer (type="built-in", must also specify class)
135 //
136 // The only built-in layer so far is the ribbon for the magnetic compass
137 // (class="compass-ribbon").
138 //
139 // There are three types of actions:
140 //
141 // 1. Adjust (type="adjust"), the default
142 // 2. Swap (type="swap")
143 // 3. Toggle (type="toggle")
144 //
145 // There are three types of transformations:
146 //
147 // 1. X shift (type="x-shift"), the default
148 // 2. Y shift (type="y-shift")
149 // 3. Rotation (type="rotation")
150 //
151 // Each of these may be associated with a property, so that a needle
152 // will rotate with the airspeed, for example, or may have a fixed
153 // floating-point value.
154 ////////////////////////////////////////////////////////////////////////
155
156
157 /**
158  * Read a cropped texture from the instrument's property list.
159  *
160  * The x1 and y1 properties give the starting position of the texture
161  * (between 0.0 and 1.0), and the the x2 and y2 properties give the
162  * ending position.  For example, to use the bottom-left quarter of a
163  * texture, x1=0.0, y1=0.0, x2=0.5, y2=0.5.
164  */
165 static CroppedTexture
166 readTexture (SGPropertyNode node)
167 {
168   CroppedTexture texture(node.getStringValue("path"),
169                          node.getFloatValue("x1"),
170                          node.getFloatValue("y1"),
171                          node.getFloatValue("x2", 1.0),
172                          node.getFloatValue("y2", 1.0));
173   FG_LOG(FG_INPUT, FG_INFO, "Read texture " << node.getName());
174   return texture;
175 }
176
177
178 /**
179  * Read an action from the instrument's property list.
180  *
181  * The action will be performed when the user clicks a mouse button
182  * within the specified region of the instrument.  Actions always
183  * work by modifying the value of a property (see the SGValue class).
184  *
185  * The following action types are defined:
186  *
187  * "adjust" - modify the value of a floating-point property by
188  *    the increment specified.  This is the default.
189  *
190  * "swap" - swap the values of two-floating-point properties.
191  *
192  * "toggle" - toggle the value of a boolean property between true and
193  *    false.
194  *
195  * For the adjust action, it is possible to specify an increment
196  * (use a negative number for a decrement), a minimum allowed value,
197  * a maximum allowed value, and a flag to indicate whether the value
198  * should freeze or wrap-around when it reachs the minimum or maximum.
199  *
200  * The action will be scaled automatically if the instrument is not
201  * being drawn at its regular size.
202  */
203 static FGPanelAction *
204 readAction (SGPropertyNode node, float hscale, float vscale)
205 {
206   FGPanelAction * action = 0;
207
208   string name = node.getStringValue("name");
209   string type = node.getStringValue("type");
210
211   int button = node.getIntValue("button");
212   int x = int(node.getIntValue("x") * hscale);
213   int y = int(node.getIntValue("y") * vscale);
214   int w = int(node.getIntValue("w") * hscale);
215   int h = int(node.getIntValue("h") * vscale);
216
217   if (type == "") {
218     FG_LOG(FG_INPUT, FG_ALERT,
219            "No type supplied for action " << name << " assuming \"adjust\"");
220     type = "adjust";
221   }
222
223                                 // Adjust a property value
224   if (type == "adjust") {
225     string propName = node.getStringValue("property");
226     SGValue * value = current_properties.getValue(propName, true);
227     float increment = node.getFloatValue("increment", 1.0);
228     float min = node.getFloatValue("min", 0.0);
229     float max = node.getFloatValue("max", 0.0);
230     bool wrap = node.getBoolValue("wrap", false);
231     if (min == max)
232       FG_LOG(FG_INPUT, FG_ALERT, "Action " << node.getName()
233              << " has same min and max value");
234     action = new FGAdjustAction(button, x, y, w, h, value,
235                                 increment, min, max, wrap);
236   } 
237
238                                 // Swap two property values
239   else if (type == "swap") {
240     string propName1 = node.getStringValue("property1");
241     string propName2 = node.getStringValue("property2");
242     SGValue * value1 = current_properties.getValue(propName1, true);
243     SGValue * value2 = current_properties.getValue(propName2, true);
244     action = new FGSwapAction(button, x, y, w, h, value1, value2);
245   } 
246
247                                 // Toggle a boolean value
248   else if (type == "toggle") {
249     string propName = node.getStringValue("property");
250     SGValue * value = current_properties.getValue(propName, true);
251     action = new FGToggleAction(button, x, y, w, h, value);
252   } 
253
254                                 // Unrecognized type
255   else {
256     FG_LOG(FG_INPUT, FG_ALERT, "Unrecognized action type " << type);
257     return 0;
258   }
259
260   return action;
261 }
262
263
264 /**
265  * Read a transformation from the instrument's property list.
266  *
267  * The panel module uses the transformations to slide or spin needles,
268  * knobs, and other indicators, and to place layers in the correct
269  * positions.  Every layer starts centered exactly on the x,y co-ordinate,
270  * and many layers need to be moved or rotated simply to display the
271  * instrument correctly.
272  *
273  * There are three types of transformations:
274  *
275  * "x-shift" - move the layer horizontally.
276  *
277  * "y-shift" - move the layer vertically.
278  *
279  * "rotation" - rotate the layer.
280  *
281  * Each transformation may have a fixed offset, and may also have
282  * a floating-point property value to add to the offset.  The
283  * floating-point property may be clamped to a minimum and/or
284  * maximum range and scaled (after clamping).
285  *
286  * Note that because of the way OpenGL works, transformations will
287  * appear to be applied backwards.
288  */
289 static FGPanelTransformation *
290 readTransformation (SGPropertyNode node, float hscale, float vscale)
291 {
292   FGPanelTransformation * t = new FGPanelTransformation;
293
294   string name = node.getName();
295   string type = node.getStringValue("type");
296   string propName = node.getStringValue("property", "");
297   SGValue * value = 0;
298
299   if (type == "") {
300     FG_LOG(FG_INPUT, FG_ALERT,
301            "No type supplied for transformation " << name
302            << " assuming \"rotation\"");
303     type = "rotation";
304   }
305
306   if (propName != "") {
307     value = current_properties.getValue(propName, true);
308   }
309
310   t->value = value;
311   t->min = node.getFloatValue("min", -9999999);
312   t->max = node.getFloatValue("max", 99999999);
313   t->factor = node.getFloatValue("scale", 1.0);
314   t->offset = node.getFloatValue("offset", 0.0);
315
316                                 // Move the layer horizontally.
317   if (type == "x-shift") {
318     t->type = FGPanelTransformation::XSHIFT;
319     t->min *= hscale;
320     t->max *= hscale;
321     t->offset *= hscale;
322   } 
323
324                                 // Move the layer vertically.
325   else if (type == "y-shift") {
326     t->type = FGPanelTransformation::YSHIFT;
327     t->min *= vscale;
328     t->max *= vscale;
329     t->offset *= vscale;
330   } 
331
332                                 // Rotate the layer.  The rotation
333                                 // is in degrees, and does not need
334                                 // to scale with the instrument size.
335   else if (type == "rotation") {
336     t->type = FGPanelTransformation::ROTATION;
337   } 
338
339   else {
340     FG_LOG(FG_INPUT, FG_ALERT, "Unrecognized transformation type " << type);
341     delete t;
342     return 0;
343   }
344
345   FG_LOG(FG_INPUT, FG_INFO, "Read transformation " << name);
346   return t;
347 }
348
349
350 /**
351  * Read a chunk of text from the instrument's property list.
352  *
353  * A text layer consists of one or more chunks of text.  All chunks
354  * share the same font size and color (and eventually, font), but
355  * each can come from a different source.  There are three types of
356  * text chunks:
357  *
358  * "literal" - a literal text string (the default)
359  *
360  * "text-value" - the current value of a string property
361  *
362  * "number-value" - the current value of a floating-point property.
363  *
364  * All three may also include a printf-style format string.
365  */
366 FGTextLayer::Chunk *
367 readTextChunk (SGPropertyNode node)
368 {
369   FGTextLayer::Chunk * chunk;
370   string name = node.getStringValue("name");
371   string type = node.getStringValue("type");
372   string format = node.getStringValue("format");
373
374                                 // Default to literal text.
375   if (type == "") {
376     FG_LOG(FG_INPUT, FG_INFO, "No type provided for text chunk " << name
377            << " assuming \"literal\"");
378     type = "literal";
379   }
380
381                                 // A literal text string.
382   if (type == "literal") {
383     string text = node.getStringValue("text");
384     chunk = new FGTextLayer::Chunk(text, format);
385   }
386
387                                 // The value of a string property.
388   else if (type == "text-value") {
389     SGValue * value =
390       current_properties.getValue(node.getStringValue("property"), true);
391     chunk = new FGTextLayer::Chunk(FGTextLayer::TEXT_VALUE, value, format);
392   }
393
394                                 // The value of a float property.
395   else if (type == "number-value") {
396     string propName = node.getStringValue("property");
397     float scale = node.getFloatValue("scale", 1.0);
398     SGValue * value = current_properties.getValue(propName, true);
399     chunk = new FGTextLayer::Chunk(FGTextLayer::DOUBLE_VALUE, value,
400                                    format, scale);
401   }
402
403                                 // Unknown type.
404   else {
405     FG_LOG(FG_INPUT, FG_ALERT, "Unrecognized type " << type
406            << " for text chunk " << name);
407     return 0;
408   }
409
410   return chunk;
411 }
412
413
414 /**
415  * Read a single layer from an instrument's property list.
416  *
417  * Each instrument consists of one or more layers stacked on top
418  * of each other; the lower layers show through only where the upper
419  * layers contain an alpha component.  Each layer can be moved
420  * horizontally and vertically and rotated using transformations.
421  *
422  * This module currently recognizes four kinds of layers:
423  *
424  * "texture" - a layer containing a texture (the default)
425  *
426  * "text" - a layer containing text
427  *
428  * "switch" - a layer that switches between two other layers
429  *   based on the current value of a boolean property.
430  *
431  * "built-in" - a hard-coded layer supported by C++ code in FlightGear.
432  *
433  * Currently, the only built-in layer class is "compass-ribbon".
434  */
435 static FGInstrumentLayer *
436 readLayer (SGPropertyNode node, float hscale, float vscale)
437 {
438   FGInstrumentLayer * layer;
439   string name = node.getStringValue("name");
440   string type = node.getStringValue("type");
441   int w = node.getIntValue("w", -1);
442   int h = node.getIntValue("h", -1);
443   if (w != -1)
444     w = int(w * hscale);
445   if (h != -1)
446     h = int(h * vscale);
447
448
449   if (type == "") {
450     FG_LOG(FG_INPUT, FG_ALERT,
451            "No type supplied for layer " << name
452            << " assuming \"texture\"");
453     type = "texture";
454   }
455
456
457                                 // A textured instrument layer.
458   if (type == "texture") {
459     CroppedTexture texture = readTexture(node.getSubNode("texture"));
460     layer = new FGTexturedLayer(texture, w, h);
461   }
462
463
464                                 // A textual instrument layer.
465   else if (type == "text") {
466     FGTextLayer * tlayer = new FGTextLayer(w, h); // FIXME
467
468                                 // Set the text color.
469     float red = node.getFloatValue("color/red", 0.0);
470     float green = node.getFloatValue("color/green", 0.0);
471     float blue = node.getFloatValue("color/blue", 0.0);
472     tlayer->setColor(red, green, blue);
473
474                                 // Set the point size.
475     float pointSize = node.getFloatValue("point-size", 10.0) * hscale;
476     tlayer->setPointSize(pointSize);
477
478                                 // Set the font.
479     // TODO
480
481     SGPropertyNode chunk_group = node.getSubNode("chunks");
482     int nChunks = chunk_group.size();
483     for (int i = 0; i < nChunks; i++) {
484       FGTextLayer::Chunk * chunk = readTextChunk(chunk_group.getChild(i));
485       if (chunk == 0) {
486         delete layer;
487         return 0;
488       }
489       tlayer->addChunk(chunk);
490     }
491     layer = tlayer;
492   }
493
494                                 // A switch instrument layer.
495   else if (type == "switch") {
496     SGValue * value =
497       current_properties.getValue(node.getStringValue("property"));
498     FGInstrumentLayer * layer1 =
499       readLayer(node.getSubNode("layer1"), hscale, vscale);
500     FGInstrumentLayer * layer2 =
501       readLayer(node.getSubNode("layer2"), hscale, vscale);
502     layer = new FGSwitchLayer(w, h, value, layer1, layer2);
503   }
504
505                                 // A built-in instrument layer.
506   else if (type == "built-in") {
507     string layerclass = node.getStringValue("class");
508
509     if (layerclass == "mag-ribbon") {
510       layer = new FGMagRibbon(w, h);
511     }
512
513     else if (layerclass == "") {
514       FG_LOG(FG_INPUT, FG_ALERT, "No class provided for built-in layer "
515              << name);
516       return 0;
517     }
518
519     else {
520       FG_LOG(FG_INPUT, FG_ALERT, "Unknown built-in layer class "
521              << layerclass);
522       return 0;
523     }
524   }
525
526                                 // An unknown type.
527   else {
528     FG_LOG(FG_INPUT, FG_ALERT, "Unrecognized layer type " << type);
529     delete layer;
530     return 0;
531   }
532   
533   //
534   // Get the transformations for each layer.
535   //
536   SGPropertyNode trans_group = node.getSubNode("transformations");
537   int nTransformations = trans_group.size();
538   for (int k = 0; k < nTransformations; k++) {
539     FGPanelTransformation * t = readTransformation(trans_group.getChild(k),
540                                                    hscale, vscale);
541     if (t == 0) {
542       delete layer;
543       return 0;
544     }
545     layer->addTransformation(t);
546   }
547   
548   FG_LOG(FG_INPUT, FG_INFO, "Read layer " << name);
549   return layer;
550 }
551
552
553 /**
554  * Read an instrument from a property list.
555  *
556  * The instrument consists of a preferred width and height
557  * (the panel may override these), together with a list of layers
558  * and a list of actions to be performed when the user clicks 
559  * the mouse over the instrument.  All co-ordinates are relative
560  * to the instrument's position, so instruments are fully relocatable;
561  * likewise, co-ordinates for actions and transformations will be
562  * scaled automatically if the instrument is not at its preferred size.
563  */
564 static FGPanelInstrument *
565 readInstrument (SGPropertyNode node, int x, int y, int real_w, int real_h)
566 {
567   int w = node.getIntValue("w");
568   int h = node.getIntValue("h");
569   const string &name = node.getStringValue("name");
570
571   float hscale = 1.0;
572   float vscale = 1.0;
573   if (real_w != -1) {
574     hscale = float(real_w) / float(w);
575     w = real_w;
576     cerr << "hscale is " << hscale << endl;
577   }
578   if (real_h != -1) {
579     vscale = float(real_h) / float(h);
580     h = real_h;
581     cerr << "vscale is " << hscale << endl;
582   }
583
584   FG_LOG(FG_INPUT, FG_INFO, "Reading instrument " << name);
585
586   FGLayeredInstrument * instrument =
587     new FGLayeredInstrument(x, y, w, h);
588
589   //
590   // Get the actions for the instrument.
591   //
592   SGPropertyNode action_group = node.getSubNode("actions");
593   int nActions = action_group.size();
594   for (int j = 0; j < nActions; j++) {
595     FGPanelAction * action = readAction(action_group.getChild(j),
596                                         hscale, vscale);
597     if (action == 0) {
598       delete instrument;
599       return 0;
600     }
601     instrument->addAction(action);
602   }
603
604   //
605   // Get the layers for the instrument.
606   //
607   SGPropertyNode layer_group = node.getSubNode("layers");
608   int nLayers = layer_group.size();
609   for (int j = 0; j < nLayers; j++) {
610     FGInstrumentLayer * layer = readLayer(layer_group.getChild(j),
611                                           hscale, vscale);
612     if (layer == 0) {
613       delete instrument;
614       return 0;
615     }
616     instrument->addLayer(layer);
617   }
618
619   FG_LOG(FG_INPUT, FG_INFO, "Done reading instrument " << name);
620   return instrument;
621 }
622
623
624 /**
625 pp * Read a panel from a property list.
626  *
627  * Each panel instrument will appear in its own, separate
628  * property list.  The top level simply names the panel and
629  * places the instruments in their appropriate locations (and
630  * optionally resizes them if necessary).
631  *
632  * Returns 0 if the read fails for any reason.
633  */
634 FGPanel *
635 fgReadPanel (istream &input)
636 {
637   SGPropertyList props;
638
639
640   //
641   // Read the property list from disk.
642   //
643   if (!readPropertyList(input, &props)) {
644     FG_LOG(FG_INPUT, FG_ALERT, "Malformed property list for panel.");
645     return 0;
646   }
647   FG_LOG(FG_INPUT, FG_INFO, "Read properties for panel " <<
648          props.getStringValue("/name"));
649
650   //
651   // Construct a new, empty panel.
652   //
653   FGPanel * panel = new FGPanel(0, 0, 1024, 768);// FIXME: use variable size
654
655   //
656   // Assign the background texture, if any, or a bogus chequerboard.
657   //
658   string bgTexture = props.getStringValue("/background");
659   if (bgTexture == "")
660     bgTexture = "FOO";
661   panel->setBackground(FGTextureManager::createTexture(bgTexture.c_str()));
662   FG_LOG(FG_INPUT, FG_INFO, "Set background texture to " << bgTexture);
663
664
665   //
666   // Create each instrument.
667   //
668   FG_LOG(FG_INPUT, FG_INFO, "Reading panel instruments");
669   SGPropertyNode instrument_group("/instruments", &props);
670   int nInstruments = instrument_group.size();
671   for (int i = 0; i < nInstruments; i++) {
672     SGPropertyList props2;
673     SGPropertyNode node = instrument_group.getChild(i);
674
675     string path = node.getStringValue("path");
676
677     FG_LOG(FG_INPUT, FG_INFO, "Reading instrument "
678            << node.getName()
679            << " from "
680            << path);
681
682     int x = node.getIntValue("x", -1);
683     int y = node.getIntValue("y", -1);
684     int w = node.getIntValue("w", -1);
685     int h = node.getIntValue("h", -1);
686
687     if (x == -1 || y == -1) {
688       FG_LOG(FG_INPUT, FG_ALERT, "x and y positions must be specified and >0");
689       delete panel;
690       return 0;
691     }
692
693     if (!readPropertyList(path, &props2)) {
694       delete panel;
695       return 0;
696     }
697
698     FGPanelInstrument * instrument =
699       readInstrument(SGPropertyNode("/", &props2), x, y, w, h);
700     if (instrument == 0) {
701       delete instrument;
702       delete panel;
703       return 0;
704     }
705     panel->addInstrument(instrument);
706   }
707   FG_LOG(FG_INPUT, FG_INFO, "Done reading panel instruments");
708
709
710   //
711   // Return the new panel.
712   //
713   return panel;
714 }
715
716
717 // end of panel_io.cxx