/* * Copyright (C) 2018 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 #include #include #include #include #include #include #include #include #include "StaticMapPlugin.hh" namespace gazebo { /// \brief Class to provide helper functions for Web Mercator projection class MercatorProjection { /// \brief Convert point in world coordinates to latitude and longitude /// \param[in] _point Point in world coordinates /// \return Latitude and longitude coorindates public: static ignition::math::SphericalCoordinates PointToLatLon( const ignition::math::Vector2d &_point); /// \brief Convert latitdue and longitude to point in world coordinates /// \param[in] Latitude and longitude coorindates /// \return Point in world coordinates public: static ignition::math::Vector2d LatLonToPoint( const ignition::math::SphericalCoordinates &_latLon); /// \brief Google map base level tile size public: static const unsigned int TILE_SIZE; }; /// \brief Private data class for StaticMapPlugin class StaticMapPluginPrivate { /// \brief Download map tiles. /// \param[in] _centerLat Latitude of center point of map /// \param[in] _centerLon Longitude of center point of map /// \param[in] _zoom Map zoom level between 0 (entire world) and 21+ /// (streets). /// \param[in] _tileSizePx Size of each map tile in pixels. Tiles will be /// square. /// \param[in] _worldSize Size of map in the world in meters. /// \param[in] _mapType Type of map to download: roadmap, satellite, /// terrain, hybrid /// \param[in] _apiKey Google API key /// \param[in] _saveDirPath Location in local filesystem to save tile /// images. public: std::vector DownloadMapTiles(const double _centerLat, const double _centerLon, const unsigned int _zoom, const unsigned int _tileSizePx, const ignition::math::Vector2d &_worldSize, const std::string &_mapType, const std::string &_apiKey, const std::string &_saveDirPath); /// \brief Create textured map model and save it in specified path. /// \param[in] _name Name of map model /// \param[in] _tileWorldSize Size of map tiles in meters /// \param[in] _xNumTiles Number of tiles in x direction /// \param[in] _yNumTiles Number of tiles in y direction /// \param[in] _tiles Tile image filenames /// \param[in] _modelPath Path to model directory /// \return True if map tile model has been successfully created. public: bool CreateMapTileModel( const std::string &_name, const double _tileWorldSize, const unsigned int xNumTiles, const unsigned int yNumTiles, const std::vector &_tiles, const std::string &_modelPath); /// \brief Get the ground resolution at the specified latitude and zoom /// level. /// \param[in] _lat Latitude /// \param[in] _zoom Map zoom Level /// \return Ground resolution in meters per pixel. public: double GroundResolution(const double _lat, const unsigned int _zoom) const; /// \brief Spawn a model into the world /// \param[in] _name Name of model /// \param[in] _pose Pose of model public: void SpawnModel(const std::string &_name, const ignition::math::Pose3d &_pose); /// \brief Pointer to world. public: physics::WorldPtr world; /// \brief Name of map model public: std::string modelName; /// \brief Pose of map model public: ignition::math::Pose3d modelPose; /// \brief Latitude and Longitude of map center public: ignition::math::Vector2d center; /// \brief Target size of world to be covered by map in meters. public: ignition::math::Vector2d worldSize; /// \brief Map zoom level. From 0 (entire world) to 21+ (streets) public: unsigned int zoom = 21u; /// \brief Size of map tile in pixels. 640 is max resolution for users of /// standard API public: unsigned int tileSizePx = 640u; /// \brief Type of map to use as texture: roadmap, satellite, terrain, /// hybrid public: std::string mapType = "satellite"; /// \brief True to use cached model and image data from gazebo model path. /// False to redownload image tiles and recreate model sdf and config /// files. public: bool useCache = false; /// \brief Google API key public: std::string apiKey; /// \brief Filenames of map tile images public: std::vector mapTileFilenames; /// \brief Pointer to a node for communication. public: transport::NodePtr node; /// \brief Factory publisher. public: transport::PublisherPtr factoryPub; /// \brief True if the plugin is loaded successfully public: bool loaded = false; }; } using namespace gazebo; const unsigned int MercatorProjection::TILE_SIZE = 256; GZ_REGISTER_WORLD_PLUGIN(StaticMapPlugin) ///////////////////////////////////////////////// size_t WriteData(void *_ptr, size_t _size, size_t _nmemb, FILE *_stream) { return fwrite(_ptr, _size, _nmemb, _stream); } ///////////////////////////////////////////////// bool DownloadFile(const std::string &_url, const std::string &_outputFile) { if (_url.empty()) return false; CURL *curl = curl_easy_init(); curl_easy_setopt(curl, CURLOPT_URL, _url.c_str()); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteData); FILE *fp = fopen(_outputFile.c_str(), "wb"); if (!fp) { gzerr << "Could not download model[" << _url << "] because we were" << "unable to write to file[" << _outputFile << "]." << "Please fix file permissions."; return false; } curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); char errbuf[CURL_ERROR_SIZE]; // provide a buffer to store errors in curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf); // set the error buffer as empty before performing a request errbuf[0] = 0; CURLcode success = curl_easy_perform(curl); if (success != CURLE_OK) { gzerr << "Error in REST request" << std::endl; size_t len = strlen(errbuf); fprintf(stderr, "\nlibcurl: (%d) ", success); if (len) { fprintf(stderr, "%s%s", errbuf, ((errbuf[len - 1] != '\n') ? "\n" : "")); } else fprintf(stderr, "%s\n", curl_easy_strerror(success)); } fclose(fp); // Update the status code. int statusCode = 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &statusCode); // Cleaning. curl_easy_cleanup(curl); return true; } ///////////////////////////////////////////////// ignition::math::Vector2d MercatorProjection::LatLonToPoint( const ignition::math::SphericalCoordinates &_latLon) { // Adapted from: // https://developers.google.com/maps/documentation/javascript/examples/map-coordinates ignition::math::Vector2d point; // Truncating to 0.9999 effectively limits latitude to 89.189. This is // about a third of a tile past the edge of the world tile. double siny = std::min(std::max(std::sin( _latLon.LatitudeReference().Radian()), -0.9999), 0.9999); point.X() = TILE_SIZE * (0.5 + _latLon.LongitudeReference().Degree()/ 360.0); point.Y() = TILE_SIZE * (0.5 - std::log((1 + siny) / (1 - siny)) / (4 * IGN_PI)); return point; } ///////////////////////////////////////////////// ignition::math::SphericalCoordinates MercatorProjection::PointToLatLon( const ignition::math::Vector2d &_point) { ignition::math::SphericalCoordinates latLon; ignition::math::Angle lonAngle; ignition::math::Angle latAngle; double lonDegrees = (_point.X() / TILE_SIZE - 0.5) * 360; double latRadians = (_point.Y() - TILE_SIZE/2.0) / -(TILE_SIZE/(2*IGN_PI)); lonAngle.Degree(lonDegrees); latAngle.Radian(2 * std::atan(std::exp(latRadians)) - IGN_PI / 2.0); latLon.SetLongitudeReference(lonAngle); latLon.SetLatitudeReference(latAngle); return latLon; } ///////////////////////////////////////////////// StaticMapPlugin::StaticMapPlugin() : dataPtr(new StaticMapPluginPrivate) { } ///////////////////////////////////////////////// void StaticMapPlugin::Load(physics::WorldPtr _world, sdf::ElementPtr _sdf) { this->dataPtr->world = _world; if (!_sdf->HasElement("api_key")) { gzerr << "Missing Google API key needed to download map tiles" << std::endl; return; } if (!_sdf->HasElement("center")) { gzerr << "Please specify latitude and longitude coordinates of map center" << std::endl; return; } if (!_sdf->HasElement("world_size")) { gzerr << "Please specify size of map to cover in meters" << std::endl; return; } this->dataPtr->apiKey = _sdf->Get("api_key"); this->dataPtr->center = _sdf->Get("center"); double wSize = _sdf->Get("world_size"); if (wSize > 0) { // support only square map for now this->dataPtr->worldSize.X() = wSize; this->dataPtr->worldSize.Y() = wSize; } else { gzerr << "World size must be greater than 0 meters" << std::endl; return; } // optional params if (_sdf->HasElement("zoom")) this->dataPtr->zoom = _sdf->Get("zoom"); if (_sdf->HasElement("tile_size")) { this->dataPtr->tileSizePx = _sdf->Get("tile_size"); if (this->dataPtr->tileSizePx > 640u) { gzerr << "Tile size exceeds standard API usage limit. Setting to 640px." << std::endl; this->dataPtr->tileSizePx = 640u; } } if (_sdf->HasElement("map_type")) this->dataPtr->mapType= _sdf->Get("map_type"); if (_sdf->HasElement("use_cache")) this->dataPtr->useCache = _sdf->Get("use_cache"); if (_sdf->HasElement("pose")) this->dataPtr->modelPose = _sdf->Get("pose"); if (_sdf->HasElement("model_name")) this->dataPtr->modelName = _sdf->Get("model_name"); else { // generate name based on input std::stringstream name; name << "map_" << this->dataPtr->mapType << "_" << std::setprecision(9) << this->dataPtr->center.X() << "_" << this->dataPtr->center.Y() << "_" << this->dataPtr->worldSize.X() << "_" << this->dataPtr->worldSize.Y(); this->dataPtr->modelName = name.str(); } this->dataPtr->loaded = true; } ///////////////////////////////////////////////// void StaticMapPlugin::Init() { // don't init if params are not loaded successfully if (!this->dataPtr->loaded) return; // check if model exists locally auto basePath = common::SystemPaths::Instance()->GetLogPath() / boost::filesystem::path("models"); this->dataPtr->node = transport::NodePtr(new transport::Node()); this->dataPtr->node->Init(); this->dataPtr->factoryPub = this->dataPtr->node->Advertise("~/factory"); boost::filesystem::path modelPath = basePath / this->dataPtr->modelName; if (this->dataPtr->useCache && common::exists(modelPath.string())) { gzmsg << "Model: '" << this->dataPtr->modelName << "' exists. " << "Spawning existing model.." << std::endl; this->dataPtr->SpawnModel("model://" + this->dataPtr->modelName, this->dataPtr->modelPose); return; } // create tmp dir to save model files boost::filesystem::path tmpModelPath = boost::filesystem::temp_directory_path() / this->dataPtr->modelName; boost::filesystem::path scriptsPath(tmpModelPath / "materials" / "scripts"); boost::filesystem::create_directories(scriptsPath); boost::filesystem::path texturesPath(tmpModelPath / "materials" / "textures"); boost::filesystem::create_directories(texturesPath); // download map tile images into model/materials/textures std::vector tiles = this->dataPtr->DownloadMapTiles( this->dataPtr->center.X(), this->dataPtr->center.Y(), this->dataPtr->zoom, this->dataPtr->tileSizePx, this->dataPtr->worldSize, this->dataPtr->mapType, this->dataPtr->apiKey, texturesPath.string()); // assume square model for now unsigned int xNumTiles = std::sqrt(tiles.size()); unsigned int yNumTiles = xNumTiles; double tileWorldSize = this->dataPtr->GroundResolution( IGN_DTOR(this->dataPtr->center.X()), this->dataPtr->zoom) * this->dataPtr->tileSizePx; // create model and spawn it into the world if (this->dataPtr->CreateMapTileModel( this->dataPtr->modelName, tileWorldSize, xNumTiles, yNumTiles, tiles, tmpModelPath.string())) { // verify model dir is created if (common::exists(tmpModelPath.string())) { // remove existing map model if (common::exists(modelPath.string())) boost::filesystem::remove_all(modelPath); // move new map model to gazebo model path boost::filesystem::rename(tmpModelPath, modelPath); // spawn the model this->dataPtr->SpawnModel("model://" + this->dataPtr->modelName, this->dataPtr->modelPose); } else gzerr << "Failed to create model: " << tmpModelPath.string() << std::endl; } } ///////////////////////////////////////////////// double StaticMapPluginPrivate::GroundResolution(const double _lat, const unsigned int _zoom) const { double earthEquatorialRadius = 6378137; double metersPerPx = 2 * IGN_PI * earthEquatorialRadius * std::cos(_lat) / (MercatorProjection::TILE_SIZE * std::pow(2, _zoom)); return metersPerPx; } ///////////////////////////////////////////////// std::vector StaticMapPluginPrivate::DownloadMapTiles( const double _centerLat, const double _centerLon, const unsigned int _zoom, const unsigned int _tileSizePx, const ignition::math::Vector2d &_worldSize, const std::string &_mapType, const std::string &_apiKey, const std::string &_saveDirPath) { ignition::math::Angle lonAngle; ignition::math::Angle latAngle; latAngle.Degree(_centerLat); lonAngle.Degree(_centerLon); ignition::math::SphericalCoordinates centerLatLon; centerLatLon.SetLatitudeReference(latAngle); centerLatLon.SetLongitudeReference(lonAngle); // ground resolution - varies by latitude and zoom level double metersPerPx = this->GroundResolution(centerLatLon.LatitudeReference().Radian(), _zoom); // determine number of tiles necessary to cover specified world size unsigned int xNumTiles = static_cast( std::ceil(_worldSize.X() / metersPerPx / _tileSizePx)); // y is only approximate because ground resolution is based on latitude unsigned int yNumTiles = static_cast( std::ceil(_worldSize.Y() / metersPerPx / _tileSizePx)); // scale for converting between pixel and world point double scale = std::pow(2, _zoom); ignition::math::Vector2d centerPx = MercatorProjection::LatLonToPoint(centerLatLon) * scale; // compute starting x, y values in pixel coordinates double halfWidthPx = _tileSizePx * std::floor(xNumTiles / 2); double halfHeightPx = _tileSizePx * std::floor(yNumTiles / 2); double halfTileSize = _tileSizePx / 2.0; double x = centerPx.X() - halfWidthPx; double y = centerPx.Y() - halfHeightPx; if (xNumTiles % 2 == 0u) x += halfTileSize; if (yNumTiles % 2 == 0u) y += halfTileSize; double startx = x; // download map tiles using google static map API std::string url = "https://maps.googleapis.com/maps/api/staticmap"; for (unsigned int i = 0; i < yNumTiles; ++i) { for (unsigned int j = 0; j < xNumTiles; ++j) { // convert from pixels to world point auto px = ignition::math::Vector2d(x, y); auto point = px / scale; // convert world point to lat lon auto latLon = MercatorProjection::PointToLatLon(point); // download tile image std::stringstream query; query << "?center=" << std::setprecision(9) << latLon.LatitudeReference().Degree() << "," << latLon.LongitudeReference().Degree() << "&zoom=" << _zoom << "&size=" << _tileSizePx << "x" << _tileSizePx << "&maptype=" << _mapType << "&key=" << _apiKey; std::string fullURL = url + query.str(); std::stringstream filename; filename << "tile_" << std::setprecision(9) << latLon.LatitudeReference().Degree() << "_" << latLon.LongitudeReference().Degree() << ".png"; std::string fullPath = _saveDirPath + "/" + filename.str(); DownloadFile(fullURL, fullPath); gzmsg << "Downloading map tile: " << filename.str() << std::endl; mapTileFilenames.push_back(filename.str()); x += _tileSizePx; } x = startx; y += _tileSizePx; } return mapTileFilenames; } ///////////////////////////////////////////////// bool StaticMapPluginPrivate::CreateMapTileModel( const std::string &_name, const double _tileWorldSize, const unsigned int _xNumTiles, const unsigned int _yNumTiles, const std::vector &_tiles, const std::string &_modelPath) { // create material script std::stringstream materialScriptStr; for (unsigned int i = 0; i < _yNumTiles; ++i) { for (unsigned int j = 0; j < _xNumTiles; ++j) { materialScriptStr << "material " << _name << "/" << i << "_" << j << "\n" "{\n" " technique\n" " {\n" " pass\n" " {\n" " texture_unit\n" " {\n" " texture " << _tiles[j + i * _xNumTiles] << "\n" " }\n" " }\n" " }\n" "}\n\n"; } } // save material script file to disk boost::filesystem::path scriptFilePath(_modelPath); scriptFilePath = scriptFilePath / "materials" / "scripts" / "map_tiles.material"; std::ofstream scriptFile; scriptFile.open(scriptFilePath.string().c_str()); if (!scriptFile.is_open()) { gzerr << "Couldn't open file for writing: " << scriptFilePath.string() << std::endl; return false; } scriptFile << materialScriptStr.str(); scriptFile.close(); // create model.sdf file double sizeX = _tileWorldSize; double sizeY = _tileWorldSize; double sizeZ = 1.0; ignition::math::Vector2d wCenter(0, 0); double halfTileWorldWidth = sizeX * std::floor(_xNumTiles / 2); double halfTileWorldHeight = sizeY * std::floor(_yNumTiles / 2); double x = wCenter.X() - halfTileWorldWidth; double y = wCenter.Y() + halfTileWorldHeight; double halfTileWorldSizeX = sizeX / 2.0; double halfTileWorldSizeY = sizeY / 2.0; if (_xNumTiles % 2 == 0u) x += halfTileWorldSizeX; if (_yNumTiles % 2 == 0u) y -= halfTileWorldSizeY; double startx = x; double zPos = -sizeZ / 2.0; // rotate around z to line up textures ignition::math::Vector3d tileRot(0, 0, IGN_PI / 2.0); ignition::math::Vector3d colSize(sizeX * _xNumTiles, sizeY * _yNumTiles, sizeZ); // Model will have boxed-shaped tiles with z size of 1.0 // Surface of model will be at z=0.0 std::stringstream newModelStr; newModelStr << "\n" "\n" " true\n" " \n" " \n" " 0 0 " << zPos << " 0 0 0\n" " \n" " \n" " " << colSize << "\n" " \n" " \n" " \n"; for (unsigned int i = 0; i < _yNumTiles; ++i) { for (unsigned int j = 0; j < _xNumTiles; ++j) { newModelStr << " \n" " " << x << " " << y << " " << zPos << " " << tileRot << "\n" " \n" " \n" " " << sizeX << " " << sizeY << " " << sizeZ << "\n" " \n" " \n" " \n" " \n" " \n" " \n"; x += sizeX; } x = startx; y -= sizeY; } newModelStr << " \n" "\n" ""; // save model.sdf file to disk boost::filesystem::path modelSDFFilePath(_modelPath); modelSDFFilePath /= "model.sdf"; std::ofstream modelSDFFile; modelSDFFile.open(modelSDFFilePath.string().c_str()); if (!modelSDFFile.is_open()) { gzerr << "Couldn't open file for writing: " << modelSDFFilePath.string() << std::endl; return false; } modelSDFFile << newModelStr.str(); modelSDFFile.close(); // create model.config file std::ostringstream modelConfigStr; modelConfigStr << "\n" << "\n" << " " << _name << "\n" << " 1.0\n" << " model.sdf\n" << " \n" << " gazebo\n" << " \n" << " \n" << " \n" << " Made with Gazebo using Google Static Map API. " << "https://developers.google.com/maps/documentation/static-maps\n" << " \n" << ""; // save model.config file to disk boost::filesystem::path modelConfigFilePath(_modelPath); modelConfigFilePath /= "model.config"; std::ofstream modelConfigFile; modelConfigFile.open(modelConfigFilePath.string().c_str()); if (!modelConfigFile.is_open()) { gzerr << "Couldn't open file for writing: " << modelConfigFilePath.string() << std::endl; return false; } modelConfigFile << modelConfigStr.str(); modelConfigFile.close(); return true; } ///////////////////////////////////////////////// void StaticMapPluginPrivate::SpawnModel(const std::string &_uri, const ignition::math::Pose3d &_pose) { // publish to factory topic to spawn the model msgs::Factory msg; msg.set_sdf_filename(_uri); msgs::Set(msg.mutable_pose(), _pose); this->factoryPub->Publish(msg); }