ppovb5fc7/gazebo/plugins/StaticMapPlugin.cc

692 lines
22 KiB
C++

/*
* 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 <curl/curl.h>
#include <boost/filesystem.hpp>
#include <ignition/math/Angle.hh>
#include <ignition/math/Helpers.hh>
#include <ignition/math/SphericalCoordinates.hh>
#include <ignition/math/Vector2.hh>
#include <gazebo/physics/physics.hh>
#include <gazebo/common/CommonIface.hh>
#include <gazebo/transport/Node.hh>
#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<std::string> 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<std::string> &_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<std::string> 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<std::string>("api_key");
this->dataPtr->center =
_sdf->Get<ignition::math::Vector2d>("center");
double wSize = _sdf->Get<double>("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<unsigned int>("zoom");
if (_sdf->HasElement("tile_size"))
{
this->dataPtr->tileSizePx = _sdf->Get<unsigned int>("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<std::string>("map_type");
if (_sdf->HasElement("use_cache"))
this->dataPtr->useCache = _sdf->Get<bool>("use_cache");
if (_sdf->HasElement("pose"))
this->dataPtr->modelPose = _sdf->Get<ignition::math::Pose3d>("pose");
if (_sdf->HasElement("model_name"))
this->dataPtr->modelName = _sdf->Get<std::string>("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<msgs::Factory>("~/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<std::string> 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<std::string> 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<unsigned int>(
std::ceil(_worldSize.X() / metersPerPx /
_tileSizePx));
// y is only approximate because ground resolution is based on latitude
unsigned int yNumTiles = static_cast<unsigned int>(
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<std::string> &_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 << "<sdf version='" << SDF_VERSION << "'>\n"
"<model name='" << _name << "'>\n"
" <static>true</static>\n"
" <link name='link'>\n"
" <collision name='collision'>\n"
" <pose>0 0 " << zPos << " 0 0 0</pose>\n"
" <geometry>\n"
" <box>\n"
" <size>" << colSize << "</size>\n"
" </box>\n"
" </geometry>\n"
" </collision>\n";
for (unsigned int i = 0; i < _yNumTiles; ++i)
{
for (unsigned int j = 0; j < _xNumTiles; ++j)
{
newModelStr <<
" <visual name='visual" << i << "_" << j <<"'>\n"
" <pose>" << x << " " << y << " " << zPos
<< " " << tileRot << "</pose>\n"
" <geometry>\n"
" <box>\n"
" <size>" << sizeX << " " << sizeY << " " << sizeZ
<< "</size>\n"
" </box>\n"
" </geometry>\n"
" <material>\n"
" <script>\n"
" <uri>model://" << _name << "/materials/scripts</uri>\n"
" <uri>model://" << _name << "/materials/textures</uri>\n"
" <name>" << _name << "/" << i << "_" << j << "</name>\n"
" </script>\n"
" </material>\n"
" </visual>\n";
x += sizeX;
}
x = startx;
y -= sizeY;
}
newModelStr <<
" </link>\n"
"</model>\n"
"</sdf>";
// 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 << "<?xml version=\"1.0\"?>\n"
<< "<model>\n"
<< " <name>" << _name << "</name>\n"
<< " <version>1.0</version>\n"
<< " <sdf version=\"" << SDF_VERSION << "\">model.sdf</sdf>\n"
<< " <author>\n"
<< " <name>gazebo</name>\n"
<< " <email></email>\n"
<< " </author>\n"
<< " <description>\n"
<< " Made with Gazebo using Google Static Map API. "
<< "https://developers.google.com/maps/documentation/static-maps\n"
<< " </description>\n"
<< "</model>";
// 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);
}