677 lines
19 KiB
C++
677 lines
19 KiB
C++
/*
|
|
* Copyright (C) 2012 Open Source Robotics Foundation
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*
|
|
*/
|
|
|
|
#include <tinyxml.h>
|
|
#ifndef _WIN32
|
|
#include <libtar.h>
|
|
#endif
|
|
#include <curl/curl.h>
|
|
#include <fcntl.h>
|
|
#include <sys/stat.h>
|
|
#include <iostream>
|
|
|
|
#include <boost/algorithm/string/replace.hpp>
|
|
#include <boost/bind.hpp>
|
|
#include <boost/filesystem.hpp>
|
|
#include <boost/function.hpp>
|
|
#include <boost/iostreams/filtering_streambuf.hpp>
|
|
#include <boost/iostreams/copy.hpp>
|
|
#include <boost/iostreams/filter/gzip.hpp>
|
|
|
|
#include <sdf/sdf.hh>
|
|
|
|
#include "gazebo/common/Time.hh"
|
|
#include "gazebo/common/SystemPaths.hh"
|
|
#include "gazebo/common/Console.hh"
|
|
#include "gazebo/common/ModelDatabasePrivate.hh"
|
|
#include "gazebo/common/ModelDatabase.hh"
|
|
#include "gazebo/common/SemanticVersion.hh"
|
|
|
|
using namespace gazebo;
|
|
using namespace common;
|
|
|
|
ModelDatabase *ModelDatabase::myself = ModelDatabase::Instance();
|
|
|
|
/////////////////////////////////////////////////
|
|
size_t write_data(void *ptr, size_t size, size_t nmemb, FILE *stream)
|
|
{
|
|
size_t written;
|
|
written = fwrite(ptr, size, nmemb, stream);
|
|
return written;
|
|
}
|
|
|
|
/////////////////////////////////////////////////
|
|
size_t get_models_cb(void *_buffer, size_t _size, size_t _nmemb, void *_userp)
|
|
{
|
|
std::string *str = static_cast<std::string*>(_userp);
|
|
_size *= _nmemb;
|
|
|
|
// Append the new character data to the string
|
|
str->append(static_cast<const char*>(_buffer), _size);
|
|
return _size;
|
|
}
|
|
|
|
/////////////////////////////////////////////////
|
|
ModelDatabase::ModelDatabase()
|
|
: dataPtr(new ModelDatabasePrivate)
|
|
{
|
|
this->dataPtr->updateCacheThread = NULL;
|
|
this->Start();
|
|
}
|
|
|
|
/////////////////////////////////////////////////
|
|
ModelDatabase::~ModelDatabase()
|
|
{
|
|
this->Fini();
|
|
delete this->dataPtr;
|
|
this->dataPtr = NULL;
|
|
}
|
|
|
|
/////////////////////////////////////////////////
|
|
void ModelDatabase::Start(bool _fetchImmediately)
|
|
{
|
|
boost::recursive_mutex::scoped_lock lock(this->dataPtr->startCacheMutex);
|
|
|
|
if (!this->dataPtr->updateCacheThread)
|
|
{
|
|
this->dataPtr->stop = false;
|
|
|
|
// Create the thread that is used to update the model cache. This
|
|
// retreives online data in the background to improve startup times.
|
|
this->dataPtr->updateCacheThread = new boost::thread(
|
|
boost::bind(&ModelDatabase::UpdateModelCache, this, _fetchImmediately));
|
|
}
|
|
}
|
|
|
|
/////////////////////////////////////////////////
|
|
void ModelDatabase::Fini()
|
|
{
|
|
// Stop the update thread.
|
|
this->dataPtr->stop = true;
|
|
this->dataPtr->updateCacheCompleteCondition.notify_all();
|
|
this->dataPtr->updateCacheCondition.notify_all();
|
|
|
|
{
|
|
boost::recursive_mutex::scoped_lock lock(this->dataPtr->startCacheMutex);
|
|
if (this->dataPtr->updateCacheThread)
|
|
this->dataPtr->updateCacheThread->join();
|
|
delete this->dataPtr->updateCacheThread;
|
|
this->dataPtr->updateCacheThread = NULL;
|
|
}
|
|
}
|
|
|
|
/////////////////////////////////////////////////
|
|
std::string ModelDatabase::GetURI()
|
|
{
|
|
std::string result;
|
|
char *uriStr = getenv("GAZEBO_MODEL_DATABASE_URI");
|
|
if (uriStr)
|
|
result = uriStr;
|
|
else
|
|
{
|
|
// No env var. Take compile-time default.
|
|
result = GAZEBO_MODEL_DATABASE_URI;
|
|
}
|
|
|
|
if (result[result.size()-1] != '/')
|
|
result += '/';
|
|
|
|
return result;
|
|
}
|
|
|
|
/////////////////////////////////////////////////
|
|
bool ModelDatabase::HasModel(const std::string &_modelURI)
|
|
{
|
|
std::string uri = _modelURI;
|
|
|
|
size_t uriSeparator = uri.find("://");
|
|
|
|
// Make sure there is a URI separator
|
|
if (uriSeparator == std::string::npos)
|
|
{
|
|
gzerr << "No URI separator \"://\" in [" << _modelURI << "]\n";
|
|
return false;
|
|
}
|
|
|
|
boost::replace_first(uri, "model://", ModelDatabase::GetURI());
|
|
uri = uri.substr(0, uri.find("/", ModelDatabase::GetURI().size()));
|
|
|
|
std::map<std::string, std::string> models = ModelDatabase::GetModels();
|
|
|
|
for (std::map<std::string, std::string>::iterator iter = models.begin();
|
|
iter != models.end(); ++iter)
|
|
{
|
|
if (iter->first == uri)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/////////////////////////////////////////////////
|
|
std::string ModelDatabase::GetDBConfig(const std::string &_uri)
|
|
{
|
|
std::string xmlString;
|
|
std::string uri = _uri;
|
|
boost::replace_first(uri, "model://", ModelDatabase::GetURI());
|
|
|
|
if (!uri.empty())
|
|
{
|
|
std::string manifestURI = uri + "/" + GZ_MODEL_DB_MANIFEST_FILENAME;
|
|
xmlString = this->GetManifestImpl(manifestURI);
|
|
}
|
|
|
|
return xmlString;
|
|
}
|
|
|
|
/////////////////////////////////////////////////
|
|
std::string ModelDatabase::GetModelConfig(const std::string &_uri)
|
|
{
|
|
std::string xmlString;
|
|
std::string uri = _uri;
|
|
boost::replace_first(uri, "model://", ModelDatabase::GetURI());
|
|
|
|
if (!uri.empty())
|
|
{
|
|
std::string manifestURI = uri + "/" + GZ_MODEL_MANIFEST_FILENAME;
|
|
xmlString = this->GetManifestImpl(manifestURI);
|
|
}
|
|
|
|
return xmlString;
|
|
}
|
|
|
|
/////////////////////////////////////////////////
|
|
std::string ModelDatabase::GetManifestImpl(const std::string &_uri)
|
|
{
|
|
std::string xmlString;
|
|
if (!_uri.empty())
|
|
{
|
|
CURL *curl = curl_easy_init();
|
|
curl_easy_setopt(curl, CURLOPT_URL, _uri.c_str());
|
|
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, get_models_cb);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &xmlString);
|
|
|
|
CURLcode success = curl_easy_perform(curl);
|
|
if (success != CURLE_OK)
|
|
{
|
|
gzwarn << "Unable to connect to model database using [" << _uri
|
|
<< "]. Only locally installed models will be available.\n";
|
|
}
|
|
|
|
curl_easy_cleanup(curl);
|
|
}
|
|
|
|
return xmlString;
|
|
}
|
|
|
|
/////////////////////////////////////////////////
|
|
bool ModelDatabase::UpdateModelCacheImpl()
|
|
{
|
|
std::string xmlString = ModelDatabase::GetDBConfig(ModelDatabase::GetURI());
|
|
|
|
if (!xmlString.empty())
|
|
{
|
|
TiXmlDocument xmlDoc;
|
|
xmlDoc.Parse(xmlString.c_str());
|
|
|
|
TiXmlElement *databaseElem = xmlDoc.FirstChildElement("database");
|
|
if (!databaseElem)
|
|
{
|
|
gzerr << "No <database> tag in the model database "
|
|
<< GZ_MODEL_DB_MANIFEST_FILENAME << " found"
|
|
<< " here[" << ModelDatabase::GetURI() << "]\n";
|
|
return false;
|
|
}
|
|
|
|
TiXmlElement *modelsElem = databaseElem->FirstChildElement("models");
|
|
if (!modelsElem)
|
|
{
|
|
gzerr << "No <models> tag in the model database "
|
|
<< GZ_MODEL_DB_MANIFEST_FILENAME << " found"
|
|
<< " here[" << ModelDatabase::GetURI() << "]\n";
|
|
return false;
|
|
}
|
|
|
|
TiXmlElement *uriElem;
|
|
for (uriElem = modelsElem->FirstChildElement("uri");
|
|
uriElem != NULL && !this->dataPtr->stop;
|
|
uriElem = uriElem->NextSiblingElement("uri"))
|
|
{
|
|
std::string uri = uriElem->GetText();
|
|
|
|
size_t index = uri.find("://");
|
|
std::string suffix = uri;
|
|
if (index != std::string::npos)
|
|
{
|
|
suffix = uri.substr(index + 3, uri.size() - index - 3);
|
|
}
|
|
|
|
std::string fullURI = ModelDatabase::GetURI() + suffix;
|
|
std::string modelName = ModelDatabase::GetModelName(fullURI);
|
|
|
|
this->dataPtr->modelCache[fullURI] = modelName;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/////////////////////////////////////////////////
|
|
void ModelDatabase::UpdateModelCache(bool _fetchImmediately)
|
|
{
|
|
boost::mutex::scoped_lock lock(this->dataPtr->updateMutex);
|
|
|
|
// Continually update the model cache when requested.
|
|
while (!this->dataPtr->stop)
|
|
{
|
|
// Wait for an update request.
|
|
if (!_fetchImmediately)
|
|
this->dataPtr->updateCacheCondition.wait(lock);
|
|
else
|
|
_fetchImmediately = false;
|
|
|
|
// Exit if notified and stopped.
|
|
if (this->dataPtr->stop)
|
|
break;
|
|
|
|
// Update the model cache.
|
|
if (!this->UpdateModelCacheImpl())
|
|
gzerr << "Unable to download model manifests\n";
|
|
else
|
|
{
|
|
boost::mutex::scoped_lock lock2(this->dataPtr->callbacksMutex);
|
|
if (this->dataPtr->stop)
|
|
break;
|
|
this->dataPtr->modelDBUpdated(this->dataPtr->modelCache);
|
|
}
|
|
this->dataPtr->updateCacheCompleteCondition.notify_all();
|
|
}
|
|
|
|
// Make sure no one is waiting on us.
|
|
this->dataPtr->updateCacheCompleteCondition.notify_all();
|
|
}
|
|
|
|
/////////////////////////////////////////////////
|
|
std::map<std::string, std::string> ModelDatabase::GetModels()
|
|
{
|
|
size_t size = 0;
|
|
|
|
{
|
|
boost::recursive_mutex::scoped_lock startLock(
|
|
this->dataPtr->startCacheMutex);
|
|
if (!this->dataPtr->updateCacheThread)
|
|
{
|
|
boost::mutex::scoped_lock lock(this->dataPtr->updateMutex);
|
|
this->Start(true);
|
|
this->dataPtr->updateCacheCompleteCondition.wait(lock);
|
|
}
|
|
else
|
|
{
|
|
boost::mutex::scoped_try_lock lock(this->dataPtr->updateMutex);
|
|
if (!lock)
|
|
{
|
|
gzmsg << "Waiting for model database update to complete...\n";
|
|
boost::mutex::scoped_lock lock2(this->dataPtr->updateMutex);
|
|
}
|
|
}
|
|
|
|
size = this->dataPtr->modelCache.size();
|
|
}
|
|
|
|
if (size != 0)
|
|
return this->dataPtr->modelCache;
|
|
else
|
|
{
|
|
gzwarn << "Getting models from[" << GetURI()
|
|
<< "]. This may take a few seconds.\n";
|
|
|
|
boost::mutex::scoped_lock lock(this->dataPtr->updateMutex);
|
|
|
|
// Tell the background thread to grab the models from online.
|
|
this->dataPtr->updateCacheCondition.notify_all();
|
|
|
|
// Wait for the thread to finish.
|
|
this->dataPtr->updateCacheCompleteCondition.wait(lock);
|
|
}
|
|
|
|
return this->dataPtr->modelCache;
|
|
}
|
|
|
|
/////////////////////////////////////////////////
|
|
event::ConnectionPtr ModelDatabase::GetModels(
|
|
boost::function<void (const std::map<std::string, std::string> &)> _func)
|
|
{
|
|
boost::mutex::scoped_lock lock2(this->dataPtr->callbacksMutex);
|
|
this->dataPtr->updateCacheCondition.notify_one();
|
|
return this->dataPtr->modelDBUpdated.Connect(_func);
|
|
}
|
|
|
|
/////////////////////////////////////////////////
|
|
std::string ModelDatabase::GetModelName(const std::string &_uri)
|
|
{
|
|
std::string result;
|
|
std::string xmlStr = ModelDatabase::GetModelConfig(_uri);
|
|
|
|
if (!xmlStr.empty())
|
|
{
|
|
TiXmlDocument xmlDoc;
|
|
if (xmlDoc.Parse(xmlStr.c_str()))
|
|
{
|
|
TiXmlElement *modelElem = xmlDoc.FirstChildElement("model");
|
|
if (modelElem)
|
|
{
|
|
TiXmlElement *nameElem = modelElem->FirstChildElement("name");
|
|
if (nameElem)
|
|
result = nameElem->GetText();
|
|
else
|
|
gzerr << "No <name> element in " << GZ_MODEL_MANIFEST_FILENAME
|
|
<< " for model[" << _uri << "]\n";
|
|
}
|
|
else
|
|
gzerr << "No <model> element in " << GZ_MODEL_MANIFEST_FILENAME
|
|
<< " for model[" << _uri << "]\n";
|
|
}
|
|
else
|
|
gzerr << "Unable to parse " << GZ_MODEL_MANIFEST_FILENAME
|
|
<< " for model[" << _uri << "]\n";
|
|
}
|
|
else
|
|
gzerr << "Unable to get model name[" << _uri << "]\n";
|
|
|
|
return result;
|
|
}
|
|
|
|
/////////////////////////////////////////////////
|
|
std::string ModelDatabase::GetModelPath(const std::string &_uri,
|
|
bool _forceDownload)
|
|
{
|
|
std::string path, suffix;
|
|
|
|
if (!_forceDownload)
|
|
path = SystemPaths::Instance()->FindFileURI(_uri);
|
|
|
|
struct stat st;
|
|
|
|
if (path.empty() || stat(path.c_str(), &st) != 0 )
|
|
{
|
|
if (!ModelDatabase::HasModel(_uri))
|
|
{
|
|
gzerr << "Unable to download model[" << _uri << "]\n";
|
|
return std::string();
|
|
}
|
|
|
|
// Get the model name from the uri
|
|
size_t startIndex = _uri.find_first_of("://");
|
|
if (startIndex == std::string::npos)
|
|
{
|
|
gzerr << "URI[" << _uri << "] is missing ://\n";
|
|
return std::string();
|
|
}
|
|
|
|
std::string modelName = _uri;
|
|
boost::replace_first(modelName, "model://", "");
|
|
boost::replace_first(modelName, ModelDatabase::GetURI(), "");
|
|
|
|
startIndex = modelName[0] == '/' ? 1 : 0;
|
|
size_t endIndex = modelName.find_first_of("/", startIndex);
|
|
size_t modelNameLen = endIndex == std::string::npos ? std::string::npos :
|
|
endIndex - startIndex;
|
|
|
|
if (endIndex != std::string::npos)
|
|
suffix = modelName.substr(endIndex, std::string::npos);
|
|
|
|
modelName = modelName.substr(startIndex, modelNameLen);
|
|
|
|
// Store downloaded .tar.gz and intermediate .tar files in temp location
|
|
boost::filesystem::path tmppath = boost::filesystem::temp_directory_path();
|
|
tmppath /= boost::filesystem::unique_path("gz_model-%%%%-%%%%-%%%%-%%%%");
|
|
std::string tarfilename = tmppath.string() + ".tar";
|
|
std::string tgzfilename = tarfilename + ".gz";
|
|
|
|
CURL *curl = curl_easy_init();
|
|
if (!curl)
|
|
{
|
|
gzerr << "Unable to initialize libcurl\n";
|
|
return std::string();
|
|
}
|
|
|
|
curl_easy_setopt(curl, CURLOPT_URL,
|
|
(ModelDatabase::GetURI() + "/" +
|
|
modelName + "/model.tar.gz").c_str());
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data);
|
|
|
|
bool retry = true;
|
|
int iterations = 0;
|
|
while (retry && iterations < 4)
|
|
{
|
|
retry = false;
|
|
iterations++;
|
|
|
|
FILE *fp = fopen(tgzfilename.c_str(), "wb");
|
|
if (!fp)
|
|
{
|
|
gzerr << "Could not download model[" << _uri << "] because we were"
|
|
<< "unable to write to file[" << tgzfilename << "]."
|
|
<< "Please fix file permissions.";
|
|
return std::string();
|
|
}
|
|
|
|
/// Download the model tarball
|
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
|
|
CURLcode success = curl_easy_perform(curl);
|
|
|
|
if (success != CURLE_OK)
|
|
{
|
|
gzwarn << "Unable to connect to model database using ["
|
|
<< _uri << "]\n";
|
|
retry = true;
|
|
continue;
|
|
}
|
|
|
|
fclose(fp);
|
|
|
|
try
|
|
{
|
|
// Unzip model tarball
|
|
std::ifstream file(tgzfilename.c_str(),
|
|
std::ios_base::in | std::ios_base::binary);
|
|
std::ofstream out(tarfilename.c_str(),
|
|
std::ios_base::out | std::ios_base::binary);
|
|
boost::iostreams::filtering_streambuf<boost::iostreams::input> in;
|
|
in.push(boost::iostreams::gzip_decompressor());
|
|
in.push(file);
|
|
boost::iostreams::copy(in, out);
|
|
}
|
|
catch(...)
|
|
{
|
|
gzerr << "Failed to unzip model tarball. Trying again...\n";
|
|
retry = true;
|
|
continue;
|
|
}
|
|
|
|
#ifndef _WIN32
|
|
TAR *tar;
|
|
tar_open(&tar, const_cast<char*>(tarfilename.c_str()),
|
|
NULL, O_RDONLY, 0644, TAR_GNU);
|
|
|
|
std::string outputPath = getenv("HOME");
|
|
outputPath += "/.gazebo/models";
|
|
|
|
tar_extract_all(tar, const_cast<char*>(outputPath.c_str()));
|
|
path = outputPath + "/" + modelName;
|
|
|
|
ModelDatabase::DownloadDependencies(path);
|
|
#endif
|
|
}
|
|
|
|
curl_easy_cleanup(curl);
|
|
if (retry)
|
|
{
|
|
gzerr << "Could not download model[" << _uri << "]."
|
|
<< "The model may be corrupt.\n";
|
|
path.clear();
|
|
}
|
|
|
|
// Clean up
|
|
try
|
|
{
|
|
boost::filesystem::remove(tarfilename);
|
|
boost::filesystem::remove(tgzfilename);
|
|
}
|
|
catch(...)
|
|
{
|
|
gzwarn << "Failed to remove temporary model files after download.";
|
|
}
|
|
}
|
|
|
|
return path + suffix;
|
|
}
|
|
|
|
/////////////////////////////////////////////////
|
|
void ModelDatabase::DownloadDependencies(const std::string &_path)
|
|
{
|
|
boost::filesystem::path manifestPath = _path;
|
|
|
|
// Get the GZ_MODEL_MANIFEST_FILENAME.
|
|
if (boost::filesystem::exists(manifestPath / GZ_MODEL_MANIFEST_FILENAME))
|
|
manifestPath /= GZ_MODEL_MANIFEST_FILENAME;
|
|
else
|
|
{
|
|
gzerr << "Missing " << GZ_MODEL_MANIFEST_FILENAME
|
|
<< " for model " << _path << "\n";
|
|
}
|
|
|
|
TiXmlDocument xmlDoc;
|
|
if (xmlDoc.LoadFile(manifestPath.string()))
|
|
{
|
|
TiXmlElement *modelXML = xmlDoc.FirstChildElement("model");
|
|
if (!modelXML)
|
|
{
|
|
gzerr << "No <model> element in manifest file[" << _path << "]\n";
|
|
return;
|
|
}
|
|
|
|
TiXmlElement *dependXML = modelXML->FirstChildElement("depend");
|
|
if (!dependXML)
|
|
return;
|
|
|
|
for (TiXmlElement *depXML = dependXML->FirstChildElement("model");
|
|
depXML; depXML = depXML->NextSiblingElement())
|
|
{
|
|
TiXmlElement *uriXML = depXML->FirstChildElement("uri");
|
|
if (uriXML)
|
|
{
|
|
// Download the model if it doesn't exist.
|
|
ModelDatabase::GetModelPath(uriXML->GetText());
|
|
}
|
|
else
|
|
{
|
|
gzerr << "Model depend is missing <uri> in manifest["
|
|
<< manifestPath << "]\n";
|
|
}
|
|
}
|
|
}
|
|
else
|
|
gzerr << "Unable to load manifest file[" << manifestPath << "]\n";
|
|
}
|
|
|
|
/////////////////////////////////////////////////
|
|
std::string ModelDatabase::GetModelFile(const std::string &_uri)
|
|
{
|
|
std::string result;
|
|
|
|
// This will download the model if necessary
|
|
std::string path = ModelDatabase::GetModelPath(_uri);
|
|
|
|
boost::filesystem::path manifestPath = path;
|
|
|
|
// Get the GZ_MODEL_MANIFEST_FILENAME.
|
|
if (boost::filesystem::exists(manifestPath / GZ_MODEL_MANIFEST_FILENAME))
|
|
manifestPath /= GZ_MODEL_MANIFEST_FILENAME;
|
|
else
|
|
{
|
|
gzerr << "Missing " << GZ_MODEL_MANIFEST_FILENAME
|
|
<< " for model " << manifestPath << "\n";
|
|
}
|
|
|
|
TiXmlDocument xmlDoc;
|
|
SemanticVersion sdfParserVersion(SDF_VERSION);
|
|
std::string bestVersionStr = "0.0";
|
|
if (xmlDoc.LoadFile(manifestPath.string()))
|
|
{
|
|
TiXmlElement *modelXML = xmlDoc.FirstChildElement("model");
|
|
if (modelXML)
|
|
{
|
|
TiXmlElement *sdfXML = modelXML->FirstChildElement("sdf");
|
|
TiXmlElement *sdfSearch = sdfXML;
|
|
|
|
// Find the SDF element that matches our current SDF version.
|
|
// If a match is not found, use the latest version of the element
|
|
// that is not older than the SDF parser.
|
|
while (sdfSearch)
|
|
{
|
|
if (sdfSearch->Attribute("version"))
|
|
{
|
|
std::string version = std::string(sdfSearch->Attribute("version"));
|
|
SemanticVersion modelVersion(version);
|
|
SemanticVersion bestVersion(bestVersionStr);
|
|
if (modelVersion > bestVersion)
|
|
{
|
|
// this model is better than the previous one
|
|
if (modelVersion <= sdfParserVersion)
|
|
{
|
|
// the parser can read it
|
|
sdfXML = sdfSearch;
|
|
bestVersionStr = version;
|
|
}
|
|
else
|
|
{
|
|
gzwarn << "Ignoring version " << version
|
|
<< " for model " << _uri
|
|
<< " because Gazebo is using an older sdf parser (version "
|
|
<< SDF_VERSION << ")" << std::endl;
|
|
}
|
|
}
|
|
}
|
|
sdfSearch = sdfSearch->NextSiblingElement("sdf");
|
|
}
|
|
|
|
if (sdfXML)
|
|
{
|
|
result = path + "/" + sdfXML->GetText();
|
|
}
|
|
else
|
|
{
|
|
gzerr << "Manifest[" << manifestPath << "] doesn't have "
|
|
<< "<model><sdf>...</sdf></model> element.\n";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
gzerr << "Manifest[" << manifestPath
|
|
<< "] doesn't have a <model> element\n";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
gzerr << "Invalid model manifest file[" << manifestPath << "]\n";
|
|
}
|
|
|
|
return result;
|
|
}
|