Initfile.h 19 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/******************************************************************************
 *
 * AMDiS - Adaptive multidimensional simulations
 *
 * Copyright (C) 2013 Dresden University of Technology. All Rights Reserved.
 * Web: https://fusionforge.zih.tu-dresden.de/projects/amdis
 *
 * Authors: 
 * Simon Vey, Thomas Witkowski, Andreas Naumann, Simon Praetorius, et al.
 *
 * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
 * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 *
 *
 * This file is part of AMDiS
 *
 * See also license.opensource.txt in the distribution.
 * 
 ******************************************************************************/
20

Praetorius, Simon's avatar
Praetorius, Simon committed
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#ifndef INITFILE_H
#define INITFILE_H

#include <fstream>
#include <sstream>
#include <string>
#include <map>
#include <list>
#include <set>
#include <vector>
#include <stdexcept>
#include <iostream>
#include <typeinfo>
#include "FixVec.h"

#include <boost/lexical_cast.hpp>
37
38
#include <boost/numeric/conversion/cast.hpp> 

39
#include <boost/type_traits.hpp>
Praetorius, Simon's avatar
Praetorius, Simon committed
40

41
42
// a parser for arithmetic expressions
#include "muParser.h"
Praetorius, Simon's avatar
Praetorius, Simon committed
43

44
45
namespace AMDiS {

46
  namespace detail {
47
  
48
49
    // Exceptions
    //_________________________________________________________________________________________
Praetorius, Simon's avatar
Praetorius, Simon committed
50
    
51
    /// Exception for wrong vector size when parsing for concrete vector type
Praetorius, Simon's avatar
Praetorius, Simon committed
52
    struct WrongVectorSize : std::runtime_error {
Praetorius, Simon's avatar
Praetorius, Simon committed
53
      WrongVectorSize(std::string m) : std::runtime_error(m) {}
54
55
    };

56

57
    /// Exception for no delimiter found in vector
Praetorius, Simon's avatar
Praetorius, Simon committed
58
    struct NoDelim : std::runtime_error {
Praetorius, Simon's avatar
Praetorius, Simon committed
59
      NoDelim(std::string m) : std::runtime_error(m) {}
60
61
    };

62

63
    /// Exception for begin and end brackets are different in vector
Praetorius, Simon's avatar
Praetorius, Simon committed
64
    struct WrongVectorFormat : std::runtime_error {
Praetorius, Simon's avatar
Praetorius, Simon committed
65
      WrongVectorFormat(std::string m) : std::runtime_error(m) {}
66
    };
67
    
Praetorius, Simon's avatar
Praetorius, Simon committed
68
    
69
    /// Exception for wrong value format
70
    template<typename T>
71
72
    struct WrongValueFormat : std::runtime_error 
    {  
Praetorius, Simon's avatar
Praetorius, Simon committed
73
74
75
76
77
      static std::string name(bool) { return "bool"; }
      static std::string name(double) { return "double"; }
      static std::string name(float) { return "float"; }
      static std::string name(int) { return "int"; }
      static std::string name(unsigned int) { return "unsigned int"; }
78
79
80
81

