1 // metar interface class
2 //
3 // Written by Melchior FRANZ, started December 2003.
4 //
5 // Copyright (C) 2003 Melchior FRANZ - mfranz@aon.at
6 //
7 // This program is free software; you can redistribute it and/or
8 // modify it under the terms of the GNU General Public License as
9 // published by the Free Software Foundation; either version 2 of the
10 // License, or (at your option) any later version.
11 //
12 // This program is distributed in the hope that it will be useful, but
13 // WITHOUT ANY WARRANTY; without even the implied warranty of
14 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 // General Public License for more details.
16 //
17 // You should have received a copy of the GNU General Public License
18 // along with this program; if not, write to the Free Software
19 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 //
21 // $Id$
22
23 /**
24 * @file metar.cxx
25 * Interface for encoded Meteorological Aerodrome Reports (METAR).
26 *
27 * @see WMO-49
28 * Technical Regulations, Basic Documents No. 2 (WMO No. 49)
29 * Volume II - Meteorological Service for International Air Navigation
30 * http://library.wmo.int/pmb_ged/wmo_49-v2_2013_en.pdf
31 *
32 * Refer to Table A3-2 (Template for METAR and SPECI) following page 78.
33 *
34 * For general information:
35 * World Meteorological Organization http://library.wmo.int
36 */
37 #ifdef HAVE_CONFIG_H
38 # include <simgear_config.h>
39 #endif
40
41 #include <iomanip>
42 #include <string>
43 #include <time.h>
44 #include <cstring>
45 #include <ostream>
46 #include <sstream>
47
48 #include <simgear/debug/logstream.hxx>
49 #include <simgear/math/sg_random.h>
50 #include <simgear/structure/exception.hxx>
51
52 #include "metar.hxx"
53
54 #define NaN SGMetarNaN
55
56 using std::string;
57 using std::map;
58 using std::vector;
59
60 /**
61 * The constructor takes a Metar string
62 * The constructor throws sg_io_exceptions on failure. The "METAR"
63 * keyword has no effect (apart from incrementing the group counter
64 * @a grpcount) and can be left away. A keyword "SPECI" is
65 * likewise accepted.
66 *
67 * @param m ICAO station id or metar string
68 *
69 * @par Examples:
70 * @code
71 * SGMetar *m = new SGMetar("METAR KSFO 061656Z 19004KT 9SM SCT100 OVC200 08/03 A3013");
72 * double t = m->getTemperature_F();
73 * delete m;
74
75 * @endcode
76 */
SGMetar(const string & m)77 SGMetar::SGMetar(const string& m) :
78 _grpcount(0),
79 _x_proxy(false),
80 _year(-1),
81 _month(-1),
82 _day(-1),
83 _hour(-1),
84 _minute(-1),
85 _report_type(-1),
86 _wind_dir(-1),
87 _wind_speed(NaN),
88 _gust_speed(NaN),
89 _wind_range_from(-1),
90 _wind_range_to(-1),
91 _temp(NaN),
92 _dewp(NaN),
93 _pressure(NaN),
94 _rain(false),
95 _hail(false),
96 _snow(false),
97 _cavok(false)
98 {
99 _data = new char[m.length() + 2]; // make room for " \0"
100 strcpy(_data, m.c_str());
101 _url = _data;
102
103 normalizeData();
104
105 _m = _data;
106 _icao[0] = '\0';
107
108 // NOAA preample
109 if (!scanPreambleDate())
110 useCurrentDate();
111 scanPreambleTime();
112
113 // METAR header
114 scanType();
115 if (!scanId() || !scanDate()) {
116 delete[] _data;
117 throw sg_io_exception("metar data bogus ", sg_location(_url));
118 }
119 scanModifier();
120
121 // base set
122 scanWind();
123 scanVariability();
124 while (scanVisibility()) ;
125 while (scanRwyVisRange()) ;
126 while (scanWeather()) ;
127 while (scanSkyCondition()) ;
128 scanTemperature();
129 scanPressure();
130 while (scanSkyCondition()) ;
131 while (scanRunwayReport()) ;
132 scanWindShear();
133
134 // appendix
135 while (scanColorState()) ;
136 scanTrendForecast();
137 while (scanRunwayReport()) ;
138 scanRemainder();
139 scanRemark();
140
141 if (_grpcount < 4) {
142 delete[] _data;
143 throw sg_io_exception("metar data incomplete ", sg_location(_url));
144 }
145
146 _url = "";
147 }
148
149
150 /**
151 * Clears lists and maps to discourage access after destruction.
152 */
~SGMetar()153 SGMetar::~SGMetar()
154 {
155 _clouds.clear();
156 _runways.clear();
157 _weather.clear();
158 delete[] _data;
159 }
160
161
azimuthName(double d)162 static const char *azimuthName(double d)
163 {
164 const char *dir[] = {
165 "N", "NNE", "NE", "ENE",
166 "E", "ESE", "SE", "SSE",
167 "S", "SSW", "SW", "WSW",
168 "W", "WNW", "NW", "NNW"
169 };
170 d += 11.25;
171 while (d < 0)
172 d += 360;
173 while (d >= 360)
174 d -= 360;
175 return dir[int(d / 22.5)];
176 }
177
178
179 // round double to 10^g
rnd(double r,int g=0)180 static double rnd(double r, int g = 0)
181 {
182 double f = pow(10.0, g);
183 return f * floor(r / f + 0.5);
184 }
185
186
187 /* A manipulator that can use spaces to emulate tab characters. */
188 struct Tab
189 {
190 /* If <stops> is 0, we simply insert tab characters. Otherwise we insert
191 spaces to align with the next column at multiple of <stops>. */
TabTab192 explicit Tab(int stops)
193 :
194 _stops(stops)
195 {}
196 int _stops;
197 };
198
operator <<(std::ostream & out,const Tab & t)199 std::ostream& operator << (std::ostream& out, const Tab& t)
200 {
201 if (t._stops == 0) {
202 return out << '\t';
203 }
204
205 std::ostringstream& out2 = *(std::ostringstream*) &out;
206 std::string s = out2.str();
207
208 if (t._stops < 0) {
209 if (!s.size() || s[s.size()-1] != ' ') {
210 out << ' ';
211 }
212 return out;
213 }
214
215 auto nl = s.rfind('\n');
216 if (nl < 0) nl = 0;
217 int column = 0;
218 for (auto i = nl+1; i != s.size(); ++i) {
219 if (s[i] == '\t')
220 column = (column + t._stops) / t._stops * t._stops;
221 else
222 column += 1;
223 }
224 int column2 = (column + t._stops) / t._stops * t._stops;
225 for (int i=column; i<column2; ++i) {
226 out << ' ';
227 }
228 return out;
229 }
230
231 /* Manipulator for SGMetarVisibility using a Tab. */
232 struct SGMetarVisibilityManip
233 {
SGMetarVisibilityManipSGMetarVisibilityManip234 explicit SGMetarVisibilityManip(const SGMetarVisibility& v, const Tab& tab)
235 :
236 _v(v),
237 _tab(tab)
238 {}
239 const SGMetarVisibility& _v;
240 const Tab& _tab;
241 };
242
operator <<(std::ostream & out,const SGMetarVisibilityManip & v)243 std::ostream& operator << (std::ostream& out, const SGMetarVisibilityManip& v)
244 {
245 int m = v._v.getModifier();
246 const char *mod;
247 if (m == SGMetarVisibility::GREATER_THAN)
248 mod = ">=";
249 else if (m == SGMetarVisibility::LESS_THAN)
250 mod = "<";
251 else
252 mod = "";
253 out << mod;
254
255 double dist = rnd(v._v.getVisibility_m(), 1);
256 if (dist < 1000.0)
257 out << rnd(dist, 1) << " m";
258 else
259 out << rnd(dist / 1000.0, -1) << " km";
260
261 const char *dir = "";
262 int i;
263 if ((i = v._v.getDirection()) != -1) {
264 dir = azimuthName(i);
265 out << " " << dir;
266 }
267 out << v._tab << v._tab << v._tab << v._tab << v._tab;
268 out << mod << rnd(v._v.getVisibility_sm(), -1) << " US-miles " << dir;
269 return out;
270 }
271
272
getDescription(int tabstops) const273 std::string SGMetar::getDescription(int tabstops) const
274 {
275 std::ostringstream out;
276 const char *s;
277 char buf[256];
278 double d;
279 int i, lineno;
280 Tab tab(tabstops);
281
282 if ((i = getReportType()) == SGMetar::AUTO)
283 out << "(METAR automatically generated)\n";
284 else if (i == SGMetar::COR)
285 out << "(METAR manually corrected)\n";
286 else if (i == SGMetar::RTD)
287 out << "(METAR routine delayed)\n";
288
289 out << "Airport-Id:" << tab << tab << getId() << "\n";
290
291 // date/time
292 int year = getYear();
293 int month = getMonth();
294 out << "Report time:" << tab << tab << year << '/' << month << '/' << getDay();
295 out << ' ' << getHour() << ':';
296 out << std::setw(2) << std::setfill('0') << getMinute() << " UTC\n";
297
298
299 // visibility
300 SGMetarVisibility minvis = getMinVisibility();
301 SGMetarVisibility maxvis = getMaxVisibility();
302 double min = minvis.getVisibility_m();
303 double max = maxvis.getVisibility_m();
304 if (min != NaN) {
305 if (max != NaN) {
306 out << "min. Visibility:" << tab << SGMetarVisibilityManip(minvis, tab) << "\n";
307 out << "max. Visibility:" << tab << SGMetarVisibilityManip(maxvis, tab) << "\n";
308 } else {
309 out << "Visibility:" << tab << tab << SGMetarVisibilityManip(minvis, tab) << "\n";
310 }
311 }
312
313
314 // directed visibility
315 const SGMetarVisibility *dirvis = getDirVisibility();
316 for (i = 0; i < 8; i++, dirvis++)
317 if (dirvis->getVisibility_m() != NaN)
318 out << tab << tab << tab << SGMetarVisibilityManip(*dirvis, tab) << "\n";
319
320
321 // vertical visibility
322 SGMetarVisibility vertvis = getVertVisibility();
323 if ((d = vertvis.getVisibility_ft()) != NaN)
324 out << "Vert. visibility:" << tab << SGMetarVisibilityManip(vertvis, tab) << "\n";
325 else if (vertvis.getModifier() == SGMetarVisibility::NOGO)
326 out << "Vert. visibility:" << tab << "impossible to determine" << "\n";
327
328
329 // wind
330 d = getWindSpeed_kmh();
331 out << "Wind:" << tab << tab << tab;
332 if (d < .1)
333 out << "none" << "\n";
334 else {
335 if ((i = getWindDir()) == -1)
336 out << "from variable directions";
337 else
338 out << "from the " << azimuthName(i) << " (" << i << " deg)";
339 out << " at " << rnd(d, -1) << " km/h";
340
341 out << tab << tab << rnd(getWindSpeed_kt(), -1) << " kt";
342 out << " = " << rnd(getWindSpeed_mph(), -1) << " mph";
343 out << " = " << rnd(getWindSpeed_mps(), -1) << " m/s";
344 out << "\n";
345
346 d = getGustSpeed_kmh();
347 if (d != NaN && d != 0) {
348 out << tab << tab << tab << "with gusts at " << rnd(d, -1) << " km/h";
349 out << tab << tab << tab << rnd(getGustSpeed_kt(), -1) << " kt";
350 out << " = " << rnd(getGustSpeed_mph(), -1) << " mph";
351 out << " = " << rnd(getGustSpeed_mps(), -1) << " m/s";
352 out << "\n";
353 }
354
355 int from = getWindRangeFrom();
356 int to = getWindRangeTo();
357 if (from != to) {
358 out << tab << tab << tab << "variable from " << azimuthName(from);
359 out << " to " << azimuthName(to);
360 out << " (" << from << "deg --" << to << " deg)" << "\n";
361 }
362 }
363
364
365 // temperature/humidity/air pressure
366 if ((d = getTemperature_C()) != NaN) {
367 out << "Temperature:" << tab << tab << d << " C" << tab << tab << tab << tab << tab;
368 out << rnd(getTemperature_F(), -1) << " F" << "\n";
369
370 if ((d = getDewpoint_C()) != NaN) {
371 out << "Dewpoint:" << tab << tab << d << " C" << tab << tab << tab << tab << tab;
372 out << rnd(getDewpoint_F(), -1) << " F" << "\n";
373 out << "Rel. Humidity: " << tab << tab << rnd(getRelHumidity()) << " %" << "\n";
374 }
375 }
376 if ((d = getPressure_hPa()) != NaN) {
377 out << "Pressure:" << tab << tab << rnd(d) << " hPa" << tab << tab << tab << tab;
378 out << rnd(getPressure_inHg(), -2) << " in. Hg" << "\n";
379 }
380
381
382 // weather phenomena
383 vector<string> wv = getWeather();
384 vector<string>::iterator weather;
385 for (i = 0, weather = wv.begin(); weather != wv.end(); weather++, i++) {
386 out << (i ? ", " : "Weather:") << tab << tab << weather->c_str();
387 }
388 if (i)
389 out << "\n";
390
391
392 // cloud layers
393 const char *coverage_string[5] = {
394 "clear skies", "few clouds", "scattered clouds", "broken clouds", "sky overcast"
395 };
396 vector<SGMetarCloud> cv = getClouds();
397 vector<SGMetarCloud>::iterator cloud;
398 for (lineno = 0, cloud = cv.begin(); cloud != cv.end(); cloud++, lineno++) {
399 if (lineno) out << tab << tab << tab;
400 else out << "Sky condition:" << tab << tab;
401
402 if ((i = cloud->getCoverage()) != -1)
403 out << coverage_string[i];
404 if ((d = cloud->getAltitude_ft()) != NaN)
405 out << " at " << rnd(d, 1) << " ft";
406 if ((s = cloud->getTypeLongString()))
407 out << " (" << s << ')';
408 if (d != NaN)
409 out << tab << tab << tab << rnd(cloud->getAltitude_m(), 1) << " m";
410 out << "\n";
411 }
412
413
414 // runways
415 map<string, SGMetarRunway> rm = getRunways();
416 map<string, SGMetarRunway>::iterator runway;
417 for (runway = rm.begin(); runway != rm.end(); runway++) {
418 lineno = 0;
419 if (!strcmp(runway->first.c_str(), "ALL"))
420 out << "All runways:" << tab << tab;
421 else
422 out << "Runway " << runway->first << ":" << tab << tab;
423 SGMetarRunway rwy = runway->second;
424
425 // assemble surface string
426 vector<string> surface;
427 if ((s = rwy.getDepositString()) && strlen(s))
428 surface.push_back(s);
429 if ((s = rwy.getExtentString()) && strlen(s))
430 surface.push_back(s);
431 if ((d = rwy.getDepth()) != NaN) {
432 sprintf(buf, "%.1lf mm", d * 1000.0);
433 surface.push_back(buf);
434 }
435 if ((s = rwy.getFrictionString()) && strlen(s))
436 surface.push_back(s);
437 if ((d = rwy.getFriction()) != NaN) {
438 sprintf(buf, "friction: %.2lf", d);
439 surface.push_back(buf);
440 }
441
442 if (! surface.empty()) {
443 vector<string>::iterator rwysurf = surface.begin();
444 for (i = 0; rwysurf != surface.end(); rwysurf++, i++) {
445 if (i)
446 out << ", ";
447 out << *rwysurf;
448 }
449 lineno++;
450 }
451
452 // assemble visibility string
453 SGMetarVisibility minvis = rwy.getMinVisibility();
454 SGMetarVisibility maxvis = rwy.getMaxVisibility();
455 if ((d = minvis.getVisibility_m()) != NaN) {
456 if (lineno++)
457 out << "\n" << tab << tab << tab;
458 out << SGMetarVisibilityManip(minvis, tab);
459 }
460 if (maxvis.getVisibility_m() != d) {
461 out << "\n" << tab << tab << tab << SGMetarVisibilityManip(maxvis, tab) << "\n";
462 lineno++;
463 }
464
465 if (rwy.getWindShear()) {
466 if (lineno++)
467 out << "\n" << tab << tab << tab;
468 out << "critical wind shear" << "\n";
469 }
470 out << "\n";
471 }
472 out << "\n";
473 return out.str();
474 }
475
useCurrentDate()476 void SGMetar::useCurrentDate()
477 {
478 struct tm now;
479 time_t now_sec = time(0);
480 #ifdef _WIN32
481 now = *gmtime(&now_sec);
482 #else
483 gmtime_r(&now_sec, &now);
484 #endif
485 _year = now.tm_year + 1900;
486 _month = now.tm_mon + 1;
487 }
488
489 /**
490 * Replace any number of subsequent spaces by just one space, and add
491 * a trailing space. This makes scanning for things like "ALL RWY" easier.
492 */
normalizeData()493 void SGMetar::normalizeData()
494 {
495 char *src, *dest;
496 for (src = dest = _data; (*dest++ = *src++); )
497 while (*src == ' ' && src[1] == ' ')
498 src++;
499 for (dest--; isspace(*--dest); ) ;
500 *++dest = ' ';
501 *++dest = '\0';
502 }
503
504
505 // \d\d\d\d/\d\d/\d\d
scanPreambleDate()506 bool SGMetar::scanPreambleDate()
507 {
508 char *m = _m;
509 int year, month, day;
510 if (!scanNumber(&m, &year, 4))
511 return false;
512 if (*m++ != '/')
513 return false;
514 if (!scanNumber(&m, &month, 2))
515 return false;
516 if (*m++ != '/')
517 return false;
518 if (!scanNumber(&m, &day, 2))
519 return false;
520 if (!scanBoundary(&m))
521 return false;
522 _year = year;
523 _month = month;
524 _day = day;
525 _m = m;
526 return true;
527 }
528
529
530 // \d\d:\d\d
scanPreambleTime()531 bool SGMetar::scanPreambleTime()
532 {
533 char *m = _m;
534 int hour, minute;
535 if (!scanNumber(&m, &hour, 2))
536 return false;
537 if (*m++ != ':')
538 return false;
539 if (!scanNumber(&m, &minute, 2))
540 return false;
541 if (!scanBoundary(&m))
542 return false;
543 _hour = hour;
544 _minute = minute;
545 _m = m;
546 return true;
547 }
548
549
550 // (METAR|SPECI)
scanType()551 bool SGMetar::scanType()
552 {
553 if (strncmp(_m, "METAR ", 6) && strncmp(_m, "SPECI ", 6))
554 return false;
555 _m += 6;
556 _grpcount++;
557 return true;
558 }
559
560
561 // [A-Z]{4}
scanId()562 bool SGMetar::scanId()
563 {
564 char *m = _m;
565 for (int i = 0; i < 4; m++, i++)
566 if (!(isalpha(*m) || isdigit(*m)))
567 return false;
568 if (!scanBoundary(&m))
569 return false;
570 strncpy(_icao, _m, 4);
571 _icao[4] = '\0';
572 _m = m;
573 _grpcount++;
574 return true;
575 }
576
577
578 // \d{6}Z
scanDate()579 bool SGMetar::scanDate()
580 {
581 char *m = _m;
582 int day, hour, minute;
583 if (!scanNumber(&m, &day, 2))
584 return false;
585 if (!scanNumber(&m, &hour, 2))
586 return false;
587 if (!scanNumber(&m, &minute, 2))
588 return false;
589 if (*m++ != 'Z')
590 return false;
591 if (!scanBoundary(&m))
592 return false;
593 _day = day;
594 _hour = hour;
595 _minute = minute;
596 _m = m;
597 _grpcount++;
598 return true;
599 }
600
601
602 // (NIL|AUTO|COR|RTD)
scanModifier()603 bool SGMetar::scanModifier()
604 {
605 char *m = _m;
606 int type;
607 if (!strncmp(m, "NIL", 3)) {
608 _m += strlen(_m);
609 return true;
610 }
611 if (!strncmp(m, "AUTO", 4)) // automatically generated
612 m += 4, type = AUTO;
613 else if (!strncmp(m, "COR", 3)) // manually corrected
614 m += 3, type = COR;
615 else if (!strncmp(m, "RTD", 3)) // routine delayed
616 m += 3, type = RTD;
617 else
618 return false;
619 if (!scanBoundary(&m))
620 return false;
621 _report_type = type;
622 _m = m;
623 _grpcount++;
624 return true;
625 }
626
627
628 // (\d{3}|VRB)\d{1,3}(G\d{2,3})?(KT|KMH|MPS)
scanWind()629 bool SGMetar::scanWind()
630 {
631 char *m = _m;
632 int dir;
633 if (!strncmp(m, "VRB", 3))
634 m += 3, dir = -1;
635 else if (!strncmp(m, "///", 3)) // direction not measurable
636 m += 3, dir = -1;
637 else if (!scanNumber(&m, &dir, 3))
638 return false;
639
640 int i;
641 if (!strncmp(m, "//", 2)) // speed not measurable
642 m += 2, i = -1;
643 else if (!scanNumber(&m, &i, 2, 3))
644 return false;
645 double speed = i;
646
647 double gust = NaN;
648 if (*m == 'G') {
649 m++;
650 if (!strncmp(m, "//", 2)) // speed not measurable
651 m += 2, i = -1;
652 else if (!scanNumber(&m, &i, 2, 3))
653 return false;
654
655 if (i != -1)
656 gust = i;
657 }
658
659 double factor;
660 if (!strncmp(m, "KT", 2))
661 m += 2, factor = SG_KT_TO_MPS;
662 else if (!strncmp(m, "KMH", 3)) // invalid Km/h
663 m += 3, factor = SG_KMH_TO_MPS;
664 else if (!strncmp(m, "KPH", 3)) // invalid Km/h
665 m += 3, factor = SG_KMH_TO_MPS;
666 else if (!strncmp(m, "MPS", 3))
667 m += 3, factor = 1.0;
668 else if (!strncmp(m, " ", 1)) // default to Knots
669 factor = SG_KT_TO_MPS;
670 else
671 return false;
672 if (!scanBoundary(&m))
673 return false;
674 _m = m;
675 _wind_dir = dir;
676 _wind_speed = speed * factor;
677 if (gust != NaN)
678 _gust_speed = gust * factor;
679 _grpcount++;
680 return true;
681 }
682
683
684 // \d{3}V\d{3}
scanVariability()685 bool SGMetar::scanVariability()
686 {
687 char *m = _m;
688 int from, to;
689
690 if (!strncmp(m, "///", 3)) // direction not measurable
691 m += 3, from = -1;
692 else if (!scanNumber(&m, &from, 3))
693 return false;
694
695 if (*m++ != 'V')
696 return false;
697
698 if (!strncmp(m, "///", 3)) // direction not measurable
699 m += 3, to = -1;
700 else if (!scanNumber(&m, &to, 3))
701 return false;
702
703 if (!scanBoundary(&m))
704 return false;
705
706 _m = m;
707 _wind_range_from = from;
708 _wind_range_to = to;
709 _grpcount++;
710
711 return true;
712 }
713
714
scanVisibility()715 bool SGMetar::scanVisibility()
716 // TODO: if only directed vis are given, do still set min/max
717 {
718 if (!strncmp(_m, "//// ", 5)) { // spec compliant?
719 _m += 5;
720 _grpcount++;
721 return true;
722 }
723
724 char *m = _m;
725 double distance;
726 int i, dir = -1;
727 int modifier = SGMetarVisibility::EQUALS;
728 // \d{4}(N|NE|E|SE|S|SW|W|NW)?
729 if (scanNumber(&m, &i, 4)) {
730 if( strncmp( m, "NDV",3 ) == 0 ) {
731 m+=3; // tolerate NDV (no directional validation)
732 } else if (*m == 'E') {
733 m++, dir = 90;
734 } else if (*m == 'W') {
735 m++, dir = 270;
736 } else if (*m == 'N') {
737 m++;
738 if (*m == 'E')
739 m++, dir = 45;
740 else if (*m == 'W')
741 m++, dir = 315;
742 else
743 dir = 0;
744 } else if (*m == 'S') {
745 m++;
746 if (*m == 'E')
747 m++, dir = 135;
748 else if (*m == 'W')
749 m++, dir = 225;
750 else
751 dir = 180;
752 }
753 if (i == 0)
754 i = 50, modifier = SGMetarVisibility::LESS_THAN;
755 else if (i == 9999)
756 i++, modifier = SGMetarVisibility::GREATER_THAN;
757 distance = i;
758 } else {
759 // M?(\d{1,2}|\d{1,2}/\d{1,2}|\d{1,2} \d{1,2}/\d{1,2})(SM|KM)
760 if (*m == 'M')
761 m++, modifier = SGMetarVisibility::LESS_THAN;
762
763 if (!scanNumber(&m, &i, 1, 2))
764 return false;
765 distance = i;
766
767 if (*m == '/') {
768 m++;
769 if (!scanNumber(&m, &i, 1, 2))
770 return false;
771 distance /= i;
772 } else if (*m == ' ') {
773 m++;
774 int denom;
775 if (!scanNumber(&m, &i, 1, 2))
776 return false;
777 if (*m++ != '/')
778 return false;
779 if (!scanNumber(&m, &denom, 1, 2))
780 return false;
781 distance += (double)i / denom;
782 }
783
784 if (!strncmp(m, "SM", 2))
785 distance *= SG_SM_TO_METER, m += 2;
786 else if (!strncmp(m, "KM", 2))
787 distance *= 1000, m += 2;
788 else
789 return false;
790 }
791 if (!scanBoundary(&m))
792 return false;
793
794 SGMetarVisibility *v;
795 if (dir != -1)
796 v = &_dir_visibility[dir / 45];
797 else if (_min_visibility._distance == NaN)
798 v = &_min_visibility;
799 else
800 v = &_max_visibility;
801
802 v->_distance = distance;
803 v->_modifier = modifier;
804 v->_direction = dir;
805 _m = m;
806 _grpcount++;
807 return true;
808 }
809
810
811 // R\d\d[LCR]?/([PM]?\d{4}V)?[PM]?\d{4}(FT)?[DNU]?
scanRwyVisRange()812 bool SGMetar::scanRwyVisRange()
813 {
814 char *m = _m;
815 int i;
816 SGMetarRunway r;
817
818 if (*m++ != 'R')
819 return false;
820 if (!scanNumber(&m, &i, 2))
821 return false;
822 if (*m == 'L' || *m == 'C' || *m == 'R')
823 m++;
824
825 char id[4];
826 strncpy(id, _m + 1, i = m - _m - 1);
827 id[i] = '\0';
828
829 if (*m++ != '/')
830 return false;
831
832 int from, to;
833 if (*m == 'P')
834 m++, r._min_visibility._modifier = SGMetarVisibility::GREATER_THAN;
835 else if (*m == 'M')
836 m++, r._min_visibility._modifier = SGMetarVisibility::LESS_THAN;
837 if (!scanNumber(&m, &from, 4))
838 return false;
839 if (*m == 'V') {
840 m++;
841 if (*m == 'P')
842 m++, r._max_visibility._modifier = SGMetarVisibility::GREATER_THAN;
843 else if (*m == 'M')
844 m++, r._max_visibility._modifier = SGMetarVisibility::LESS_THAN;
845 if (!scanNumber(&m, &to, 4))
846 return false;
847 } else
848 to = from;
849
850 if (!strncmp(m, "FT", 2)) {
851 from = int(from * SG_FEET_TO_METER);
852 to = int(to * SG_FEET_TO_METER);
853 m += 2;
854 }
855 r._min_visibility._distance = from;
856 r._max_visibility._distance = to;
857
858 if (*m == '/') // this is not in the spec!
859 m++;
860 if (*m == 'D')
861 m++, r._min_visibility._tendency = SGMetarVisibility::DECREASING;
862 else if (*m == 'N')
863 m++, r._min_visibility._tendency = SGMetarVisibility::STABLE;
864 else if (*m == 'U')
865 m++, r._min_visibility._tendency = SGMetarVisibility::INCREASING;
866
867 if (!scanBoundary(&m))
868 return false;
869 _m = m;
870
871 _runways[id]._min_visibility = r._min_visibility;
872 _runways[id]._max_visibility = r._max_visibility;
873 _grpcount++;
874 return true;
875 }
876
877
878 static const struct Token special[] = {
879 { "NSW", "no significant weather" },
880 /* { "VCSH", "showers in the vicinity" },
881 { "VCTS", "thunderstorm in the vicinity" }, */
882 { 0, 0 }
883 };
884
885
886 static const struct Token description[] = {
887 { "SH", "showers of" },
888 { "TS", "thunderstorm with" },
889 { "BC", "patches of" },
890 { "BL", "blowing" },
891 { "DR", "low drifting" },
892 { "FZ", "freezing" },
893 { "MI", "shallow" },
894 { "PR", "partial" },
895 { 0, 0 }
896 };
897
898
899 static const struct Token phenomenon[] = {
900 { "DZ", "drizzle" },
901 { "GR", "hail" },
902 { "GS", "small hail and/or snow pellets" },
903 { "IC", "ice crystals" },
904 { "PE", "ice pellets" },
905 { "RA", "rain" },
906 { "SG", "snow grains" },
907 { "SN", "snow" },
908 { "UP", "unknown precipitation" },
909 { "BR", "mist" },
910 { "DU", "widespread dust" },
911 { "FG", "fog" },
912 { "FGBR", "fog bank" },
913 { "FU", "smoke" },
914 { "HZ", "haze" },
915 { "PY", "spray" },
916 { "SA", "sand" },
917 { "VA", "volcanic ash" },
918 { "DS", "duststorm" },
919 { "FC", "funnel cloud/tornado waterspout" },
920 { "PO", "well-developed dust/sand whirls" },
921 { "SQ", "squalls" },
922 { "SS", "sandstorm" },
923 { "UP", "unknown" }, // ... due to failed automatic acquisition
924 { 0, 0 }
925 };
926
927
928 // (+|-|VC)?(NSW|MI|PR|BC|DR|BL|SH|TS|FZ)?((DZ|RA|SN|SG|IC|PE|GR|GS|UP){0,3})(BR|FG|FU|VA|DU|SA|HZ|PY|PO|SQ|FC|SS|DS){0,3}
scanWeather()929 bool SGMetar::scanWeather()
930 {
931 char *m = _m;
932 string weather;
933 const struct Token *a;
934
935 // @see WMO-49 Section 4.4.2.9
936 // Denotes a temporary failure of the sensor
937 if (!strncmp(m, "// ", 3)) {
938 _m += 3;
939 _grpcount++;
940 return false;
941 }
942
943 if ((a = scanToken(&m, special))) {
944 if (!scanBoundary(&m))
945 return false;
946 _weather.push_back(a->text);
947 _m = m;
948 return true;
949 }
950
951 string pre, post;
952 struct Weather w;
953 if (*m == '-')
954 m++, pre = "light ", w.intensity = LIGHT;
955 else if (*m == '+')
956 m++, pre = "heavy ", w.intensity = HEAVY;
957 else if (!strncmp(m, "VC", 2))
958 m += 2, post = "in the vicinity ", w.vincinity=true;
959 else
960 pre = "moderate ", w.intensity = MODERATE;
961
962 int i;
963 for (i = 0; i < 3; i++) {
964 if (!(a = scanToken(&m, description)))
965 break;
966 w.descriptions.push_back(a->id);
967 weather += string(a->text) + " ";
968 }
969
970 for (i = 0; i < 3; i++) {
971 if (!(a = scanToken(&m, phenomenon)))
972 break;
973 w.phenomena.push_back(a->id);
974 weather += string(a->text) + " ";
975 if (!strcmp(a->id, "RA"))
976 _rain = w.intensity;
977 else if (!strcmp(a->id, "DZ"))
978 _rain = LIGHT;
979 else if (!strcmp(a->id, "HA"))
980 _hail = w.intensity;
981 else if (!strcmp(a->id, "SN"))
982 _snow = w.intensity;
983 }
984 if (!weather.length())
985 return false;
986 if (!scanBoundary(&m))
987 return false;
988 _m = m;
989 weather = pre + weather + post;
990 weather.erase(weather.length() - 1);
991 _weather.push_back(weather);
992 if( ! w.phenomena.empty() ) {
993 _weather2.push_back( w );
994 }
995 _grpcount++;
996 return true;
997 }
998
999
1000 static const struct Token cloud_types[] = {
1001 { "AC", "altocumulus" },
1002 { "ACC", "altocumulus castellanus" },
1003 { "ACSL", "altocumulus standing lenticular" },
1004 { "AS", "altostratus" },
1005 { "CB", "cumulonimbus" },
1006 { "CBMAM", "cumulonimbus mammatus" },
1007 { "CC", "cirrocumulus" },
1008 { "CCSL", "cirrocumulus standing lenticular" },
1009 { "CI", "cirrus" },
1010 { "CS", "cirrostratus" },
1011 { "CU", "cumulus" },
1012 { "CUFRA", "cumulus fractus" },
1013 { "NS", "nimbostratus" },
1014 { "SAC", "stratoaltocumulus" }, // guessed
1015 { "SC", "stratocumulus" },
1016 { "SCSL", "stratocumulus standing lenticular" },
1017 { "ST", "stratus" },
1018 { "STFRA", "stratus fractus" },
1019 { "TCU", "towering cumulus" },
1020 { 0, 0 }
1021 };
1022
1023 #include <iostream>
1024 // (FEW|SCT|BKN|OVC|SKC|CLR|CAVOK|VV)([0-9]{3}|///)?[:cloud_type:]?
scanSkyCondition()1025 bool SGMetar::scanSkyCondition()
1026 {
1027 char *m = _m;
1028 int i;
1029 SGMetarCloud cl;
1030
1031 if (!strncmp(m, "//////", 6)) {
1032 m += 6;
1033 if (!scanBoundary(&m))
1034 return false;
1035 _m = m;
1036 return true;
1037 }
1038
1039 if (!strncmp(m, "CLR", i = 3) // clear
1040 || !strncmp(m, "SKC", i = 3) // sky clear
1041 || !strncmp(m, "NCD", i = 3) // nil cloud detected
1042 || !strncmp(m, "NSC", i = 3) // no significant clouds
1043 || !strncmp(m, "CAVOK", i = 5)) { // ceiling and visibility OK (implies 9999)
1044 m += i;
1045 if (!scanBoundary(&m))
1046 return false;
1047
1048 if (i == 3) {
1049 cl._coverage = SGMetarCloud::COVERAGE_CLEAR;
1050 _clouds.push_back(cl);
1051 } else {
1052 _cavok = true;
1053 }
1054 _m = m;
1055 return true;
1056 }
1057
1058 if (!strncmp(m, "VV", i = 2)) // vertical visibility
1059 ;
1060 else if (!strncmp(m, "FEW", i = 3))
1061 cl._coverage = SGMetarCloud::COVERAGE_FEW;
1062 else if (!strncmp(m, "SCT", i = 3))
1063 cl._coverage = SGMetarCloud::COVERAGE_SCATTERED;
1064 else if (!strncmp(m, "BKN", i = 3))
1065 cl._coverage = SGMetarCloud::COVERAGE_BROKEN;
1066 else if (!strncmp(m, "OVC", i = 3))
1067 cl._coverage = SGMetarCloud::COVERAGE_OVERCAST;
1068 else
1069 return false;
1070 m += i;
1071
1072 if (!strncmp(m, "///", 3)) { // vis not measurable (e.g. because of heavy snowing)
1073 m += 3, i = -1;
1074 sg_srandom_time();
1075 // randomize the base height to avoid the black sky issue
1076 i = 50 + static_cast<int>(sg_random() * 250.0); // range [5,000, 30,000]
1077 } else if (scanBoundary(&m)) {
1078 _m = m;
1079 return true; // ignore single OVC/BKN/...
1080 } else if (!scanNumber(&m, &i, 3))
1081 i = -1;
1082
1083 if (cl._coverage == SGMetarCloud::COVERAGE_NIL) {
1084 if (!scanBoundary(&m))
1085 return false;
1086 if (i == -1) // 'VV///'
1087 _vert_visibility._modifier = SGMetarVisibility::NOGO;
1088 else
1089 _vert_visibility._distance = i * 100 * SG_FEET_TO_METER;
1090 _m = m;
1091 return true;
1092 }
1093
1094 if (i != -1)
1095 cl._altitude = i * 100 * SG_FEET_TO_METER;
1096
1097 const struct Token *a;
1098 if ((a = scanToken(&m, cloud_types))) {
1099 cl._type = a->id;
1100 cl._type_long = a->text;
1101 }
1102
1103 // @see WMO-49 Section 4.5.4.5
1104 // Denotes temporary failure of sensor and covers cases like FEW045///
1105 if (!strncmp(m, "///", 3))
1106 m += 3;
1107 if (!scanBoundary(&m))
1108 return false;
1109 _clouds.push_back(cl);
1110
1111 _m = m;
1112 _grpcount++;
1113 return true;
1114 }
1115
1116
1117 // M?[0-9]{2}/(M?[0-9]{2})? (spec)
1118 // (M?[0-9]{2}|XX)/(M?[0-9]{2}|XX)? (Namibia)
scanTemperature()1119 bool SGMetar::scanTemperature()
1120 {
1121 char *m = _m;
1122 int sign = 1, temp, dew;
1123 if (!strncmp(m, "XX/XX", 5)) { // not spec compliant!
1124 _m += 5;
1125 return scanBoundary(&_m);
1126 }
1127
1128 if (*m == 'M')
1129 m++, sign = -1;
1130 if (!scanNumber(&m, &temp, 2))
1131 return false;
1132 temp *= sign;
1133
1134 if (*m++ != '/')
1135 return false;
1136 if (!scanBoundary(&m)) {
1137 if (!strncmp(m, "XX", 2)) // not spec compliant!
1138 m += 2, sign = 0, dew = temp;
1139 else {
1140 sign = 1;
1141 if (*m == 'M')
1142 m++, sign = -1;
1143 if (!scanNumber(&m, &dew, 2))
1144 return false;
1145 }
1146 if (!scanBoundary(&m))
1147 return false;
1148 if (sign)
1149 _dewp = sign * dew;
1150 }
1151 _temp = temp;
1152 _m = m;
1153 _grpcount++;
1154 return true;
1155 }
1156
1157
getRelHumidity() const1158 double SGMetar::getRelHumidity() const
1159 {
1160 if (_temp == NaN || _dewp == NaN)
1161 return NaN;
1162 double dewp = pow(10.0, 7.5 * _dewp / (237.7 + _dewp));
1163 double temp = pow(10.0, 7.5 * _temp / (237.7 + _temp));
1164 return dewp * 100 / temp;
1165 }
1166
1167
1168 // [AQ]\d{4} (spec)
1169 // [AQ]\d{2}(\d{2}|//) (Namibia)
scanPressure()1170 bool SGMetar::scanPressure()
1171 {
1172 char *m = _m;
1173 double factor;
1174 int press, i;
1175
1176 if (*m == 'A')
1177 factor = SG_INHG_TO_PA / 100;
1178 else if (*m == 'Q')
1179 factor = 100;
1180 else
1181 return false;
1182 m++;
1183 if (!scanNumber(&m, &press, 2))
1184 return false;
1185 press *= 100;
1186 if (!strncmp(m, "//", 2)) // not spec compliant!
1187 m += 2;
1188 else if (scanNumber(&m, &i, 2))
1189 press += i;
1190 else
1191 return false;
1192 if (!scanBoundary(&m))
1193 return false;
1194 _pressure = press * factor;
1195 _m = m;
1196 _grpcount++;
1197 return true;
1198 }
1199
1200
1201 static const char *runway_deposit[] = {
1202 "clear and dry",
1203 "damp",
1204 "wet or puddles",
1205 "frost",
1206 "dry snow",
1207 "wet snow",
1208 "slush",
1209 "ice",
1210 "compacted snow",
1211 "frozen ridges"
1212 };
1213
1214
1215 static const char *runway_deposit_extent[] = {
1216 0, "1-10%", "11-25%", 0, 0, "26-50%", 0, 0, 0, "51-100%"
1217 };
1218
1219
1220 static const char *runway_friction[] = {
1221 0,
1222 "poor braking action",
1223 "poor/medium braking action",
1224 "medium braking action",
1225 "medium/good braking action",
1226 "good braking action",
1227 0, 0, 0,
1228 "friction: unreliable measurement"
1229 };
1230
1231
1232 // \d\d(CLRD|[\d/]{4})(\d\d|//)
scanRunwayReport()1233 bool SGMetar::scanRunwayReport()
1234 {
1235 char *m = _m;
1236 int i;
1237 char id[4];
1238 SGMetarRunway r;
1239
1240 if (!scanNumber(&m, &i, 2))
1241 return false;
1242 if (i == 88)
1243 strcpy(id, "ALL");
1244 else if (i == 99)
1245 strcpy(id, "REP"); // repetition of previous report
1246 else if (i >= 50) {
1247 i -= 50;
1248 id[0] = i / 10 + '0', id[1] = i % 10 + '0', id[2] = 'R', id[3] = '\0';
1249 } else
1250 id[0] = i / 10 + '0', id[1] = i % 10 + '0', id[2] = '\0';
1251
1252 if (!strncmp(m, "CLRD", 4)) {
1253 m += 4; // runway cleared
1254 r._deposit_string = "cleared";
1255 } else {
1256 if (scanNumber(&m, &i, 1)) {
1257 r._deposit = i;
1258 r._deposit_string = runway_deposit[i];
1259 } else if (*m == '/')
1260 m++;
1261 else
1262 return false;
1263
1264 if (*m == '1' || *m == '2' || *m == '5' || *m == '9') { // extent of deposit
1265 r._extent = *m - '0';
1266 r._extent_string = runway_deposit_extent[*m - '0'];
1267 } else if (*m != '/')
1268 return false;
1269
1270 m++;
1271 i = -1;
1272 if (!strncmp(m, "//", 2))
1273 m += 2;
1274 else if (!scanNumber(&m, &i, 2))
1275 return false;
1276
1277 if (i == 0)
1278 r._depth = 0.0005; // < 1 mm deep (let's say 0.5 :-)
1279 else if (i > 0 && i <= 90)
1280 r._depth = i / 1000.0; // i mm deep
1281 else if (i >= 92 && i <= 98)
1282 r._depth = (i - 90) / 20.0;
1283 else if (i == 99)
1284 r._comment = "runway not in use";
1285 else if (i == -1) // no depth given ("//")
1286 ;
1287 else
1288 return false;
1289 }
1290 i = -1;
1291 if (m[0] == '/' && m[1] == '/')
1292 m += 2;
1293 else if (!scanNumber(&m, &i, 2))
1294 return false;
1295 if (i >= 1 && i < 90) {
1296 r._friction = i / 100.0;
1297 } else if ((i >= 91 && i <= 95) || i == 99) {
1298 r._friction_string = runway_friction[i - 90];
1299 }
1300 if (!scanBoundary(&m))
1301 return false;
1302
1303 _runways[id]._deposit = r._deposit;
1304 _runways[id]._deposit_string = r._deposit_string;
1305 _runways[id]._extent = r._extent;
1306 _runways[id]._extent_string = r._extent_string;
1307 _runways[id]._depth = r._depth;
1308 _runways[id]._friction = r._friction;
1309 _runways[id]._friction_string = r._friction_string;
1310 _runways[id]._comment = r._comment;
1311 _m = m;
1312 _grpcount++;
1313 return true;
1314 }
1315
1316
1317 // WS (ALL RWYS?|RWY ?\d\d[LCR]?)?
scanWindShear()1318 bool SGMetar::scanWindShear()
1319 {
1320 char *m = _m;
1321 if (strncmp(m, "WS", 2))
1322 return false;
1323 m += 2;
1324 if (!scanBoundary(&m))
1325 return false;
1326
1327 if (!strncmp(m, "ALL", 3)) {
1328 m += 3;
1329 if (!scanBoundary(&m))
1330 return false;
1331 if (strncmp(m, "RWY", 3))
1332 return false;
1333 m += 3;
1334 if (*m == 'S')
1335 m++;
1336 if (!scanBoundary(&m))
1337 return false;
1338 _runways["ALL"]._wind_shear = true;
1339 _m = m;
1340 return true;
1341 }
1342
1343 char id[4], *mm;
1344 int i, cnt;
1345 for (cnt = 0;; cnt++) { // ??
1346 if (strncmp(m, "RWY", 3))
1347 break;
1348 m += 3;
1349 scanBoundary(&m);
1350 mm = m;
1351 if (!scanNumber(&m, &i, 2))
1352 return false;
1353 if (*m == 'L' || *m == 'C' || *m == 'R')
1354 m++;
1355 strncpy(id, mm, i = m - mm);
1356 id[i] = '\0';
1357 if (!scanBoundary(&m))
1358 return false;
1359 _runways[id]._wind_shear = true;
1360 }
1361 if (!cnt)
1362 _runways["ALL"]._wind_shear = true;
1363 _m = m;
1364 return true;
1365 }
1366
1367
scanTrendForecast()1368 bool SGMetar::scanTrendForecast()
1369 {
1370 char *m = _m;
1371 if (strncmp(m, "NOSIG", 5))
1372 return false;
1373
1374 m += 5;
1375 if (!scanBoundary(&m))
1376 return false;
1377 _m = m;
1378 return true;
1379 }
1380
1381
1382 // (BLU|WHT|GRN|YLO|AMB|RED)
1383 static const struct Token colors[] = {
1384 { "BLU", "Blue" }, // 2500 ft, 8.0 km
1385 { "WHT", "White" }, // 1500 ft, 5.0 km
1386 { "GRN", "Green" }, // 700 ft, 3.7 km
1387 { "YLO", "Yellow" }, // 300 ft, 1.6 km
1388 { "AMB", "Amber" }, // 200 ft, 0.8 km
1389 { "RED", "Red" }, // <200 ft, <0.8 km
1390 { 0, 0 }
1391 };
1392
1393
scanColorState()1394 bool SGMetar::scanColorState()
1395 {
1396 char *m = _m;
1397 const struct Token *a;
1398 if (!(a = scanToken(&m, colors)))
1399 return false;
1400 if (!scanBoundary(&m))
1401 return false;
1402 //printf(Y"Code %s\n"N, a->text);
1403 _m = m;
1404 return true;
1405 }
1406
1407
scanRemark()1408 bool SGMetar::scanRemark()
1409 {
1410 if (strncmp(_m, "RMK", 3))
1411 return false;
1412 _m += 3;
1413 if (!scanBoundary(&_m))
1414 return false;
1415
1416 while (*_m) {
1417 if (!scanRunwayReport()) {
1418 while (*_m && !isspace(*_m))
1419 _m++;
1420 scanBoundary(&_m);
1421 }
1422 }
1423 return true;
1424 }
1425
1426
scanRemainder()1427 bool SGMetar::scanRemainder()
1428 {
1429 char *m = _m;
1430 if (!(strncmp(m, "NOSIG", 5))) {
1431 m += 5;
1432 if (scanBoundary(&m))
1433 _m = m; //_comment.push_back("No significant tendency");
1434 }
1435
1436 if (!scanBoundary(&m))
1437 return false;
1438 _m = m;
1439 return true;
1440 }
1441
1442
scanBoundary(char ** s)1443 bool SGMetar::scanBoundary(char **s)
1444 {
1445 if (**s && !isspace(**s))
1446 return false;
1447 while (isspace(**s))
1448 (*s)++;
1449 return true;
1450 }
1451
1452
scanNumber(char ** src,int * num,int min,int max)1453 int SGMetar::scanNumber(char **src, int *num, int min, int max)
1454 {
1455 int i;
1456 char *s = *src;
1457 *num = 0;
1458 for (i = 0; i < min; i++) {
1459 if (!isdigit(*s))
1460 return 0;
1461 else
1462 *num = *num * 10 + *s++ - '0';
1463 }
1464 for (; i < max && isdigit(*s); i++)
1465 *num = *num * 10 + *s++ - '0';
1466 *src = s;
1467 return i;
1468 }
1469
1470
1471 // find longest match of str in list
scanToken(char ** str,const struct Token * list)1472 const struct Token *SGMetar::scanToken(char **str, const struct Token *list)
1473 {
1474 const struct Token *longest = 0;
1475 int maxlen = 0, len;
1476 const char *s;
1477 for (int i = 0; (s = list[i].id); i++) {
1478 len = strlen(s);
1479 if (!strncmp(s, *str, len) && len > maxlen) {
1480 maxlen = len;
1481 longest = &list[i];
1482 }
1483 }
1484 *str += maxlen;
1485 return longest;
1486 }
1487
1488
set(double alt,Coverage cov)1489 void SGMetarCloud::set(double alt, Coverage cov)
1490 {
1491 _altitude = alt;
1492 if (cov != -1)
1493 _coverage = cov;
1494 }
1495
getCoverage(const std::string & coverage)1496 SGMetarCloud::Coverage SGMetarCloud::getCoverage( const std::string & coverage )
1497 {
1498 if( coverage == "clear" ) return COVERAGE_CLEAR;
1499 if( coverage == "few" ) return COVERAGE_FEW;
1500 if( coverage == "scattered" ) return COVERAGE_SCATTERED;
1501 if( coverage == "broken" ) return COVERAGE_BROKEN;
1502 if( coverage == "overcast" ) return COVERAGE_OVERCAST;
1503 return COVERAGE_NIL;
1504 }
1505
1506 const char * SGMetarCloud::COVERAGE_NIL_STRING = "nil";
1507 const char * SGMetarCloud::COVERAGE_CLEAR_STRING = "clear";
1508 const char * SGMetarCloud::COVERAGE_FEW_STRING = "few";
1509 const char * SGMetarCloud::COVERAGE_SCATTERED_STRING = "scattered";
1510 const char * SGMetarCloud::COVERAGE_BROKEN_STRING = "broken";
1511 const char * SGMetarCloud::COVERAGE_OVERCAST_STRING = "overcast";
1512
set(double dist,int dir,int mod,int tend)1513 void SGMetarVisibility::set(double dist, int dir, int mod, int tend)
1514 {
1515 _distance = dist;
1516 if (dir != -1)
1517 _direction = dir;
1518 if (mod != -1)
1519 _modifier = mod;
1520 if (tend != 1)
1521 _tendency = tend;
1522 }
1523
1524 #undef NaN
1525