1 // stgmerge.cxx -- combined STG models
2 //
3 // Copyright (C) 2015 Stuart Buchanan
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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18 
19 #ifdef HAVE_CONFIG_H
20 #include <config.h>
21 #endif
22 
23 #include <iostream>
24 #include <string>
25 #include <cstdlib>
26 #include <iomanip>
27 #include <dirent.h>
28 
29 #include <osg/MatrixTransform>
30 #include <osg/ArgumentParser>
31 #include <osg/Image>
32 #include <osgViewer/Viewer>
33 #include <osgViewer/ViewerEventHandlers>
34 
35 #include <osgGA/StateSetManipulator>
36 #include <osgGA/TrackballManipulator>
37 
38 #include <osgDB/FileNameUtils>
39 #include <osgDB/FileUtils>
40 #include <osgDB/Registry>
41 #include <osgDB/ReaderWriter>
42 #include <osgDB/ReadFile>
43 #include <osgDB/WriteFile>
44 
45 #include <osgUtil/Optimizer>
46 
47 #include <simgear/bucket/newbucket.hxx>
48 #include <simgear/bvh/BVHNode.hxx>
49 #include <simgear/bvh/BVHLineSegmentVisitor.hxx>
50 #include <simgear/bvh/BVHPager.hxx>
51 #include <simgear/bvh/BVHPageNode.hxx>
52 #include <simgear/debug/logstream.hxx>
53 #include <simgear/math/SGGeodesy.hxx>
54 #include <simgear/misc/sg_path.hxx>
55 #include <simgear/io/iostreams/sgstream.hxx>
56 #include <simgear/misc/ResourceManager.hxx>
57 #include <simgear/props/props.hxx>
58 #include <simgear/props/props_io.hxx>
59 #include <simgear/scene/material/matlib.hxx>
60 #include <simgear/scene/model/BVHPageNodeOSG.hxx>
61 #include <simgear/scene/model/ModelRegistry.hxx>
62 #include <simgear/scene/tgdb/apt_signs.hxx>
63 #include <simgear/scene/tgdb/userdata.hxx>
64 #include <simgear/scene/util/OptionsReadFileCallback.hxx>
65 #include <simgear/scene/util/OsgMath.hxx>
66 #include <simgear/scene/util/RenderConstants.hxx>
67 #include <simgear/scene/util/SGReaderWriterOptions.hxx>
68 
69 namespace sg = simgear;
70 
71 struct _ObjectStatic {
_ObjectStatic_ObjectStatic72 	_ObjectStatic() :
73 			_lon(0), _lat(0), _elev(0), _hdg(0), _pitch(0), _roll(0), _shared(false) {
74 	}
75 	std::string _token;
76 	std::string _name;
77 	double _lon, _lat, _elev;
78 	double _hdg, _pitch, _roll;
79 	bool _shared;
80 	osg::ref_ptr<sg::SGReaderWriterOptions> _options;
81 };
82 
83 std::string fg_root;
84 std::string fg_scenery;
85 std::string fg_terrasync_root;
86 std::string input;
87 std::string output;
88 bool display_viewer = false;
89 bool osg_optimizer = false;
90 bool copy_files = false;
91 int group_size = 5000;
92 
staticOptions(const std::string & filePath,const osgDB::Options * options)93 sg::SGReaderWriterOptions* staticOptions(const std::string& filePath,
94 		const osgDB::Options* options) {
95 	osg::ref_ptr<sg::SGReaderWriterOptions> staticOptions;
96 	staticOptions = sg::SGReaderWriterOptions::copyOrCreate(options);
97 	staticOptions->getDatabasePathList().clear();
98 
99 	staticOptions->getDatabasePathList().push_back(filePath);
100 	staticOptions->setObjectCacheHint(osgDB::Options::CACHE_NONE);
101 
102 	// Every model needs its own SGModelData to ensure load/unload is
103 	// working properly
104 	staticOptions->setModelData(
105 			staticOptions->getModelData() ?
106 					staticOptions->getModelData()->clone() : 0);
107 
108 	return staticOptions.release();
109 }
110 
sharedOptions(const std::string & filePath,const osgDB::Options * options)111 sg::SGReaderWriterOptions* sharedOptions(const std::string& filePath,
112 		const osgDB::Options* options) {
113 	osg::ref_ptr<sg::SGReaderWriterOptions> sharedOptions;
114 	sharedOptions = sg::SGReaderWriterOptions::copyOrCreate(options);
115 	sharedOptions->getDatabasePathList().clear();
116 
117 	SGPath path(filePath);
118 	path.append("..");
119 	path.append("..");
120 	path.append("..");
121 	sharedOptions->getDatabasePathList().push_back(path.str());
122 
123 	// ensure Models directory synced via TerraSync is searched before the copy in
124 	// FG_ROOT, so that updated models can be used.
125 	std::string terrasync_root = options->getPluginStringData("SimGear::TERRASYNC_ROOT");
126 	if (!terrasync_root.empty()) {
127 		sharedOptions->getDatabasePathList().push_back(terrasync_root);
128 	}
129 
130 	std::string fg_root = options->getPluginStringData("SimGear::FG_ROOT");
131 	sharedOptions->getDatabasePathList().push_back(fg_root);
132 
133 	// TODO how should we handle this for OBJECT_SHARED?
134 	sharedOptions->setModelData(
135 			sharedOptions->getModelData() ?
136 					sharedOptions->getModelData()->clone() : 0);
137 
138 	return sharedOptions.release();
139 }
140 
processSTG(osg::ref_ptr<simgear::SGReaderWriterOptions> options,std::string stg)141 int processSTG(osg::ref_ptr<simgear::SGReaderWriterOptions> options,
142 		std::string stg) {
143 
144 	// List of .ac files to remove
145 	std::vector<std::string> ac_files;
146 
147 	// Get the STG file
148 	if (stg.empty()) {
149 		SG_LOG(SG_TERRAIN, SG_ALERT, "STG file empty");
150 		return EXIT_FAILURE;
151 	}
152 
153 	SGPath sourcestg(input);
154 	sourcestg.append(stg);
155 
156 	SGPath deststg(output);
157 	deststg.append(stg);
158 
159 	SG_LOG(SG_TERRAIN, SG_INFO, "Processing " << sourcestg.c_str());
160 
161 	sg_gzifstream stream((std::string) sourcestg.c_str());
162 	if (!stream.is_open()) {
163 		SG_LOG(SG_TERRAIN, SG_ALERT, "Unable to open STG file " << sourcestg.c_str());
164 		return EXIT_FAILURE;
165 	}
166 
167 	// Extract the bucket from the filename
168 	std::istringstream ss(osgDB::getStrippedName(sourcestg.file().c_str()));
169 	long index;
170 	ss >> index;
171 	if (ss.fail()) {
172 		SG_LOG(SG_TERRAIN, SG_ALERT,
173 				"Unable to determine bucket from STG filename " << ss.rdbuf());
174 		return EXIT_FAILURE;
175 	}
176 
177 	SGBucket bucket = SGBucket(index);
178 
179 	// We will group the object into group_size x group_size
180 	// block, so work out the number and dimensions in degrees
181 	// of each block.
182 	int lon_blocks = (int) ceil(bucket.get_width_m() / group_size);
183 	int lat_blocks = (int) ceil(bucket.get_height_m() / group_size);
184 	float lat_delta = bucket.get_height() / lat_blocks;
185 	float lon_delta = bucket.get_width() / lon_blocks;
186 	SG_LOG(SG_TERRAIN, SG_DEBUG,
187 			"Splitting into (" << lon_blocks << "," << lat_blocks << ") blocks"
188 			<< "of size (" << lon_delta << "," << lat_delta << ") degrees\n");
189 
190 	std::list<_ObjectStatic> _objectStaticList[lon_blocks][lat_blocks];
191 
192 	// Write out the STG files.
193 	SG_LOG(SG_TERRAIN, SG_DEBUG, "Writing to " << deststg.c_str());
194 	std::ofstream stgout(deststg.c_str(), std::ofstream::out);
195 
196 	if (!stgout.is_open()) {
197 		SG_LOG(SG_TERRAIN, SG_ALERT,
198 				"Unable to open STG file to write " << deststg.c_str());
199 		return EXIT_FAILURE;
200 	}
201 
202 	while (!stream.eof()) {
203 		// read a line
204 		std::string line;
205 		std::getline(stream, line);
206 
207 		// strip comments
208 		std::string::size_type hash_pos = line.find('#');
209 
210 		if (hash_pos == 0)
211 			stgout << line << "\n";
212 
213 		if (hash_pos != std::string::npos)
214 			line.resize(hash_pos);
215 
216 		// and process further
217 		std::stringstream in(line);
218 
219 		std::string token;
220 		in >> token;
221 
222 		// No comment
223 		if (token.empty())
224 			continue;
225 
226 		// Then there is always a name
227 		std::string name;
228 		in >> name;
229 
230 		//  The following tokens are ignored
231 		// OBJECT_BASE - base scenery, not relevant
232 		// OBJECT - airport BTG files
233 		// OBJECT_STATIC_AGL - elevation needs to be calculated at runtime
234 		// OBJECT_SHARED_AGL - elevation needs to be calculated at runtime
235 		// OBJECT_SIGN - depends on materials library, which is runtime dependent
236 
237 		if ((token == "OBJECT_STATIC") || (token == "OBJECT_SHARED")) {
238 			if (SGPath(name).lower_extension() != "ac") {
239 				// We only attempt to merge static .ac objects, as they don't have any
240 				// animations
241 				SG_LOG(SG_TERRAIN, SG_DEBUG, "Ignoring non .ac file '" << name << "'");
242 				stgout << line << "\n";
243 				continue;
244 			}
245 
246 			// If we merge it, then we should remove the .ac file from the output.
247 			ac_files.push_back(name);
248 
249 			SGPath filePath(sourcestg.dir());
250 			filePath.append(name);
251 
252 			_ObjectStatic obj;
253 			osg::ref_ptr<sg::SGReaderWriterOptions> opt;
254 
255 			if (token == "OBJECT_STATIC") {
256 				opt = staticOptions(deststg.dir().c_str(), options);
257 				//if (SGPath(name).lower_extension() == "ac")
258 				//opt->setInstantiateEffects(true);  // Is this output correctly?
259 				obj._shared = false;
260 			} else if (token == "OBJECT_SHARED") {
261 				opt = sharedOptions(deststg.dir().c_str(), options);
262 				//if (SGPath(name).lower_extension() == "ac")
263 				//opt->setInstantiateEffects(true);  // Is this output correctly?
264 				obj._shared = true;
265 			} else {
266 				SG_LOG(SG_TERRAIN, SG_ALERT, "Broken code - unexpected object '" << token << "'");
267 			}
268 
269 			obj._token = token;
270 			obj._name = name;
271 			obj._options = opt;
272 			in >> obj._lon >> obj._lat >> obj._elev >> obj._hdg >> obj._pitch
273 					>> obj._roll;
274 
275 			// Sanity check this object is in the correct bucket
276 			if ((obj._lon < bucket.get_corner(0).getLongitudeDeg()) ||
277 					(obj._lon > bucket.get_corner(2).getLongitudeDeg()) ||
278 					(obj._lat < bucket.get_corner(0).getLatitudeDeg())  ||
279 					(obj._lat > bucket.get_corner(2).getLatitudeDeg())     ) {
280 				SG_LOG(SG_TERRAIN, SG_ALERT,
281 						"File " << sourcestg.c_str() << " contains object outside bounds of bucket:\n" <<
282 						" Bucket bounds (lat, lon) : (" <<
283 						bucket.get_corner(0).getLatitudeDeg() << ", " <<
284 						bucket.get_corner(0).getLongitudeDeg() << ") - (" <<
285 						bucket.get_corner(2).getLatitudeDeg() << ", " <<
286 						bucket.get_corner(2).getLongitudeDeg() << ") \n" <<
287 						line << "\n");
288 
289 					// Simply leave it in place for the moment.
290 					stgout << line << "\n";
291 			} else {
292 				// Determine the correct bucket for this object.
293 				int x = (int) floor((obj._lon - bucket.get_corner(0).getLongitudeDeg()) / lon_delta);
294 				int y = (int) floor((obj._lat - bucket.get_corner(0).getLatitudeDeg()) / lat_delta);
295 				SG_LOG(SG_TERRAIN, SG_INFO,
296 						"Assigned (" << obj._lon << "," << obj._lat << ") to block (" << x << "," << y << ")");
297 
298 				_objectStaticList[x][y].push_back(obj);
299 			}
300 		} else {
301 			SG_LOG(SG_TERRAIN, SG_DEBUG, "Ignoring token '" << token << "'");
302 			stgout << line << "\n";
303 		}
304 	}
305 
306 	stream.close();
307 
308 	for (int x = 0; x < lon_blocks; ++x) {
309 		for (int y = 0; y < lat_blocks; ++y) {
310 
311 			if (_objectStaticList[x][y].size() == 0) {
312 				// Nothing to do, so skip
313 				continue;
314 			}
315 
316 			//SG_LOG(SG_TERRAIN, SG_ALERT, "Object files " << _objectStaticList[x][y].size());
317 
318 			osg::ref_ptr<osg::Group> group = new osg::Group;
319 			group->setName("STG merge");
320 			group->setDataVariance(osg::Object::STATIC);
321 			int files_loaded = 0;
322 
323 			// Calculate center of this block
324 			const SGGeod center = SGGeod::fromDegM(
325 					bucket.get_center_lon() - 0.5 * bucket.get_width() + (double) ((x +0.5) * lon_delta),
326 					bucket.get_center_lat() - 0.5 * bucket.get_height() + (double) ((y + 0.5) * lat_delta),
327 					0.0);
328 
329 			//SG_LOG(SG_TERRAIN, SG_ALERT,
330 			//		"Center of block: " << center.getLongitudeDeg() << ", " << center.getLatitudeDeg());
331 
332 			// Inverse used to translate individual matrices
333 			SGVec3d shift;
334 			SGGeodesy::SGGeodToCart(center , shift);
335 
336 			for (std::list<_ObjectStatic>::iterator i = _objectStaticList[x][y].begin();
337 					i != _objectStaticList[x][y].end(); ++i) {
338 
339 				SG_LOG(SG_TERRAIN, SG_INFO, "Processing " << i->_name);
340 
341 				osg::ref_ptr<osg::Node> node;
342 				node = osgDB::readRefNodeFile(i->_name, i->_options.get());
343 				if (!node.valid()) {
344 					SG_LOG(SG_TERRAIN, SG_ALERT,
345 							stg << ": Failed to load " << i->_token << " '" << i->_name << "'");
346 					continue;
347 				}
348 				files_loaded++;
349 
350 				if (SGPath(i->_name).lower_extension() == "ac")
351 					node->setNodeMask(~sg::MODELLIGHT_BIT);
352 
353 				const SGGeod q = SGGeod::fromDegM(i->_lon, i->_lat, i->_elev);
354 				SGVec3d coord;
355 				SGGeodesy::SGGeodToCart(q, coord);
356 				coord = coord - shift;
357 
358 				// Create an matrix to convert from global coordinates to the
359 				// Z-Up local coordinate system used by scenery models.
360 				// This is simply the inverse of the normal scenery model
361 				// matrix.
362 				osg::Matrix m = makeZUpFrameRelative(center);
363 				osg::Matrix inv = osg::Matrix::inverse(m);
364 				osg::Vec3f v = toOsg(coord) * inv;
365 
366 				osg::Matrix matrix;
367 				matrix.setTrans(v);
368 				matrix.preMultRotate(
369 						osg::Quat(SGMiscd::deg2rad(i->_hdg), osg::Vec3(0, 0, 1)));
370 				matrix.preMultRotate(
371 						osg::Quat(SGMiscd::deg2rad(i->_pitch), osg::Vec3(0, 1, 0)));
372 				matrix.preMultRotate(
373 						osg::Quat(SGMiscd::deg2rad(i->_roll), osg::Vec3(1, 0, 0)));
374 
375 				osg::MatrixTransform* matrixTransform;
376 				matrixTransform = new osg::MatrixTransform(matrix);
377 				matrixTransform->setName("positionStaticObject");
378 				matrixTransform->setDataVariance(osg::Object::STATIC);
379 				matrixTransform->addChild(node.get());
380 
381 				// Shift the models so they are centered on the center of the block.
382 				// We will place the object at the right position in the tile later.
383 				group->addChild(matrixTransform);
384 			}
385 
386 			osgViewer::Viewer viewer;
387 
388 			if (osg_optimizer || display_viewer) {
389 				// Create a viewer - required for some Optimizers and if we are to display
390 				// the results
391 				viewer.setSceneData(group.get());
392 				viewer.addEventHandler(new osgViewer::StatsHandler);
393 				viewer.addEventHandler(new osgViewer::WindowSizeHandler);
394 				viewer.addEventHandler(
395 						new osgGA::StateSetManipulator(
396 								viewer.getCamera()->getOrCreateStateSet()));
397 				viewer.setCameraManipulator(new osgGA::TrackballManipulator());
398 				viewer.realize();
399 			}
400 
401 			if (osg_optimizer) {
402 				// Run the Optimizer
403 				osgUtil::Optimizer optimizer;
404 
405 				//  See osgUtil::Optimizer for list of optimizations available.
406 				//optimizer.optimize(group, osgUtil::Optimizer::ALL_OPTIMIZATIONS);
407 				int optimizationOptions = osgUtil::Optimizer::FLATTEN_STATIC_TRANSFORMS
408 						| osgUtil::Optimizer::REMOVE_REDUNDANT_NODES
409 						| osgUtil::Optimizer::COMBINE_ADJACENT_LODS
410 						| osgUtil::Optimizer::SHARE_DUPLICATE_STATE
411 						| osgUtil::Optimizer::MERGE_GEOMETRY
412 						| osgUtil::Optimizer::MAKE_FAST_GEOMETRY
413 						| osgUtil::Optimizer::SPATIALIZE_GROUPS
414 						| osgUtil::Optimizer::OPTIMIZE_TEXTURE_SETTINGS
415 						| osgUtil::Optimizer::TEXTURE_ATLAS_BUILDER
416 						| osgUtil::Optimizer::CHECK_GEOMETRY
417 						| osgUtil::Optimizer::STATIC_OBJECT_DETECTION;
418 
419 				optimizer.optimize(group, optimizationOptions);
420 			}
421 
422 			// Serialize the result as a binary OSG file, including textures:
423 			std::string filename = sourcestg.file();
424 
425 			// Include both the STG name and the indexes for uniqueness.
426 			// ostringstream required for compilers that don't support C++11
427 			std::ostringstream oss;
428 			oss << x << y;
429 			filename.append(oss.str());
430 			filename.append(".osg");
431 			SGPath osgpath = SGPath(deststg.dir(), filename);
432 			osgDB::writeNodeFile(*group, osgpath.c_str(),
433 					new osgDB::Options("WriteImageHint=IncludeData Compressor=zlib"));
434 
435 			// Write out the required STG entry for this merged set of objects, centered
436 			// on the center of the tile.
437 			stgout << "OBJECT_STATIC " << osgpath.file() << " " << center.getLongitudeDeg()
438 					<< " " << center.getLatitudeDeg() << " 0.0 0.0 0.0 0.0\n";
439 
440 			if (display_viewer) {
441 				viewer.run();
442 			}
443 		}
444 	}
445 
446 	if (copy_files) {
447 		std::vector<std::string>::const_iterator iter;
448 
449 		for (iter = ac_files.begin(); iter != ac_files.end(); ++iter) {
450 			SGPath acfile = SGPath(deststg.dir());
451 			acfile.append(*iter);
452 			if (acfile.isFile()) {
453 				if (remove(acfile.c_str()) != 0) {
454 					SG_LOG(SG_GENERAL, SG_ALERT, "Unable to remove " << acfile.c_str());
455 				}
456 			}
457 		}
458 	}
459 
460 	// Finished with this file.
461 	stgout.flush();
462 	stgout.close();
463 
464 	return EXIT_SUCCESS;
465 }
466 
processDirectory(osg::ref_ptr<simgear::SGReaderWriterOptions> options,SGPath directory_name)467 int processDirectory(osg::ref_ptr<simgear::SGReaderWriterOptions> options,
468 		SGPath directory_name) {
469 
470 	// List of STG files to process
471 	std::vector<std::string> stg_files;
472 
473 	SGPath source = SGPath(input);
474 	source.append(directory_name.c_str());
475 
476 	SG_LOG(SG_GENERAL, SG_INFO, "Processing " << source.c_str());
477 
478 	DIR *dir = NULL;
479 	dir = opendir(source.c_str());
480 
481 	if (dir == NULL) {
482 		SG_LOG(SG_GENERAL, SG_ALERT, "Unable to open " << source.c_str());
483 		return EXIT_FAILURE;
484 	}
485 
486 	if (copy_files) {
487 		// Create the directory in the output directory
488 		SGPath destination = SGPath(output);
489 		destination.append(directory_name.c_str());
490 		destination.append(".");  //  Do this so the path is a directory rather than a file!
491 		if (destination.exists()) {
492 			if (! destination.isDir()) {
493 				SG_LOG(SG_GENERAL, SG_ALERT, destination.c_str() << " exists and is not a directory.");
494 				return EXIT_FAILURE;
495 			}
496 			if (! destination.canWrite()) {
497 				SG_LOG(SG_GENERAL, SG_ALERT, destination.c_str() << " cannot be written.");
498 				return EXIT_FAILURE;
499 			}
500 		} else {
501 
502 			// Note that this create the directory part of the path,
503 			int success = destination.create_dir();
504 
505 			if (success != 0) {
506 				SG_LOG(SG_GENERAL, SG_ALERT, "Unable to create " << destination.dir());
507 				return EXIT_FAILURE;
508 			}
509 
510 			if (! destination.exists()) {
511 				SG_LOG(SG_GENERAL, SG_ALERT, "Failed to create " << destination.dir());
512 				return EXIT_FAILURE;
513 			}
514 		}
515 	}
516 
517 	struct dirent *pent = NULL;
518 	pent = readdir(dir);
519 
520 	while (pent != NULL) {
521 		std::string fname = pent->d_name;
522 		SG_LOG(SG_GENERAL, SG_DEBUG, "  checking " << fname.c_str());
523 
524 		if ((fname.compare(".") == 0) || (fname.compare("..") == 0)) {
525 			pent = readdir(dir);
526 			continue;
527 		}
528 
529 		SGPath fpath = SGPath(input);
530 		fpath.append(directory_name.c_str());
531 		fpath.append(fname);
532 
533 	  if (fpath.isDir()) {
534 			SGPath dirname = SGPath(directory_name);
535 			dirname.append(fname);
536 			if (processDirectory(options, dirname) == EXIT_FAILURE) {
537 				return EXIT_FAILURE;
538 			}
539     } else if (SGPath(fname).lower_extension() == "stg") {
540 			stg_files.push_back(fname);
541 		} else if (copy_files) {
542 			// Copy it over if we're copying all files.
543 			SGPath fsource = SGPath(input);
544 			fsource.append(directory_name.c_str());
545 			fsource.append(fname);
546 			SGPath fdestination = SGPath(output);
547 			fdestination.append(directory_name.c_str());
548 			fdestination.append(fname);
549 
550 			SG_LOG(SG_GENERAL, SG_DEBUG, "Copying " << fsource.c_str() << " to " << fdestination.c_str());
551 			std::ifstream src(fsource.c_str(), std::ios::binary);
552 			std::ofstream dst(fdestination.c_str(), std::ios::binary);
553 
554 			dst << src.rdbuf();
555 		}
556 		pent = readdir(dir);
557 	}
558 
559 	closedir(dir);
560 
561 	// Now we've copied the data, process the STG files
562 	std::vector<std::string>::const_iterator iter;
563 
564 	for (iter = stg_files.begin(); iter != stg_files.end(); ++iter) {
565 		SGPath stg = SGPath(directory_name);
566 		stg.append(*iter);
567 		processSTG(options, stg.c_str());
568 	}
569 
570 	return EXIT_SUCCESS;
571 }
572 
main(int argc,char ** argv)573 int main(int argc, char** argv) {
574 	osg::ApplicationUsage* usage = new osg::ApplicationUsage();
575 	usage->setApplicationName("stgmerge");
576 	usage->setCommandLineUsage(
577 			"Merge static model files within a given STG file.");
578 	usage->addCommandLineOption("--input <dir>", "Scenery directory to read");
579 	usage->addCommandLineOption("--output <dir>",
580 			"Output directory for STGs and merged models");
581 	usage->addCommandLineOption("--fg-root <dir>", "FG root directory",
582 			"$FG_ROOT");
583 	usage->addCommandLineOption("--fg-scenery <dir>", "FG scenery path",
584 			"$FG_SCENERY");
585 	usage->addCommandLineOption("--terrasync <dir>", "Terrasync path (for Models)");
586 	usage->addCommandLineOption("--group-size <N>", "Group size (m)", "5000");
587 	usage->addCommandLineOption("--optimize", "Optimize scene-graph");
588 	usage->addCommandLineOption("--viewer", "Display loaded objects");
589 	usage->addCommandLineOption("--copy-files", "Copy all contents of input directory into output directory");
590 
591 	// use an ArgumentParser object to manage the program arguments.
592 	osg::ArgumentParser arguments(&argc, argv);
593 
594 	arguments.setApplicationUsage(usage);
595 
596 	if (arguments.read("--fg-root", fg_root)) {
597 	} else if (const char *fg_root_env = std::getenv("FG_ROOT")) {
598 		fg_root = fg_root_env;
599 	} else {
600 		fg_root = PKGLIBDIR;
601 	}
602 
603 	if (arguments.read("--fg-scenery", fg_scenery)) {
604 	} else if (const char *fg_scenery_env = std::getenv("FG_SCENERY")) {
605 		fg_scenery = fg_scenery_env;
606 	} else {
607 		SGPath path(fg_root);
608 		path.append("Scenery");
609 		fg_scenery = path.str();
610 	}
611 
612 	if (arguments.read("--terrasync", fg_terrasync_root)) {
613 	} else {
614 		fg_scenery = "";
615 	}
616 
617 	if (!arguments.read("--input", input)) {
618 		arguments.reportError("--input argument required.");
619 	} else {
620 		SGPath s(input);
621 		if (!s.isDir()) {
622 			arguments.reportError(
623 					"--input directory does not exist or is not directory.");
624 		} else if (!s.canRead()) {
625 			arguments.reportError(
626 					"--input directory cannot be read. Check permissions.");
627 		}
628 	}
629 
630 	if (!arguments.read("--output", output)) {
631 		arguments.reportError("--output argument required.");
632 	} else {
633 		// Check directory exists, we can write to it, and we're not about to write
634 		// to the same location as the STG file.
635 		SGPath p(output);
636 		SGPath s(input);
637 		if (!p.isDir()) {
638 			arguments.reportError("--output directory does not exist.");
639 		}
640 
641 		if (!p.canWrite()) {
642 			arguments.reportError(
643 					"--output directory is not writeable. Check permissions.");
644 		}
645 
646 		if (s == p) {
647 			arguments.reportError(
648 					"--output directory must differ from STG directory.");
649 		}
650 	}
651 
652 	if (arguments.read("--group-size")) {
653 		if (! arguments.read("--group-size", group_size)) {
654 			arguments.reportError(
655 								"--group-size argument number be a positive integer.");
656 		}
657 	}
658 
659 	if (arguments.read("--viewer")) {
660 		display_viewer = true;
661 	}
662 
663 	if (arguments.read("--optimize")) {
664 		osg_optimizer = true;
665 	}
666 
667 	if (arguments.read("--copy-files")) {
668 		copy_files = true;
669 	}
670 
671 	if (arguments.errors()) {
672 		arguments.writeErrorMessages(std::cout);
673 		arguments.getApplicationUsage()->write(std::cout,
674 				osg::ApplicationUsage::COMMAND_LINE_OPTION
675 						| osg::ApplicationUsage::ENVIRONMENTAL_VARIABLE, 80, true);
676 		return EXIT_FAILURE;
677 	}
678 
679 	SGSharedPtr<SGPropertyNode> props = new SGPropertyNode;
680 	try {
681 		SGPath preferencesFile = fg_root;
682 		preferencesFile.append("defaults.xml");
683 		readProperties(preferencesFile.str(), props);
684 	} catch (...) {
685 		// In case of an error, at least make summer :)
686 		props->getNode("sim/startup/season", true)->setStringValue("summer");
687 
688 		SG_LOG(SG_GENERAL, SG_ALERT,
689 				"Problems loading FlightGear preferences.\n" << "Probably FG_ROOT is not properly set.");
690 	}
691 
692 	/// now set up the simgears required model stuff
693 
694 	simgear::ResourceManager::instance()->addBasePath(fg_root,
695 			simgear::ResourceManager::PRIORITY_DEFAULT);
696 	// Just reference simgears reader writer stuff so that the globals get
697 	// pulled in by the linker ...
698 	simgear::ModelRegistry::instance();
699 
700 	sgUserDataInit(props.get());
701 	SGMaterialLibPtr ml = new SGMaterialLib;
702 	SGPath mpath(fg_root);
703 
704 	// TODO: Pick up correct materials.xml file.  Urrgh - this can't be runtime dependent...
705 	mpath.append("Materials/default/materials.xml");
706 	try {
707 		ml->load(fg_root, mpath.str(), props);
708 	} catch (...) {
709 		SG_LOG(SG_GENERAL, SG_ALERT,
710 				"Problems loading FlightGear materials.\n" << "Probably FG_ROOT is not properly set.");
711 	}
712 	simgear::SGModelLib::init(fg_root, props);
713 
714 	// Set up the reader/writer options
715 	osg::ref_ptr<simgear::SGReaderWriterOptions> options;
716 	if (osgDB::Options* ropt = osgDB::Registry::instance()->getOptions())
717 		options = new simgear::SGReaderWriterOptions(*ropt);
718 	else
719 		options = new simgear::SGReaderWriterOptions;
720 	osgDB::convertStringPathIntoFilePathList(fg_scenery,
721 			options->getDatabasePathList());
722 	options->setMaterialLib(ml);
723 	options->setPropertyNode(props);
724 	options->setPluginStringData("SimGear::FG_ROOT", fg_root);
725 	options->setPluginStringData("SimGear::TERRASYNC_ROOT", fg_terrasync_root);
726 
727 	// Here, all arguments are processed
728 	arguments.reportRemainingOptionsAsUnrecognized();
729 	arguments.writeErrorMessages(std::cerr);
730 
731 	std::string dot = "";
732 	return processDirectory(options, dot);
733 }
734