      template<typename G>
      static std::string name(G) 
      { 
82
        return std::string(typeid(G).name()); 
83
      }
84

85
      WrongValueFormat(std::string value)
Praetorius, Simon's avatar
Praetorius, Simon committed
86
      : std::runtime_error("cannot convert '" + value + "' into <" + name(T()) + ">")
87
88
      {}
    };
Praetorius, Simon's avatar
Praetorius, Simon committed
89
    
Praetorius, Simon's avatar
Praetorius, Simon committed
90

91
    /// Exception for bad arithmetic expression that can not be evaluated
92
    template<typename T>
93
94
    struct BadArithmeticExpression : std::runtime_error 
    {
Praetorius, Simon's avatar
Praetorius, Simon committed
95
96
97
98
99
      static std::string name(bool) { return "bool"; }
      static std::string name(double) { return "double"; }
      static std::string name(float) { return "float"; }
      static std::string name(int) { return "int"; }
      static std::string name(unsigned int) { return "unsigned int"; }
100

101
102
103
      template<typename G>
      static std::string name(G) 
      { 
104
        return std::string(typeid(G).name());
105
      }
106

107
      BadArithmeticExpression(std::string m, std::string value) 
Praetorius, Simon's avatar
Praetorius, Simon committed
108
109
      : std::runtime_error("cannot evaluate expression '" + value + "' into <" + name(T()) + ">\n"
			   "Parser message: '" + m + "'")
110
111
      {}
    };
Praetorius, Simon's avatar
Praetorius, Simon committed
112
      
113
// _____________________________________________________________________________
114

115
116
    /// return the delimiter or throw an exception if there is no known 
    /// delimiter in value
117
118
119
    inline size_t checkDelim(const std::string& value, const std::string& delims)
    {
      size_t pos(std::string::npos);
Praetorius, Simon's avatar
Praetorius, Simon committed
120
      for (size_t i = 0; i < delims.length(); i++) {
121
122
123
        pos = value.find(delims[i]);
        if (pos != std::string::npos)
          return i;
124
      }
Thomas Witkowski's avatar
Thomas Witkowski committed
125
      //      throw NoDelim("cannot detect the delimiter in " + value);
126
127
128
129
130
131
132
133
134
135
136
137
      return 0; 
    }


    /// convert string to string
    inline void convert(const std::string valStr, std::string& value) 
    {
      value = trim(valStr);
    }


    /// convert string to intrinsic type
138
139
140
141
142
143
144
145
    template<typename T> inline
    typename boost::enable_if
	     < boost::mpl::and_
	       < boost::is_pod<T>, 
	         boost::mpl::not_< boost::is_enum<T> > >, 
	       void
	     >::type
    convert(const std::string valStr, T& value)
146
147
148
149
150
151
152
    {
      using boost::lexical_cast;
      using boost::numeric_cast;

      mu::Parser parser;
      parser.DefineConst(_T("M_PI"), m_pi);
      parser.DefineConst(_T("M_E"), m_e);
153

Thomas Witkowski's avatar
Thomas Witkowski committed
154
      //      try {
155
156
        parser.SetExpr(valStr);
        value = numeric_cast< T >(parser.Eval());
Thomas Witkowski's avatar
Thomas Witkowski committed
157
158
159
160
161
162
163
/*       } catch (boost::bad_lexical_cast e) { */
/*         throw WrongValueFormat< T >(valStr); */
/*       } catch (boost::bad_numeric_cast e) { */
/*         throw WrongValueFormat< T >(valStr); */
/*       } catch (mu::Parser::exception_type &e) { */
/*         throw BadArithmeticExpression<T>(e.GetMsg(), valStr); */
/*       } */
164
165
166
    }


167
168
169
170
171
172
    template<typename T> inline
    typename boost::enable_if
	     < boost::is_enum<T>, 
	       void
	     >::type
    convert(const std::string valStr, T& value) 
173
    {
174
      int swap = 0;
Thomas Witkowski's avatar
Thomas Witkowski committed
175
      //      try {
176
        swap = boost::lexical_cast<int>(trim(valStr));
Thomas Witkowski's avatar
Thomas Witkowski committed
177
178
179
/*       } catch (boost::bad_lexical_cast e) { */
/*         throw WrongValueFormat< T >(valStr); */
/*       } */
180
181
182
183
      value = static_cast< T >(swap);
    }


184
185
186
187
188
189
190
191
192
193
194
195
    /// convert special enums
    inline void convert(const std::string valStr, Norm& value) 
    {
      std::string swapStr = boost::to_upper_copy(valStr);

      if (swapStr == "NO_NORM")
        value = static_cast< Norm >(NO_NORM);
      else if (swapStr == "H1_NORM")
        value = static_cast< Norm >(H1_NORM);
      else if (swapStr == "L2_NORM")
        value = static_cast< Norm >(L2_NORM);
      else {
Praetorius, Simon's avatar
Praetorius, Simon committed
196
        int swap = 0;
197
198
199
200
201
202
        convert(valStr, swap);
        value = static_cast< Norm >(swap);
      }
    }


Naumann, Andreas's avatar
Naumann, Andreas committed
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
    /// convert value of arbitrary type to string using stringstream and 
    /// operator<< for type
    template<typename T>
    inline void convert(const T value, std::string& valStr) 
    {
      std::stringstream ss;
      ss.precision(6);
      ss << value;
      valStr = ss.str();
    }


