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