1 // panel_io.cxx - I/O for 2D panel.
3 // Written by David Megginson, started January 2000.
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.
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.
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.
29 #include <simgear/compiler.h>
30 #include <simgear/debug/logstream.hxx>
31 #include <simgear/misc/props.hxx>
38 #include "panel_io.hxx"
45 ////////////////////////////////////////////////////////////////////////
46 // Built-in layer for the magnetic compass ribbon layer.
48 // TODO: move this out into a special directory for built-in
49 // layers of various sorts.
50 ////////////////////////////////////////////////////////////////////////
52 class FGMagRibbon : public FGTexturedLayer
55 FGMagRibbon (int w, int h);
56 virtual ~FGMagRibbon () {}
61 FGMagRibbon::FGMagRibbon (int w, int h)
62 : FGTexturedLayer(w, h)
64 CroppedTexture texture("Textures/Panel/compass-ribbon.rgb");
71 double heading = FGSteam::get_MH_deg();
72 double xoffset, yoffset;
74 while (heading >= 360.0) {
77 while (heading < 0.0) {
81 if (heading >= 60.0 && heading <= 180.0) {
82 xoffset = heading / 240.0;
84 } else if (heading >= 150.0 && heading <= 270.0) {
85 xoffset = (heading - 90.0) / 240.0;
87 } else if (heading >= 240.0 && heading <= 360.0) {
88 xoffset = (heading - 180.0) / 240.0;
93 xoffset = (heading - 270.0) / 240.0;
97 xoffset = 1.0 - xoffset;
98 // Adjust to put the number in the centre
101 CroppedTexture &t = getTexture();
104 t.maxX = xoffset + 0.5;
105 t.maxY = yoffset + 0.25;
106 FGTexturedLayer::draw();
111 ////////////////////////////////////////////////////////////////////////
112 // Read and construct a panel.
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
120 // A panel contains zero or more instruments.
122 // An instrument contains one or more layers and zero or more actions.
124 // A layer contains zero or more transformations.
126 // Some special types of layers also contain other objects, such as
127 // chunks of text or other layers.
129 // There are currently four types of layers:
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)
136 // The only built-in layer so far is the ribbon for the magnetic compass
137 // (class="compass-ribbon").
139 // There are three types of actions:
141 // 1. Adjust (type="adjust"), the default
142 // 2. Swap (type="swap")
143 // 3. Toggle (type="toggle")
145 // There are three types of transformations:
147 // 1. X shift (type="x-shift"), the default
148 // 2. Y shift (type="y-shift")
149 // 3. Rotation (type="rotation")
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 ////////////////////////////////////////////////////////////////////////
158 * Read a cropped texture from the instrument's property list.
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.
165 static CroppedTexture
166 readTexture (SGPropertyNode node)
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());
179 * Read an action from the instrument's property list.
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).
185 * The following action types are defined:
187 * "adjust" - modify the value of a floating-point property by
188 * the increment specified. This is the default.
190 * "swap" - swap the values of two-floating-point properties.
192 * "toggle" - toggle the value of a boolean property between true and
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.
200 * The action will be scaled automatically if the instrument is not
201 * being drawn at its regular size.
203 static FGPanelAction *
204 readAction (SGPropertyNode node, float hscale, float vscale)
206 FGPanelAction * action = 0;
208 string name = node.getStringValue("name");
209 string type = node.getStringValue("type");
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);
218 FG_LOG(FG_INPUT, FG_ALERT,
219 "No type supplied for action " << name << " assuming \"adjust\"");
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);
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);
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);
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);
256 FG_LOG(FG_INPUT, FG_ALERT, "Unrecognized action type " << type);
265 * Read a transformation from the instrument's property list.
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.
273 * There are three types of transformations:
275 * "x-shift" - move the layer horizontally.
277 * "y-shift" - move the layer vertically.
279 * "rotation" - rotate the layer.
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).
286 * Note that because of the way OpenGL works, transformations will
287 * appear to be applied backwards.
289 static FGPanelTransformation *
290 readTransformation (SGPropertyNode node, float hscale, float vscale)
292 FGPanelTransformation * t = new FGPanelTransformation;
294 string name = node.getName();
295 string type = node.getStringValue("type");
296 string propName = node.getStringValue("property", "");
300 FG_LOG(FG_INPUT, FG_ALERT,
301 "No type supplied for transformation " << name
302 << " assuming \"rotation\"");
306 if (propName != "") {
307 value = current_properties.getValue(propName, true);
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);
316 // Move the layer horizontally.
317 if (type == "x-shift") {
318 t->type = FGPanelTransformation::XSHIFT;
324 // Move the layer vertically.
325 else if (type == "y-shift") {
326 t->type = FGPanelTransformation::YSHIFT;
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;
340 FG_LOG(FG_INPUT, FG_ALERT, "Unrecognized transformation type " << type);
345 FG_LOG(FG_INPUT, FG_INFO, "Read transformation " << name);
351 * Read a chunk of text from the instrument's property list.
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
358 * "literal" - a literal text string (the default)
360 * "text-value" - the current value of a string property
362 * "number-value" - the current value of a floating-point property.
364 * All three may also include a printf-style format string.
367 readTextChunk (SGPropertyNode node)
369 FGTextLayer::Chunk * chunk;
370 string name = node.getStringValue("name");
371 string type = node.getStringValue("type");
372 string format = node.getStringValue("format");
374 // Default to literal text.
376 FG_LOG(FG_INPUT, FG_INFO, "No type provided for text chunk " << name
377 << " assuming \"literal\"");
381 // A literal text string.
382 if (type == "literal") {
383 string text = node.getStringValue("text");
384 chunk = new FGTextLayer::Chunk(text, format);
387 // The value of a string property.
388 else if (type == "text-value") {
390 current_properties.getValue(node.getStringValue("property"), true);
391 chunk = new FGTextLayer::Chunk(FGTextLayer::TEXT_VALUE, value, format);
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,
405 FG_LOG(FG_INPUT, FG_ALERT, "Unrecognized type " << type
406 << " for text chunk " << name);
415 * Read a single layer from an instrument's property list.
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.
422 * This module currently recognizes four kinds of layers:
424 * "texture" - a layer containing a texture (the default)
426 * "text" - a layer containing text
428 * "switch" - a layer that switches between two other layers
429 * based on the current value of a boolean property.
431 * "built-in" - a hard-coded layer supported by C++ code in FlightGear.
433 * Currently, the only built-in layer class is "compass-ribbon".
435 static FGInstrumentLayer *
436 readLayer (SGPropertyNode node, float hscale, float vscale)
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);
450 FG_LOG(FG_INPUT, FG_ALERT,
451 "No type supplied for layer " << name
452 << " assuming \"texture\"");
457 // A textured instrument layer.
458 if (type == "texture") {
459 CroppedTexture texture = readTexture(node.getSubNode("texture"));
460 layer = new FGTexturedLayer(texture, w, h);
464 // A textual instrument layer.
465 else if (type == "text") {
466 FGTextLayer * tlayer = new FGTextLayer(w, h); // FIXME
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);
474 // Set the point size.
475 float pointSize = node.getFloatValue("point-size", 10.0) * hscale;
476 tlayer->setPointSize(pointSize);
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));
489 tlayer->addChunk(chunk);
494 // A switch instrument layer.
495 else if (type == "switch") {
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);
505 // A built-in instrument layer.
506 else if (type == "built-in") {
507 string layerclass = node.getStringValue("class");
509 if (layerclass == "mag-ribbon") {
510 layer = new FGMagRibbon(w, h);
513 else if (layerclass == "") {
514 FG_LOG(FG_INPUT, FG_ALERT, "No class provided for built-in layer "
520 FG_LOG(FG_INPUT, FG_ALERT, "Unknown built-in layer class "
528 FG_LOG(FG_INPUT, FG_ALERT, "Unrecognized layer type " << type);
534 // Get the transformations for each layer.
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),
545 layer->addTransformation(t);
548 FG_LOG(FG_INPUT, FG_INFO, "Read layer " << name);
554 * Read an instrument from a property list.
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.
564 static FGPanelInstrument *
565 readInstrument (SGPropertyNode node, int x, int y, int real_w, int real_h)
567 int w = node.getIntValue("w");
568 int h = node.getIntValue("h");
569 const string &name = node.getStringValue("name");
574 hscale = float(real_w) / float(w);
576 cerr << "hscale is " << hscale << endl;
579 vscale = float(real_h) / float(h);
581 cerr << "vscale is " << hscale << endl;
584 FG_LOG(FG_INPUT, FG_INFO, "Reading instrument " << name);
586 FGLayeredInstrument * instrument =
587 new FGLayeredInstrument(x, y, w, h);
590 // Get the actions for the instrument.
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),
601 instrument->addAction(action);
605 // Get the layers for the instrument.
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),
616 instrument->addLayer(layer);
619 FG_LOG(FG_INPUT, FG_INFO, "Done reading instrument " << name);
625 pp * Read a panel from a property list.
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).
632 * Returns 0 if the read fails for any reason.
635 fgReadPanel (istream &input)
637 SGPropertyList props;
641 // Read the property list from disk.
643 if (!readPropertyList(input, &props)) {
644 FG_LOG(FG_INPUT, FG_ALERT, "Malformed property list for panel.");
647 FG_LOG(FG_INPUT, FG_INFO, "Read properties for panel " <<
648 props.getStringValue("/name"));
651 // Construct a new, empty panel.
653 FGPanel * panel = new FGPanel(0, 0, 1024, 768);// FIXME: use variable size
656 // Assign the background texture, if any, or a bogus chequerboard.
658 string bgTexture = props.getStringValue("/background");
661 panel->setBackground(FGTextureManager::createTexture(bgTexture.c_str()));
662 FG_LOG(FG_INPUT, FG_INFO, "Set background texture to " << bgTexture);
666 // Create each instrument.
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);
675 string path = node.getStringValue("path");
677 FG_LOG(FG_INPUT, FG_INFO, "Reading instrument "
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);
687 if (x == -1 || y == -1) {
688 FG_LOG(FG_INPUT, FG_ALERT, "x and y positions must be specified and >0");
693 if (!readPropertyList(path, &props2)) {
698 FGPanelInstrument * instrument =
699 readInstrument(SGPropertyNode("/", &props2), x, y, w, h);
700 if (instrument == 0) {
705 panel->addInstrument(instrument);
707 FG_LOG(FG_INPUT, FG_INFO, "Done reading panel instruments");
711 // Return the new panel.
717 // end of panel_io.cxx