    /// convert WorldVector to string
    template<typename T>
    inline void convert(const WorldVector<T>& c, std::string& valStr)
    {
      std::vector<T> temp_vec(c.getSize());
      for (unsigned i = 0; i < temp_vec.size(); i++)
        temp_vec[i] = c[i];
      convert(temp_vec, valStr);
    }

Thomas Witkowski's avatar
Thomas Witkowski committed
225
226
227
228
229
230
231
232
233
    template< typename T >
    inline void convert(const std::string valStr, WorldVector<T>& c);

    template<typename T>
    inline void convert(const std::string valStr, std::list<T>& value);

    template<typename T>
    inline void convert(const std::string valStr, std::vector<T>& value);

Naumann, Andreas's avatar
Naumann, Andreas committed
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
    /** parse an container from tag tag. The Container must have the properties:
    * 	- type value_type
    * 	- member function push_back
    */
    template< typename Container >
    inline void getContainer(const std::string val_, Container& c)
    {
      // accepted brackets and delimiters for vector input
      std::string begBrackets= "{[(";
      std::string endBrackets= "}])";
      std::string delims= ";,";

      c.clear();
      std::string val = trim(val_);
      bool hasBrackets = true;
      size_t pos = begBrackets.find(val[0]);
      if (pos == std::string::npos)
        hasBrackets = false;
/*       if (hasBrackets && val[val.length() - 1] != endBrackets[pos]) */
/*         throw WrongVectorFormat("begin and end bracket are different in" */
/*             " value '" + val + "'"); */
      size_t oldPos = (hasBrackets ? 1 : 0);
      size_t curDelim = 0;
      typedef typename Container::value_type ValueType;
      ValueType swap;
      try {
        curDelim = checkDelim(val, delims);
        pos = val.find(delims[curDelim], oldPos);
        while (pos != std::string::npos) {
          std::string curWord = val.substr(oldPos, pos - oldPos);
          oldPos = pos + 1;
          convert(curWord, swap);
          c.push_back(swap);
          pos = val.find(delims[curDelim], oldPos);
        }
        //last entry
        std::string curWord = val.substr(oldPos, val.length() - (hasBrackets ? 1 : 0) - oldPos);
        convert(curWord, swap);
        c.push_back(swap);
      } catch (NoDelim nd) {
        std::string curWord = val.substr(oldPos, val.length() - (hasBrackets ? 2 : 0));
        curWord = trim(curWord);
        if (curWord.length() > 0) {
          // container with one entry
          convert(curWord, swap);
          c.push_back(swap);
        }
      }
    }


285
286
287
288
289
290
    /// convert string to WorldVector
    template< typename T >
    inline void convert(const std::string valStr, WorldVector<T>& c) 
    {
      std::vector<T> temp_vec;
      getContainer(valStr, temp_vec);
Thomas Witkowski's avatar
Thomas Witkowski committed
291
292
/*       if (static_cast<int>(temp_vec.size()) != c.getSize()) */
/*         throw WrongVectorSize("wrong number of entries for WorldVector"); */
293
  
Praetorius, Simon's avatar
Praetorius, Simon committed
294
      for (size_t i = 0; i < temp_vec.size(); i++)
295
        c[i] = temp_vec[i];
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
    }

    /// convert string to std::list using begBrackets, endBrackets and delims
    template<typename T>
    inline void convert(const std::string valStr, std::list<T>& value)
    {
      getContainer(valStr, value);
    }


