source: subversion/applications/utils/export/cgimap/src/main.cpp @ 17459

Last change on this file since 17459 was 15134, checked in by zere, 11 years ago

Added a cache of changesets/users so that no joins to the changesets or users tables are necessary in the map call.

File size: 7.2 KB
Line 
1#include <pqxx/pqxx>
2#include <iostream>
3#include <sstream>
4#include <boost/bind.hpp>
5#include <boost/ref.hpp>
6#include <boost/lambda/lambda.hpp>
7#include <boost/function.hpp>
8#include <cmath>
9#include <stdexcept>
10#include <vector>
11#include <map>
12#include <string>
13#include <fcgiapp.h>
14#include <memory>
15
16#include "bbox.hpp"
17#include "temp_tables.hpp"
18#include "writer.hpp"
19#include "split_tags.hpp"
20#include "map.hpp"
21#include "http.hpp"
22
23using std::runtime_error;
24using std::vector;
25using std::string;
26using std::map;
27using std::ostringstream;
28using std::auto_ptr;
29
30#define MAX_AREA 0.25
31#define CACHE_SIZE 1000
32
33/**
34 * Lookup a string from the FCGI environment. Throws 500 error if the
35 * string isn't there.
36 */
37string
38fcgi_get_env(FCGX_Request &req, const char* name) {
39  assert(name);
40  const char* v = FCGX_GetParam(name, req.envp);
41
42  // since the map script is so simple i'm just going to assume that
43  // any time we fail to get an environment variable is a fatal error.
44  if (v == NULL) {
45    ostringstream ostr;
46    ostr << "FCGI didn't set the $" << name << " environment variable.";
47    throw http::server_error(ostr.str());
48  }
49
50  return string(v);
51}
52
53/**
54 * Validates an FCGI request, returning the valid bounding box or
55 * throwing an error if there was no valid bounding box.
56 */
57bbox
58validate_request(FCGX_Request &request) {
59  // check that the REQUEST_METHOD is a GET
60  if (fcgi_get_env(request, "REQUEST_METHOD") != "GET") 
61    throw http::method_not_allowed("Only the GET method is supported for "
62                                   "map requests.");
63
64  const map<string, string> params = 
65    http::parse_params(fcgi_get_env(request, "QUERY_STRING"));
66  map<string, string>::const_iterator itr = params.find("bbox");
67
68  bbox bounds;
69  if ((itr == params.end()) ||
70      !bounds.parse(itr->second)) { 
71    throw http::bad_request("The parameter bbox is required, and must be "
72                            "of the form min_lon,min_lat,max_lon,max_lat.");
73  }
74
75  // clip the bounding box against the world
76  bounds.clip_to_world();
77
78  // check that the bounding box is within acceptable limits. these
79  // limits taken straight from the ruby map implementation.
80  if (!bounds.valid()) {
81    throw http::bad_request("The latitudes must be between -90 and 90, "
82                            "longitudes between -180 and 180 and the "
83                            "minima must be less than the maxima.");
84  }
85
86  if (bounds.area() > MAX_AREA) {
87    ostringstream ostr;
88    ostr << "The maximum bbox size is " << MAX_AREA << ", and your request "
89      "was too large. Either request a smaller area, or use planet.osm";
90    throw http::bad_request(ostr.str());
91  }
92
93  return bounds;
94}
95
96void
97respond_error(const http::exception &e, FCGX_Request &r) {
98  ostringstream ostr;
99  ostr << "Status: " << e.code() << " " << e.header() << "\r\n"
100       << "Content-type: text/html\r\n"
101       << "\r\n"
102       << "<html><head><title>" << e.header() << "</title></head>"
103       << "<body><p>" << e.what() << "</p></body></html>\n";
104 
105  FCGX_PutS(ostr.str().c_str(), r.out);
106}
107
108bool
109get_env(const char *k, string &s) {
110  char *v = getenv(k);
111  return v == NULL ? false : (s = v, true);
112}
113
114auto_ptr<pqxx::connection>
115connect_db() {
116  // get the DB parameters from the environment
117  string db_name, db_host, db_user, db_pass, db_charset;
118  if (!get_env("DB_NAME", db_name)) { 
119    throw runtime_error("$DB_NAME not set."); 
120  }
121  if (!get_env("DB_HOST", db_host)) { 
122    throw runtime_error("$DB_HOST not set."); 
123  }
124  if (!get_env("DB_USER", db_user)) { 
125    throw runtime_error("$DB_USER not set."); 
126  }
127  if (!get_env("DB_PASS", db_pass)) { 
128    throw runtime_error("$DB_PASS not set."); 
129  }
130  if (!get_env("DB_CHARSET", db_charset)) {
131    db_charset = "utf8";
132  }
133 
134  ostringstream ostr;
135  ostr << "dbname=" << db_name
136       << " host=" << db_host
137       << " user=" << db_user
138       << " password=" << db_pass;
139
140  // connect to the database.
141  auto_ptr<pqxx::connection> con(new pqxx::connection(ostr.str()));
142
143  // set the connections to use the appropriate charset
144  con->set_client_encoding(db_charset);
145
146  return con;
147}
148
149/**
150 * Bindings to allow libxml to write directly to the FCGI
151 * library.
152 */
153class fcgi_output_buffer
154  : public xml_writer::output_buffer {
155public:
156  virtual int write(const char *buffer, int len) {
157    return FCGX_PutStr(buffer, len, r.out);
158  }
159
160  virtual int close() {
161    // we don't actually close the FCGI output, as that happens
162    // automatically on the next call to accept.
163    return 0;
164  }
165
166  virtual ~fcgi_output_buffer() {
167  }
168
169  fcgi_output_buffer(FCGX_Request &req) 
170    : r(req) {
171  }
172
173private:
174  FCGX_Request &r;
175};
176
177int
178main() {
179  try {
180    // initialise FCGI
181    if (FCGX_Init() != 0) {
182      throw runtime_error("Couldn't initialise FCGX library.");
183    }
184
185    // get the parameters for the connection from the environment
186    // and connect to the database, throws exceptions if it fails.
187    auto_ptr<pqxx::connection> con = connect_db();
188    auto_ptr<pqxx::connection> cache_con = connect_db();
189
190    // create the request object for fcgi calls
191    FCGX_Request request;
192    if (FCGX_InitRequest(&request, 0, 0) != 0) {
193      throw runtime_error("Couldn't initialise FCGX request structure.");
194    }
195
196    // start a transaction using a second connection just for looking up
197    // users/changesets for the cache.
198    pqxx::work cache_x(*cache_con, "changeset_cache");
199    cache<long int, changeset> changeset_cache(boost::bind(fetch_changeset, boost::ref(cache_x), _1), CACHE_SIZE);
200
201    // enter the main loop
202    while(FCGX_Accept_r(&request) >= 0) {
203      try {
204        // read all the input data..?
205       
206        // validate the input
207        bbox bounds = validate_request(request);
208       
209        // separate transaction for the request
210        pqxx::work x(*con);
211
212        // create temporary tables of nodes, ways and relations which
213        // are in or used by elements in the bbox
214        tmp_nodes tn(x, bounds);
215        tmp_ways tw(x);
216
217        // write the response header
218        FCGX_PutS("Status: 200 OK\r\n"
219                  "Content-type: text/xml\r\n"
220                  "\r\n", request.out);
221       
222        // create the XML writer with the FCGI streams as output
223        fcgi_output_buffer out(request);
224        xml_writer writer(out, true);
225       
226        try {
227          // call to write the map call
228          write_map(x, writer, bounds, changeset_cache);
229
230        } catch (const xml_writer::write_error &e) {
231          // don't do anything - just go on to the next request.
232         
233        } catch (const std::exception &e) {
234          // errors here are unrecoverable (fatal to the request but maybe
235          // not fatal to the process) since we already started writing to
236          // the client.
237          writer.start("error");
238          writer.text(e.what());
239          writer.end();
240        }
241      } catch (const http::exception &e) {
242        // errors here occur before we've started writing the response
243        // so we can send something helpful back to the client.
244        respond_error(e, request);
245
246      } catch (const std::exception &e) {
247        // catch an error here to provide feedback to the user
248        respond_error(http::server_error(e.what()), request);
249
250        // re-throw the exception for higher-level handling
251        throw;
252      }
253    }
254
255  } catch (const pqxx::sql_error &er) {
256    // Catch-all for any other postgres exceptions
257    std::cerr << "Error: " << er.what() << std::endl
258              << "Caused by: " << er.query() << std::endl;
259    return 1;
260
261  } catch (const std::exception &e) {
262    std::cerr << "Exception: " << e.what() << std::endl;
263    return 1;
264
265  }
266
267  return 0;
268}
Note: See TracBrowser for help on using the repository browser.