// stgmerge.cxx -- combined STG models // // Copyright (C) 2015 Stuart Buchanan // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License as // published by the Free Software Foundation; either version 2 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace sg = simgear; struct _ObjectStatic { _ObjectStatic() : _lon(0), _lat(0), _elev(0), _hdg(0), _pitch(0), _roll(0), _shared(false) { } std::string _token; std::string _name; double _lon, _lat, _elev; double _hdg, _pitch, _roll; bool _shared; osg::ref_ptr _options; }; std::string fg_root; std::string fg_scenery; std::string fg_terrasync_root; std::string input; std::string output; bool display_viewer = false; bool osg_optimizer = false; bool copy_files = false; int group_size = 5000; sg::SGReaderWriterOptions* staticOptions(const std::string& filePath, const osgDB::Options* options) { osg::ref_ptr staticOptions; staticOptions = sg::SGReaderWriterOptions::copyOrCreate(options); staticOptions->getDatabasePathList().clear(); staticOptions->getDatabasePathList().push_back(filePath); staticOptions->setObjectCacheHint(osgDB::Options::CACHE_NONE); // Every model needs its own SGModelData to ensure load/unload is // working properly staticOptions->setModelData( staticOptions->getModelData() ? staticOptions->getModelData()->clone() : 0); return staticOptions.release(); } sg::SGReaderWriterOptions* sharedOptions(const std::string& filePath, const osgDB::Options* options) { osg::ref_ptr sharedOptions; sharedOptions = sg::SGReaderWriterOptions::copyOrCreate(options); sharedOptions->getDatabasePathList().clear(); SGPath path(filePath); path.append(".."); path.append(".."); path.append(".."); sharedOptions->getDatabasePathList().push_back(path.str()); // ensure Models directory synced via TerraSync is searched before the copy in // FG_ROOT, so that updated models can be used. std::string terrasync_root = options->getPluginStringData("SimGear::TERRASYNC_ROOT"); if (!terrasync_root.empty()) { sharedOptions->getDatabasePathList().push_back(terrasync_root); } std::string fg_root = options->getPluginStringData("SimGear::FG_ROOT"); sharedOptions->getDatabasePathList().push_back(fg_root); // TODO how should we handle this for OBJECT_SHARED? sharedOptions->setModelData( sharedOptions->getModelData() ? sharedOptions->getModelData()->clone() : 0); return sharedOptions.release(); } int processSTG(osg::ref_ptr options, std::string stg) { // List of .ac files to remove std::vector ac_files; // Get the STG file if (stg.empty()) { SG_LOG(SG_TERRAIN, SG_ALERT, "STG file empty"); return EXIT_FAILURE; } SGPath sourcestg(input); sourcestg.append(stg); SGPath deststg(output); deststg.append(stg); SG_LOG(SG_TERRAIN, SG_INFO, "Processing " << sourcestg.c_str()); sg_gzifstream stream((std::string) sourcestg.c_str()); if (!stream.is_open()) { SG_LOG(SG_TERRAIN, SG_ALERT, "Unable to open STG file " << sourcestg.c_str()); return EXIT_FAILURE; } // Extract the bucket from the filename std::istringstream ss(osgDB::getStrippedName(sourcestg.file().c_str())); long index; ss >> index; if (ss.fail()) { SG_LOG(SG_TERRAIN, SG_ALERT, "Unable to determine bucket from STG filename " << ss.rdbuf()); return EXIT_FAILURE; } SGBucket bucket = SGBucket(index); // We will group the object into group_size x group_size // block, so work out the number and dimensions in degrees // of each block. int lon_blocks = (int) ceil(bucket.get_width_m() / group_size); int lat_blocks = (int) ceil(bucket.get_height_m() / group_size); float lat_delta = bucket.get_height() / lat_blocks; float lon_delta = bucket.get_width() / lon_blocks; SG_LOG(SG_TERRAIN, SG_DEBUG, "Splitting into (" << lon_blocks << "," << lat_blocks << ") blocks" << "of size (" << lon_delta << "," << lat_delta << ") degrees\n"); std::list<_ObjectStatic> _objectStaticList[lon_blocks][lat_blocks]; // Write out the STG files. SG_LOG(SG_TERRAIN, SG_DEBUG, "Writing to " << deststg.c_str()); std::ofstream stgout(deststg.c_str(), std::ofstream::out); if (!stgout.is_open()) { SG_LOG(SG_TERRAIN, SG_ALERT, "Unable to open STG file to write " << deststg.c_str()); return EXIT_FAILURE; } while (!stream.eof()) { // read a line std::string line; std::getline(stream, line); // strip comments std::string::size_type hash_pos = line.find('#'); if (hash_pos == 0) stgout << line << "\n"; if (hash_pos != std::string::npos) line.resize(hash_pos); // and process further std::stringstream in(line); std::string token; in >> token; // No comment if (token.empty()) continue; // Then there is always a name std::string name; in >> name; // The following tokens are ignored // OBJECT_BASE - base scenery, not relevant // OBJECT - airport BTG files // OBJECT_STATIC_AGL - elevation needs to be calculated at runtime // OBJECT_SHARED_AGL - elevation needs to be calculated at runtime // OBJECT_SIGN - depends on materials library, which is runtime dependent if ((token == "OBJECT_STATIC") || (token == "OBJECT_SHARED")) { if (SGPath(name).lower_extension() != "ac") { // We only attempt to merge static .ac objects, as they don't have any // animations SG_LOG(SG_TERRAIN, SG_DEBUG, "Ignoring non .ac file '" << name << "'"); stgout << line << "\n"; continue; } // If we merge it, then we should remove the .ac file from the output. ac_files.push_back(name); SGPath filePath(sourcestg.dir()); filePath.append(name); _ObjectStatic obj; osg::ref_ptr opt; if (token == "OBJECT_STATIC") { opt = staticOptions(deststg.dir().c_str(), options); //if (SGPath(name).lower_extension() == "ac") //opt->setInstantiateEffects(true); // Is this output correctly? obj._shared = false; } else if (token == "OBJECT_SHARED") { opt = sharedOptions(deststg.dir().c_str(), options); //if (SGPath(name).lower_extension() == "ac") //opt->setInstantiateEffects(true); // Is this output correctly? obj._shared = true; } else { SG_LOG(SG_TERRAIN, SG_ALERT, "Broken code - unexpected object '" << token << "'"); } obj._token = token; obj._name = name; obj._options = opt; in >> obj._lon >> obj._lat >> obj._elev >> obj._hdg >> obj._pitch >> obj._roll; // Sanity check this object is in the correct bucket if ((obj._lon < bucket.get_corner(0).getLongitudeDeg()) || (obj._lon > bucket.get_corner(2).getLongitudeDeg()) || (obj._lat < bucket.get_corner(0).getLatitudeDeg()) || (obj._lat > bucket.get_corner(2).getLatitudeDeg()) ) { SG_LOG(SG_TERRAIN, SG_ALERT, "File " << sourcestg.c_str() << " contains object outside bounds of bucket:\n" << " Bucket bounds (lat, lon) : (" << bucket.get_corner(0).getLatitudeDeg() << ", " << bucket.get_corner(0).getLongitudeDeg() << ") - (" << bucket.get_corner(2).getLatitudeDeg() << ", " << bucket.get_corner(2).getLongitudeDeg() << ") \n" << line << "\n"); // Simply leave it in place for the moment. stgout << line << "\n"; } else { // Determine the correct bucket for this object. int x = (int) floor((obj._lon - bucket.get_corner(0).getLongitudeDeg()) / lon_delta); int y = (int) floor((obj._lat - bucket.get_corner(0).getLatitudeDeg()) / lat_delta); SG_LOG(SG_TERRAIN, SG_INFO, "Assigned (" << obj._lon << "," << obj._lat << ") to block (" << x << "," << y << ")"); _objectStaticList[x][y].push_back(obj); } } else { SG_LOG(SG_TERRAIN, SG_DEBUG, "Ignoring token '" << token << "'"); stgout << line << "\n"; } } stream.close(); for (int x = 0; x < lon_blocks; ++x) { for (int y = 0; y < lat_blocks; ++y) { if (_objectStaticList[x][y].size() == 0) { // Nothing to do, so skip continue; } //SG_LOG(SG_TERRAIN, SG_ALERT, "Object files " << _objectStaticList[x][y].size()); osg::ref_ptr group = new osg::Group; group->setName("STG merge"); group->setDataVariance(osg::Object::STATIC); int files_loaded = 0; // Calculate center of this block const SGGeod center = SGGeod::fromDegM( bucket.get_center_lon() - 0.5 * bucket.get_width() + (double) ((x +0.5) * lon_delta), bucket.get_center_lat() - 0.5 * bucket.get_height() + (double) ((y + 0.5) * lat_delta), 0.0); //SG_LOG(SG_TERRAIN, SG_ALERT, // "Center of block: " << center.getLongitudeDeg() << ", " << center.getLatitudeDeg()); // Inverse used to translate individual matrices SGVec3d shift; SGGeodesy::SGGeodToCart(center , shift); for (std::list<_ObjectStatic>::iterator i = _objectStaticList[x][y].begin(); i != _objectStaticList[x][y].end(); ++i) { SG_LOG(SG_TERRAIN, SG_INFO, "Processing " << i->_name); osg::ref_ptr node; node = osgDB::readRefNodeFile(i->_name, i->_options.get()); if (!node.valid()) { SG_LOG(SG_TERRAIN, SG_ALERT, stg << ": Failed to load " << i->_token << " '" << i->_name << "'"); continue; } files_loaded++; if (SGPath(i->_name).lower_extension() == "ac") node->setNodeMask(~sg::MODELLIGHT_BIT); const SGGeod q = SGGeod::fromDegM(i->_lon, i->_lat, i->_elev); SGVec3d coord; SGGeodesy::SGGeodToCart(q, coord); coord = coord - shift; // Create an matrix to convert from global coordinates to the // Z-Up local coordinate system used by scenery models. // This is simply the inverse of the normal scenery model // matrix. osg::Matrix m = makeZUpFrameRelative(center); osg::Matrix inv = osg::Matrix::inverse(m); osg::Vec3f v = toOsg(coord) * inv; osg::Matrix matrix; matrix.setTrans(v); matrix.preMultRotate( osg::Quat(SGMiscd::deg2rad(i->_hdg), osg::Vec3(0, 0, 1))); matrix.preMultRotate( osg::Quat(SGMiscd::deg2rad(i->_pitch), osg::Vec3(0, 1, 0))); matrix.preMultRotate( osg::Quat(SGMiscd::deg2rad(i->_roll), osg::Vec3(1, 0, 0))); osg::MatrixTransform* matrixTransform; matrixTransform = new osg::MatrixTransform(matrix); matrixTransform->setName("positionStaticObject"); matrixTransform->setDataVariance(osg::Object::STATIC); matrixTransform->addChild(node.get()); // Shift the models so they are centered on the center of the block. // We will place the object at the right position in the tile later. group->addChild(matrixTransform); } osgViewer::Viewer viewer; if (osg_optimizer || display_viewer) { // Create a viewer - required for some Optimizers and if we are to display // the results viewer.setSceneData(group.get()); viewer.addEventHandler(new osgViewer::StatsHandler); viewer.addEventHandler(new osgViewer::WindowSizeHandler); viewer.addEventHandler( new osgGA::StateSetManipulator( viewer.getCamera()->getOrCreateStateSet())); viewer.setCameraManipulator(new osgGA::TrackballManipulator()); viewer.realize(); } if (osg_optimizer) { // Run the Optimizer osgUtil::Optimizer optimizer; // See osgUtil::Optimizer for list of optimizations available. //optimizer.optimize(group, osgUtil::Optimizer::ALL_OPTIMIZATIONS); int optimizationOptions = osgUtil::Optimizer::FLATTEN_STATIC_TRANSFORMS | osgUtil::Optimizer::REMOVE_REDUNDANT_NODES | osgUtil::Optimizer::COMBINE_ADJACENT_LODS | osgUtil::Optimizer::SHARE_DUPLICATE_STATE | osgUtil::Optimizer::MERGE_GEOMETRY | osgUtil::Optimizer::MAKE_FAST_GEOMETRY | osgUtil::Optimizer::SPATIALIZE_GROUPS | osgUtil::Optimizer::OPTIMIZE_TEXTURE_SETTINGS | osgUtil::Optimizer::TEXTURE_ATLAS_BUILDER | osgUtil::Optimizer::CHECK_GEOMETRY | osgUtil::Optimizer::STATIC_OBJECT_DETECTION; optimizer.optimize(group, optimizationOptions); } // Serialize the result as a binary OSG file, including textures: std::string filename = sourcestg.file(); // Include both the STG name and the indexes for uniqueness. // ostringstream required for compilers that don't support C++11 std::ostringstream oss; oss << x << y; filename.append(oss.str()); filename.append(".osg"); SGPath osgpath = SGPath(deststg.dir(), filename); osgDB::writeNodeFile(*group, osgpath.c_str(), new osgDB::Options("WriteImageHint=IncludeData Compressor=zlib")); // Write out the required STG entry for this merged set of objects, centered // on the center of the tile. stgout << "OBJECT_STATIC " << osgpath.file() << " " << center.getLongitudeDeg() << " " << center.getLatitudeDeg() << " 0.0 0.0 0.0 0.0\n"; if (display_viewer) { viewer.run(); } } } if (copy_files) { std::vector::const_iterator iter; for (iter = ac_files.begin(); iter != ac_files.end(); ++iter) { SGPath acfile = SGPath(deststg.dir()); acfile.append(*iter); if (acfile.isFile()) { if (remove(acfile.c_str()) != 0) { SG_LOG(SG_GENERAL, SG_ALERT, "Unable to remove " << acfile.c_str()); } } } } // Finished with this file. stgout.flush(); stgout.close(); return EXIT_SUCCESS; } int processDirectory(osg::ref_ptr options, SGPath directory_name) { // List of STG files to process std::vector stg_files; SGPath source = SGPath(input); source.append(directory_name.c_str()); SG_LOG(SG_GENERAL, SG_INFO, "Processing " << source.c_str()); DIR *dir = NULL; dir = opendir(source.c_str()); if (dir == NULL) { SG_LOG(SG_GENERAL, SG_ALERT, "Unable to open " << source.c_str()); return EXIT_FAILURE; } if (copy_files) { // Create the directory in the output directory SGPath destination = SGPath(output); destination.append(directory_name.c_str()); destination.append("."); // Do this so the path is a directory rather than a file! if (destination.exists()) { if (! destination.isDir()) { SG_LOG(SG_GENERAL, SG_ALERT, destination.c_str() << " exists and is not a directory."); return EXIT_FAILURE; } if (! destination.canWrite()) { SG_LOG(SG_GENERAL, SG_ALERT, destination.c_str() << " cannot be written."); return EXIT_FAILURE; } } else { // Note that this create the directory part of the path, int success = destination.create_dir(); if (success != 0) { SG_LOG(SG_GENERAL, SG_ALERT, "Unable to create " << destination.dir()); return EXIT_FAILURE; } if (! destination.exists()) { SG_LOG(SG_GENERAL, SG_ALERT, "Failed to create " << destination.dir()); return EXIT_FAILURE; } } } struct dirent *pent = NULL; pent = readdir(dir); while (pent != NULL) { std::string fname = pent->d_name; SG_LOG(SG_GENERAL, SG_DEBUG, " checking " << fname.c_str()); if ((fname.compare(".") == 0) || (fname.compare("..") == 0)) { pent = readdir(dir); continue; } SGPath fpath = SGPath(input); fpath.append(directory_name.c_str()); fpath.append(fname); if (fpath.isDir()) { SGPath dirname = SGPath(directory_name); dirname.append(fname); if (processDirectory(options, dirname) == EXIT_FAILURE) { return EXIT_FAILURE; } } else if (SGPath(fname).lower_extension() == "stg") { stg_files.push_back(fname); } else if (copy_files) { // Copy it over if we're copying all files. SGPath fsource = SGPath(input); fsource.append(directory_name.c_str()); fsource.append(fname); SGPath fdestination = SGPath(output); fdestination.append(directory_name.c_str()); fdestination.append(fname); SG_LOG(SG_GENERAL, SG_DEBUG, "Copying " << fsource.c_str() << " to " << fdestination.c_str()); std::ifstream src(fsource.c_str(), std::ios::binary); std::ofstream dst(fdestination.c_str(), std::ios::binary); dst << src.rdbuf(); } pent = readdir(dir); } closedir(dir); // Now we've copied the data, process the STG files std::vector::const_iterator iter; for (iter = stg_files.begin(); iter != stg_files.end(); ++iter) { SGPath stg = SGPath(directory_name); stg.append(*iter); processSTG(options, stg.c_str()); } return EXIT_SUCCESS; } int main(int argc, char** argv) { osg::ApplicationUsage* usage = new osg::ApplicationUsage(); usage->setApplicationName("stgmerge"); usage->setCommandLineUsage( "Merge static model files within a given STG file."); usage->addCommandLineOption("--input ", "Scenery directory to read"); usage->addCommandLineOption("--output ", "Output directory for STGs and merged models"); usage->addCommandLineOption("--fg-root ", "FG root directory", "$FG_ROOT"); usage->addCommandLineOption("--fg-scenery ", "FG scenery path", "$FG_SCENERY"); usage->addCommandLineOption("--terrasync ", "Terrasync path (for Models)"); usage->addCommandLineOption("--group-size ", "Group size (m)", "5000"); usage->addCommandLineOption("--optimize", "Optimize scene-graph"); usage->addCommandLineOption("--viewer", "Display loaded objects"); usage->addCommandLineOption("--copy-files", "Copy all contents of input directory into output directory"); // use an ArgumentParser object to manage the program arguments. osg::ArgumentParser arguments(&argc, argv); arguments.setApplicationUsage(usage); if (arguments.read("--fg-root", fg_root)) { } else if (const char *fg_root_env = std::getenv("FG_ROOT")) { fg_root = fg_root_env; } else { fg_root = PKGLIBDIR; } if (arguments.read("--fg-scenery", fg_scenery)) { } else if (const char *fg_scenery_env = std::getenv("FG_SCENERY")) { fg_scenery = fg_scenery_env; } else { SGPath path(fg_root); path.append("Scenery"); fg_scenery = path.str(); } if (arguments.read("--terrasync", fg_terrasync_root)) { } else { fg_scenery = ""; } if (!arguments.read("--input", input)) { arguments.reportError("--input argument required."); } else { SGPath s(input); if (!s.isDir()) { arguments.reportError( "--input directory does not exist or is not directory."); } else if (!s.canRead()) { arguments.reportError( "--input directory cannot be read. Check permissions."); } } if (!arguments.read("--output", output)) { arguments.reportError("--output argument required."); } else { // Check directory exists, we can write to it, and we're not about to write // to the same location as the STG file. SGPath p(output); SGPath s(input); if (!p.isDir()) { arguments.reportError("--output directory does not exist."); } if (!p.canWrite()) { arguments.reportError( "--output directory is not writeable. Check permissions."); } if (s == p) { arguments.reportError( "--output directory must differ from STG directory."); } } if (arguments.read("--group-size")) { if (! arguments.read("--group-size", group_size)) { arguments.reportError( "--group-size argument number be a positive integer."); } } if (arguments.read("--viewer")) { display_viewer = true; } if (arguments.read("--optimize")) { osg_optimizer = true; } if (arguments.read("--copy-files")) { copy_files = true; } if (arguments.errors()) { arguments.writeErrorMessages(std::cout); arguments.getApplicationUsage()->write(std::cout, osg::ApplicationUsage::COMMAND_LINE_OPTION | osg::ApplicationUsage::ENVIRONMENTAL_VARIABLE, 80, true); return EXIT_FAILURE; } SGSharedPtr props = new SGPropertyNode; try { SGPath preferencesFile = fg_root; preferencesFile.append("defaults.xml"); readProperties(preferencesFile.str(), props); } catch (...) { // In case of an error, at least make summer :) props->getNode("sim/startup/season", true)->setStringValue("summer"); SG_LOG(SG_GENERAL, SG_ALERT, "Problems loading FlightGear preferences.\n" << "Probably FG_ROOT is not properly set."); } /// now set up the simgears required model stuff simgear::ResourceManager::instance()->addBasePath(fg_root, simgear::ResourceManager::PRIORITY_DEFAULT); // Just reference simgears reader writer stuff so that the globals get // pulled in by the linker ... simgear::ModelRegistry::instance(); sgUserDataInit(props.get()); SGMaterialLibPtr ml = new SGMaterialLib; SGPath mpath(fg_root); // TODO: Pick up correct materials.xml file. Urrgh - this can't be runtime dependent... mpath.append("Materials/default/materials.xml"); try { ml->load(fg_root, mpath.str(), props); } catch (...) { SG_LOG(SG_GENERAL, SG_ALERT, "Problems loading FlightGear materials.\n" << "Probably FG_ROOT is not properly set."); } simgear::SGModelLib::init(fg_root, props); // Set up the reader/writer options osg::ref_ptr options; if (osgDB::Options* ropt = osgDB::Registry::instance()->getOptions()) options = new simgear::SGReaderWriterOptions(*ropt); else options = new simgear::SGReaderWriterOptions; osgDB::convertStringPathIntoFilePathList(fg_scenery, options->getDatabasePathList()); options->setMaterialLib(ml); options->setPropertyNode(props); options->setPluginStringData("SimGear::FG_ROOT", fg_root); options->setPluginStringData("SimGear::TERRASYNC_ROOT", fg_terrasync_root); // Here, all arguments are processed arguments.reportRemainingOptionsAsUnrecognized(); arguments.writeErrorMessages(std::cerr); std::string dot = ""; return processDirectory(options, dot); }