    /// convert string to std::vector using begBrackets, endBrackets and delims
    template<typename T>
    inline void convert(const std::string valStr, std::vector<T>& value)
    {
      getContainer(valStr, value);
    }


314
  } // end namespace detail
Praetorius, Simon's avatar
Praetorius, Simon committed
315

316
// _____________________________________________________________________________
Praetorius, Simon's avatar
Praetorius, Simon committed
317

318
319
320
321
  /** The entry in an initfile. This helper class was constructed to allow calls 
  *  like val = data.get(tag) for arbitrary types of val. At current stage, only
  *  double and bool is supported
  */
322
323
324
325
326
327
  struct InitEntry {
    ///the value as string
    std::string valStr;

    /// initialize with value as string
    InitEntry(std::string v = "")
328
    : valStr(v) 
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
    {}

    /// cast string to type T
    template<typename T>
    operator T() const 
    { 
      T t; 
      convert(valStr, t); 
      return t;
    }
  };


  /// output-stream for std::list
  template<typename T>
  std::ostream& operator<<(std::ostream& o, const std::list< T >& l)
  {
    typename std::list< T >::const_iterator it = l.begin();
    o << "[";
    for (unsigned i = 0; it != l.end() && i < l.size(); i++) {
      o << *it << (i < l.size() - 1 ? ", " : "");
      ++it;
    }
    o << "]";
    return o;
  }


  /// output-stream for std::vector
  template<typename T>
  std::ostream& operator<<(std::ostream& o, const std::vector<T>& l)
  {
    typename std::vector<T>::const_iterator it = l.begin();
    o << "[";
    for (unsigned i = 0; it != l.end() && i < l.size(); i++) {
      o << *it << (i < l.size() - 1 ? ", " : "");
      ++it;
    }
    o << "]";
    return o;
  }

371
// _____________________________________________________________________________
372

373
374
375
  /** Basis data container as a map of tag on a value as strings. The container 
  *  throws an exception, if the tag was not found.
  */
376
377
378
379
  struct Initfile : public std::map<std::string, std::string> 
  {
    typedef std::map< std::string, std::string > super;

380
381
    static const int TAG_NOT_FOUND = 1;
    static const int TAG_NOT_FOUND_BREAK = 2;
382

383
    /// Exceptions
Praetorius, Simon's avatar
Praetorius, Simon committed
384
    struct TagNotFound : std::invalid_argument {
385
      TagNotFound(std::string m) 
386
      : std::invalid_argument(m) 
387
388
389
      {}
    };

390
391
392

    struct TagNotFoundBreak : std::invalid_argument {
    // print 'tag not found' and exit
393
      TagNotFoundBreak(std::string m)
394
      : std::invalid_argument(m) 
395
396
      {}
    };
397
398
399
400
401
402


    /** initialize init-file from file with filename in, read data and save it 
    *  to singleton-map
    *  @param in: filename string
    */
403
404
    static void init(std::string in);

405
    static void init(int print, std::string filename, const char *flags = nullptr) 
406
    {
407
408
409
        WARNING("Parameters::init(int,std::string,const char*) is depreciated. "
          "Use Parameters::init(std::string) instead!\n");
        init(filename);
410
    }
411
412
413
414
415
416
417
418
419
420


    /** Static get routine for getting parameter-values from init-file 
    *  initialized in init()-method.
    *  Cast the value to the desired type using std::stringstream.
    *  @param tag: The tag to look for
    *  @param value: The result.
    *  @param debugInfo: msgInfo for current parameter. (0..no printing,
    *    1..print missing parameter info, 2..print parameter value) [optional]
    */
421
422
423
424
425
    template<typename T>
    static void get(const std::string tag, T& value, int debugInfo = -1)
    {
      initIntern();
      if (debugInfo == -1)
426
        debugInfo = singlett->getMsgInfo();
427
428
429
430
431
      else {
	int swap(debugInfo);
	debugInfo = singlett->getMsgInfo();
	singlett->msgInfo=swap;
      }
432

433
      std::string valStr;
434
      try {
435
436
437
      int error_code = singlett->checkedGet(tag, valStr);
      if (error_code == 0) {
	valStr = trim(valStr);
438
	detail::convert(valStr, value);
Thomas Witkowski's avatar
Thomas Witkowski committed
439
      } 
440
441
442
443
      } catch(mu::ParserError& e) {
	std::string parser_error = "Could not parse: " + tag;
	throw std::runtime_error(parser_error);
      }
Thomas Witkowski's avatar
Thomas Witkowski committed
444
445
      /*
else if(error_code == TAG_NOT_FOUND_BREAK)
446
	throw TagNotFoundBreak("required tag '" + tag + "' not found");
447
448
449
450
      else if (error_code == TAG_NOT_FOUND) {
	if (debugInfo == 2)
	  std::cout << "there is no tag '" + tag + "'" << std::endl;
      } else
Praetorius, Simon's avatar
Praetorius, Simon committed
451
	throw std::runtime_error("unknown error_code (" + boost::lexical_cast<std::string>(error_code) + ") returned for tag '" + tag + "'");
Thomas Witkowski's avatar
Thomas Witkowski committed
452
453
      */

454
455
456
      if (debugInfo == 2) {
	std::cout << "Parameter '" << tag << "'"
		  << " initialized with: " << value << std::endl;
457
      }
458
      singlett->msgInfo = debugInfo;
459
460
    }

461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
    /** Static get routine for getting a parameter-map from init-file 
    *  initialized in init()-method.
    *  Idea: 
    *     line in Initfile: <tag><key>: <value>
    *     is extracted in am map [key]->[value].
    *  @param tag: The tag withch labels the parameter map 
    *  @param debugInfo: msgInfo for current parameter. (0..no printing,
    *    2..print parameter value) [optional]
    */
    static void getParameterMap(const std::string tag, 
				std::map<std::string,std::string> &pm,
				int debugInfo = -1)
    {
	initIntern();
	for (Initfile::iterator it = singlett->begin(); it != singlett->end(); it++){	
	    std::string longTag= (*it).first ;
	    std::string value=(*it).second;
	    if(longTag.compare(tag)>0){
		if (debugInfo == 2){
		    std::cout <<"Extract Parameter map from "<<(*it).first   
			      << " => " <<(*it).second  << std::endl;
		}
		std::string key=trim(longTag.substr(tag.length()));
		if (debugInfo == 2){
		    std::cout <<"Parameter map "<< key << " => " 
			      << value << std::endl;
		}
		pm[key]=value;
	    }
	}	    
    }
    
493
494
495
496
497

    /// return InitEntry object for tag tag
    static InitEntry get(const std::string tag) 
    {
      InitEntry result;
498
499
500
501
502
503
      
      std::string valStr;
      int error_code = singlett->checkedGet(tag, valStr);
      if (error_code == 0) {
	valStr = trim(valStr);
	result = InitEntry(valStr);
Thomas Witkowski's avatar
Thomas Witkowski committed
504
505
506
507
      } 

#if 0
else if(error_code == TAG_NOT_FOUND_BREAK)
508
	  throw TagNotFoundBreak("get(): required tag '" + tag + "' not found");
509
      else if (error_code == TAG_NOT_FOUND)
510
	  throw TagNotFound("get(): there is no tag '" + tag + "'"); 
Praetorius, Simon's avatar
Praetorius, Simon committed
511
	// exception must be thrown, because an empty object would be return otherwise
512
      else
513
	  throw std::runtime_error("get(): unknown error_code returned for tag '" + tag + "'");
Thomas Witkowski's avatar
Thomas Witkowski committed
514
515
#endif

516
517
      return result;
    }
518

519
520
521
522
523
524
525

    /// update map tag->value_old to tag->value in singleton
    template<typename T>
    static void set(const std::string tag, T& value, int debugInfo=  -1) 
    {
      initIntern();
      if (debugInfo == -1)
526
527
        debugInfo = singlett->getMsgInfo();

528
      std::string swap = "";
529
      detail::convert(value, swap);
530
531
532
533
      (*singlett)[trim(tag)] = swap;
      // update msg parameters msgInfo, msgWait, paramInfo
      singlett->getInternalParameters();
      if (debugInfo == 2)
534
535
        std::cout << "Parameter '" << tag << "'"
                  << " set to: " << value << std::endl;
536
    }
537
538


539
540
541
542
543
544
545
    /// add map tag->value to data in singleton
    template< typename T >
    static void add(const std::string tag, T& value, int debugInfo = -1) 
    {
      set(tag, value, debugInfo);
    }

546

547
    /// rescheduling parameter
Praetorius, Simon's avatar
Praetorius, Simon committed
548
    static void readArgv(std::string parameters, int debugInfo = 2);
549

550

551
552
553
    /// Returns specified info level
    static int getMsgInfo() 
    { 
554
      return (singlett != nullptr) ? singlett->msgInfo : 0; 
555
556
    }

557

558
559
560
    /// Returns specified wait value
    static int getMsgWait() 
    { 
561
      return (singlett != nullptr) ? singlett->msgWait : 0; 
562
    }
563
564


565
566
567
    /// Checks whether parameters are initialized. if not, call init()
    static bool initialized() 
    { 
568
      return (singlett != nullptr); 
569
570
    }

571

572
573
574
575
576
    /// return pointer to singleton
    static Initfile *getSingleton() 
    { 
      return singlett; 
    }
577
578


579
580
581
    /// print all data in singleton to std::cout
    static void printParameters();

582

583
584
585
586
587
588
    /// clear data in singleton
    static void clearData()
    {
      initIntern();
      singlett->clear();
    }
589
590


591
592
593
594
595
596
    /// save singlett-data to file with filename fn
    static void save(std::string fn)
    {
      initIntern();
      singlett->write(fn);
    }
Praetorius, Simon's avatar
Praetorius, Simon committed
597
	
598
protected:	
599
    Initfile() 
600
601
602
603
    : msgInfo(0), 
      msgWait(1), 
      paramInfo(1), 
      breakOnMissingTag(0) 
604
    {}
605
606


607
608
    static void initIntern() 
    {
609
      if (singlett == nullptr)
Praetorius, Simon's avatar
Praetorius, Simon committed
610
	singlett = new Initfile;
611
    }
Praetorius, Simon's avatar
Praetorius, Simon committed
612

613

614
615
    /// list of processed files
    static std::set< std::string > fn_include_list;
Praetorius, Simon's avatar
Praetorius, Simon committed
616

617

618
619
    /// pointer to the singleton that contains the data
    static Initfile* singlett;
620
621
622
623


    /// return the value of the given tag or throws an exception if the tag 
    /// does not exist
624
    int checkedGet(const std::string& tag, std::string &valStr) const
625
626
627
    {
      super::const_iterator it = find(tag);
      if (it == end()) {
628
        if (breakOnMissingTag == 0 || msgInfo <= 2)
629
	  return TAG_NOT_FOUND;
630
        else
631
	  return TAG_NOT_FOUND_BREAK;
632
      }
633
634
      valStr = it->second;
      return 0;
635
636
    }

637
638
639
640
641
    /// replace variables by its value defined as parameter previousely
    /// variable definition is simple parameter definition
    /// variable evaluation by ${variablename} or $variablename
    /// the last version only for variablenames without whitespaces
    std::string variableReplacement(const std::string& input) const;
642

Praetorius, Simon's avatar
Praetorius, Simon committed
643
644
    std::string variableEvaluation(const std::string& input) const;
    
645
    /** Fill the initfile from an input stream.
646
647
648
649
650
    * @param in: the stream to fill the data from.
    * Current dataformat: tag:value
    * Comment char: percent '%'
    * Include files: #include "filename" or #include <filename>
    */
651
652
653
    void read(std::istream& in);

    /// Fill the initfile from a file with filename fn
654
    void read(std::string fn, bool force = false);
655

656
    /** Write data-map to initfile
657
658
    * @param out: the stream to fill the data in.
    */
659
    void write(std::ostream& out);
Praetorius, Simon's avatar
Praetorius, Simon committed
660

661
662
    /// Write data-map to initfile with filename fn
    void write(std::string fn);
663

664
665
    /// read parameters for msgInfo, msgWait, paramInfo
    void getInternalParameters();
666

667
668
    int msgInfo, msgWait, paramInfo, breakOnMissingTag;	
  };
669
  
670
  typedef Initfile Parameters;
671

672
} // end namespace AMDiS
Praetorius, Simon's avatar
Praetorius, Simon committed
673
#endif