mod_tile: Apache module and rendering daemon for serving OSM tiles

This commit is contained in:
Jon Burgess
2007-10-12 21:18:59 +00:00
commit cd9801bc1e
12 changed files with 1541 additions and 0 deletions

0
.deps Normal file
View File

51
Makefile Normal file
View File

@ -0,0 +1,51 @@
builddir = .
top_dir:=$(shell /usr/sbin/apxs -q exp_installbuilddir)
top_dir:=$(shell /usr/bin/dirname ${top_dir})
top_srcdir = ${top_dir}
top_builddir = ${top_dir}
include ${top_builddir}/build/special.mk
CXX := g++
APXS = apxs
APACHECTL = apachectl
EXTRA_CFLAGS = -I$(builddir)
EXTRA_CPPFLAGS += -g -O2 -Wall
EXTRA_LDFLAGS += $(shell pkg-config --libs libagg)
all: local-shared-build renderd
clean:
rm -f *.o *.lo *.slo *.la .libs/*
rm -f renderd
RENDER_CPPFLAGS += -g -O2 -Wall
RENDER_CPPFLAGS += -I/usr/local/include/mapnik
RENDER_CPPFLAGS += $(shell pkg-config --cflags freetype2)
RENDER_CPPFLAGS += $(shell Magick++-config --cxxflags --cppflags)
RENDER_CPPFLAGS += $(shell pkg-config --cflags libagg)
RENDER_LDFLAGS += -g
RENDER_LDFLAGS += -lmapnik -L/usr/local/lib64
RENDER_LDFLAGS += $(shell pkg-config --libs freetype2)
RENDER_LDFLAGS += $(shell Magick++-config --ldflags --libs)
RENDER_LDFLAGS += $(shell pkg-config --libs libagg)
renderd: daemon.c gen_tile.cpp
$(CXX) -o $@ $^ $(RENDER_LDFLAGS) $(RENDER_CPPFLAGS)
MYSQL_CFLAGS += -g -O2 -Wall
MYSQL_CFLAGS += $(shell mysql_config --cflags)
MYSQL_LDFLAGS += $(shell mysql_config --libs)
mysql2file: mysql2file.c
$(CC) $(MYSQL_CFLAGS) $(MYSQL_LDFLAGS) -o $@ $^
# Not sure why this is not created automatically
.deps:
touch .deps

413
daemon.c Normal file
View File

@ -0,0 +1,413 @@
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <poll.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include "gen_tile.h"
#include "protocol.h"
#define QUEUE_MAX (10)
#define MAX_CONNECTIONS (10)
#define MAX(a,b) ((a) > (b) ? (a) : (b))
#define FD_INVALID (-1)
#define REQ_LIMIT (10)
#define DIRTY_LIMIT (1000 * 1000)
#define NUM_THREADS (4)
static pthread_t render_threads[NUM_THREADS];
static struct sigaction sigPipeAction;
void pipe_handler(__attribute__((unused)) int sigNum)
{
// Needed in case the client closes the connection
// before we send a response.
// FIXME: is fprintf really safe in signal handler?
//fprintf(stderr, "Caught SIGPIPE\n");
}
// Build parent directories for the specified file name
// Note: the part following the trailing / is ignored
// e.g. mkdirp("/a/b/foo.png") == shell mkdir -p /a/b
static int mkdirp(const char *path) {
struct stat s;
char tmp[PATH_MAX];
char *p;
strncpy(tmp, path, sizeof(tmp));
// Look for parent directory
p = strrchr(tmp, '/');
if (!p)
return 0;
*p = '\0';
if (!stat(tmp, &s))
return !S_ISDIR(s.st_mode);
*p = '/';
// Walk up the path making sure each element is a directory
p = tmp;
if (!*p)
return 0;
p++; // Ignore leading /
while (*p) {
if (*p == '/') {
*p = '\0';
if (!stat(tmp, &s)) {
if (!S_ISDIR(s.st_mode))
return 1;
} else if (mkdir(tmp, 0777))
return 1;
*p = '/';
}
p++;
}
return 0;
}
static struct item reqHead, dirtyHead, renderHead;
static int reqNum, dirtyNum;
static pthread_mutex_t qLock;
struct item *fetch_request(void)
{
struct item *item = NULL;
pthread_mutex_lock(&qLock);
if (reqNum) {
item = reqHead.next;
reqNum--;
} else if (dirtyNum) {
item = dirtyHead.next;
dirtyNum--;
}
if (item) {
item->next->prev = item->prev;
item->prev->next = item->next;
item->prev = &renderHead;
item->next = renderHead.next;
renderHead.next->prev = item;
renderHead.next = item;
}
pthread_mutex_unlock(&qLock);
return item;
}
void delete_request(struct item *item)
{
pthread_mutex_lock(&qLock);
item->next->prev = item->prev;
item->prev->next = item->next;
pthread_mutex_unlock(&qLock);
free(item);
}
void clear_requests(int fd)
{
struct item *item;
pthread_mutex_lock(&qLock);
item = reqHead.next;
while (item != &reqHead) {
if (item->fd == fd)
item->fd = FD_INVALID;
item = item->next;
}
item = renderHead.next;
while (item != &renderHead) {
if (item->fd == fd)
item->fd = FD_INVALID;
item = item->next;
}
pthread_mutex_unlock(&qLock);
}
void send_response(struct item *item, enum protoCmd rsp)
{
struct protocol *req = &item->req;
int ret;
pthread_mutex_lock(&qLock);
if ((item->fd != FD_INVALID) && (req->cmd == cmdRender)) {
req->cmd = rsp;
//fprintf(stderr, "Sending message %d to %d\n", rsp, item->fd);
ret = send(item->fd, req, sizeof(*req), 0);
if (ret != sizeof(*req))
perror("send error during send_done");
}
pthread_mutex_unlock(&qLock);
}
static inline const char *cmdStr(enum protoCmd c)
{
switch (c) {
case cmdIgnore: return "Ignore";
case cmdRender: return "Render";
case cmdDirty: return "Dirty";
case cmdDone: return "Done";
case cmdNotDone: return "NotDone";
default: return "unknown";
}
}
int pending(struct item *test)
{
// check all queues and render list to see if this request already queued
// call with qLock held
struct item *item;
item = reqHead.next;
while (item != &reqHead) {
if ((item->req.x == test->req.x) && (item->req.y == test->req.y) && (item->req.z == test->req.z))
return 1;
item = item->next;
}
item = dirtyHead.next;
while (item != &dirtyHead) {
if ((item->req.x == test->req.x) && (item->req.y == test->req.y) && (item->req.z == test->req.z))
return 1;
item = item->next;
}
item = renderHead.next;
while (item != &renderHead) {
if ((item->req.x == test->req.x) && (item->req.y == test->req.y) && (item->req.z == test->req.z))
return 1;
item = item->next;
}
return 0;
}
enum protoCmd rx_request(const struct protocol *req, int fd)
{
struct item *list = NULL, *item;
if (req->ver != 1) {
fprintf(stderr, "Bad protocol version %d\n", req->ver);
return cmdIgnore;
}
fprintf(stderr, "%s z(%d), x(%d), y(%d), path(%s)\n",
cmdStr(req->cmd), req->z, req->x, req->y, req->path);
if ((req->cmd != cmdRender) && (req->cmd != cmdDirty))
return cmdIgnore;
if (mkdirp(req->path))
return cmdNotDone;
item = (struct item *)malloc(sizeof(*item));
if (!item) {
fprintf(stderr, "malloc failed\n");
return cmdNotDone;
}
item->req = *req;
pthread_mutex_lock(&qLock);
if (pending(item)) {
pthread_mutex_unlock(&qLock);
free(item);
return cmdNotDone; // No way to wait on a pending tile
}
if ((req->cmd == cmdRender) && (reqNum < REQ_LIMIT)) {
list = &reqHead;
reqNum++;
item->fd = fd;
} else if (dirtyNum < DIRTY_LIMIT) {
list = &dirtyHead;
dirtyNum++;
item->fd = FD_INVALID; // No response after render
}
if (list) {
item->next = list;
item->prev = list->prev;
item->prev->next = item;
list->prev = item;
} else
free(item);
pthread_mutex_unlock(&qLock);
return (list == &reqHead)?cmdIgnore:cmdNotDone;
}
void process_loop(int listen_fd)
{
int num_connections = 0;
int connections[MAX_CONNECTIONS];
bzero(connections, sizeof(connections));
while (1) {
struct sockaddr_un in_addr;
socklen_t in_addrlen = sizeof(in_addr);
fd_set rd;
int incoming, num, nfds, i;
FD_ZERO(&rd);
FD_SET(listen_fd, &rd);
nfds = listen_fd+1;
for (i=0; i<num_connections; i++) {
FD_SET(connections[i], &rd);
nfds = MAX(nfds, connections[i]+1);
}
num = select(nfds, &rd, NULL, NULL, NULL);
if (num == -1)
perror("select()");
else if (num) {
//printf("Data is available now on %d fds\n", num);
if (FD_ISSET(listen_fd, &rd)) {
num--;
incoming = accept(listen_fd, (struct sockaddr *) &in_addr, &in_addrlen);
if (incoming < 0) {
perror("accept()");
break;
}
if (num_connections == MAX_CONNECTIONS) {
fprintf(stderr, "Connection limit(%d) reached. Dropping connection\n", MAX_CONNECTIONS);
close(incoming);
} else {
connections[num_connections++] = incoming;
fprintf(stderr, "Got incoming connection, fd %d, number %d\n", incoming, num_connections);
}
}
for (i=0; num && (i<num_connections); i++) {
int fd = connections[i];
if (FD_ISSET(fd, &rd)) {
struct protocol cmd;
int ret;
//fprintf(stderr, "New command from fd %d, number %d, to go %d\n", fd, i, num);
// TODO: to get highest performance we should loop here until we get EAGAIN
ret = recv(fd, &cmd, sizeof(cmd), MSG_DONTWAIT);
if (ret == sizeof(cmd)) {
enum protoCmd rsp = rx_request(&cmd, fd);
switch(rsp) {
case cmdNotDone:
cmd.cmd = rsp;
fprintf(stderr, "Sending NotDone response(%d)\n", rsp);
ret = send(fd, &cmd, sizeof(cmd), 0);
if (ret != sizeof(cmd))
perror("response send error");
break;
default:
break;
}
} else if (!ret) {
int j;
num_connections--;
fprintf(stderr, "Connection %d, fd %d closed, now %d left\n", i, fd, num_connections);
for (j=i; j < num_connections; j++)
connections[j] = connections[j+1];
clear_requests(fd);
close(fd);
} else {
fprintf(stderr, "Recv Error on fd %d\n", fd);
break;
}
}
}
} else
fprintf(stderr, "Select timeout\n");
}
}
int main(void)
{
const char *spath = RENDER_SOCKET;
int fd, i;
struct sockaddr_un addr;
mode_t old;
fprintf(stderr, "Rendering daemon\n");
pthread_mutex_init(&qLock, NULL);
reqHead.next = reqHead.prev = &reqHead;
dirtyHead.next = dirtyHead.prev = &dirtyHead;
renderHead.next = renderHead.prev = &renderHead;
fd = socket(PF_UNIX, SOCK_STREAM, 0);
if (fd < 0) {
fprintf(stderr, "failed to create unix sozket\n");
exit(2);
}
bzero(&addr, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, spath, sizeof(addr.sun_path));
unlink(addr.sun_path);
old = umask(0); // Need daemon socket to be writeable by apache
if (bind(fd, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
fprintf(stderr, "socket bind failed for: %s\n", spath);
close(fd);
exit(3);
}
umask(old);
if (listen(fd, QUEUE_MAX) < 0) {
fprintf(stderr, "socket listen failed for %d\n", QUEUE_MAX);
close(fd);
exit(4);
}
#if 0
if (fcntl(fd, F_SETFD, O_RDWR | O_NONBLOCK) < 0) {
fprintf(stderr, "setting socket non-block failed\n");
close(fd);
exit(5);
}
#endif
//sigPipeAction.sa_handler = pipe_handler;
sigPipeAction.sa_handler = SIG_IGN;
if (sigaction(SIGPIPE, &sigPipeAction, NULL) < 0) {
fprintf(stderr, "failed to register signal handler\n");
close(fd);
exit(6);
}
render_init();
for(i=0; i<NUM_THREADS; i++) {
if (pthread_create(&render_threads[i], NULL, render_thread, NULL)) {
fprintf(stderr, "error spawning render thread\n");
close(fd);
exit(7);
}
}
process_loop(fd);
unlink(spath);
close(fd);
return 0;
}

199
gen_tile.cpp Normal file
View File

@ -0,0 +1,199 @@
#include <mapnik/map.hpp>
#include <mapnik/datasource_cache.hpp>
#include <mapnik/agg_renderer.hpp>
#include <mapnik/filter_factory.hpp>
#include <mapnik/color_factory.hpp>
#include <mapnik/image_util.hpp>
#include <mapnik/load_map.hpp>
#include <mapnik/image_util.hpp>
#include <Magick++.h>
#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>
#include <dirent.h>
#include <unistd.h>
#include <pthread.h>
#include "gen_tile.h"
#include "protocol.h"
using namespace mapnik;
#define DEG_TO_RAD (M_PIl/180)
#define RAD_TO_DEG (180/M_PIl)
static const int minZoom = 0;
static const int maxZoom = 18;
static const char *mapfile = "/home/jburgess/osm/svn.openstreetmap.org/applications/rendering/mapnik/osm-jb-merc.xml";
static void postProcess(const char *path)
{
// Convert the 32bit RGBA image to one with indexed colours
// TODO: Ideally this would work on the Mapnik Image32 instead of requiring the intermediate image
// Or have a post-process thread with queueing
char tmp[PATH_MAX];
Magick::Image image;
snprintf(tmp, sizeof(tmp), "%s.tmp", path);
try {
image.read(path);
image.matte(0);
image.quantizeDither(0);
image.quantizeColors(255);
image.quantize();
image.modulusDepth(8);
image.write(tmp);
rename(tmp, path);
}
catch( Magick::Exception &error_ ) {
std::cerr << "Caught exception: " << error_.what() << std::endl;
}
}
static double minmax(double a, double b, double c)
{
#define MIN(x,y) ((x)<(y)?(x):(y))
#define MAX(x,y) ((x)>(y)?(x):(y))
a = MAX(a,b);
a = MIN(a,c);
return a;
}
class GoogleProjection
{
double *Ac, *Bc, *Cc, *zc;
public:
GoogleProjection(int levels=18) {
Ac = new double[levels];
Bc = new double[levels];
Cc = new double[levels];
zc = new double[levels];
int d, c = 256;
for (d=0; d<levels; d++) {
int e = c/2;
Bc[d] = c/360.0;
Cc[d] = c/(2 * M_PIl);
zc[d] = e;
Ac[d] = c;
c *=2;
}
}
void fromLLtoPixel(double &x, double &y, int zoom) {
double d = zc[zoom];
double f = minmax(sin(DEG_TO_RAD * y),-0.9999,0.9999);
x = round(d + x * Bc[zoom]);
y = round(d + 0.5*log((1+f)/(1-f))*-Cc[zoom]);
}
void fromPixelToLL(double &x, double &y, int zoom) {
double e = zc[zoom];
double g = (y - e)/-Cc[zoom];
x = (x - e)/Bc[zoom];
y = RAD_TO_DEG * ( 2 * atan(exp(g)) - 0.5 * M_PIl);
}
};
static void load_fonts(const char *font_dir, int recurse)
{
DIR *fonts = opendir(font_dir);
struct dirent *entry;
char path[PATH_MAX]; // FIXME: Eats lots of stack space when recursive
if (!fonts) {
fprintf(stderr, "Unable to open font directory: %s\n", font_dir);
return;
}
while ((entry = readdir(fonts))) {
struct stat b;
char *p;
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
snprintf(path, sizeof(path), "%s/%s", font_dir, entry->d_name);
if (stat(path, &b))
continue;
if (S_ISDIR(b.st_mode)) {
if (recurse)
load_fonts(path, recurse);
continue;
}
p = strrchr(path, '.');
if (p && !strcmp(p, ".ttf")) {
//fprintf(stderr, "Loading font: %s\n", path);
freetype_engine::register_font(path);
}
}
closedir(fonts);
}
static GoogleProjection gprj(maxZoom+1);
static projection prj("+proj=merc +datum=WGS84");
static enum protoCmd render(Map &m, Image32 &buf, int x, int y, int z, const char *filename)
{
double p0x = x * 256.0;
double p0y = (y + 1) * 256.0;
double p1x = (x + 1) * 256.0;
double p1y = y * 256.0;
gprj.fromPixelToLL(p0x, p0y, z);
gprj.fromPixelToLL(p1x, p1y, z);
prj.forward(p0x, p0y);
prj.forward(p1x, p1y);
Envelope<double> bbox(p0x, p0y, p1x,p1y);
bbox.width(bbox.width() * 2);
bbox.height(bbox.height() * 2);
m.zoomToBox(bbox);
agg_renderer<Image32> ren(m,buf, 128,128);
ren.apply();
buf.saveToFile(filename,"png");
return cmdDone; // OK
}
pthread_mutex_t map_lock;
void render_init(void)
{
// TODO: Make these module options
datasource_cache::instance()->register_datasources("/usr/local/lib64/mapnik/input");
//load_fonts("/usr/share/fonts", 1);
load_fonts("/usr/local/lib64/mapnik/fonts", 0);
pthread_mutex_init(&map_lock, NULL);
}
void *render_thread(__attribute__((unused)) void *unused)
{
Map m(2 * 256, 2 * 256);
Image32 buf(256, 256);
load_map(m,mapfile);
while (1) {
enum protoCmd ret;
struct item *item = fetch_request();
if (item) {
struct protocol *req = &item->req;
//pthread_mutex_lock(&map_lock);
ret = render(m, buf, req->x, req->y, req->z, req->path);
//pthread_mutex_unlock(&map_lock);
postProcess(req->path);
send_response(item, ret);
delete_request(item);
} else
sleep(1); // TODO: Use an event to indicate there are new requests
}
return NULL;
}

28
gen_tile.h Normal file
View File

@ -0,0 +1,28 @@
#ifndef GEN_TILE_H
#define GEN_TILE_H
#include "protocol.h"
#ifdef __cplusplus
extern "C" {
#endif
struct item {
struct item *next;
struct item *prev;
struct protocol req;
int fd;
};
//int render(Map &m, int x, int y, int z, const char *filename);
void *render_thread(void *unused);
struct item *fetch_request(void);
void delete_request(struct item *item);
void send_response(struct item *item, enum protoCmd);
void render_init(void);
#ifdef __cplusplus
}
#endif
#endif

460
mod_tile.c Normal file
View File

@ -0,0 +1,460 @@
#include "apr.h"
#include "apr_strings.h"
#include "apr_thread_proc.h" /* for RLIMIT stuff */
#include "apr_optional.h"
#include "apr_buckets.h"
#include "apr_lib.h"
#include "apr_poll.h"
#define APR_WANT_STRFUNC
#define APR_WANT_MEMFUNC
#include "apr_want.h"
#include "util_filter.h"
#include "ap_config.h"
#include "httpd.h"
#include "http_config.h"
#include "http_request.h"
#include "http_core.h"
#include "http_protocol.h"
#include "http_main.h"
#include "http_log.h"
#include "util_script.h"
#include "ap_mpm.h"
#include "mod_core.h"
#include "mod_cgi.h"
module AP_MODULE_DECLARE_DATA tile_module;
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <limits.h>
#include <time.h>
#include "gen_tile.h"
#include "protocol.h"
#define MAX_ZOOM 18
// MAX_SIZE is the biggest file which we will return to the user
#define MAX_SIZE (1 * 1024 * 1024)
// IMG_PATH must have blank.png etc.
#define WWW_ROOT "/var/www/html"
#define IMG_PATH "/img"
// TILE_PATH must have tile z directory z(0..18)/x/y.png
#define TILE_PATH "/osm_tiles2"
//#define TILE_PATH "/tile"
// MAX_LOAD_OLD: if tile is out of date, don't re-render it if past this load threshold (users gets old tile)
#define MAX_LOAD_OLD 5
// MAX_LOAD_OLD: if tile is missing, don't render it if past this load threshold (user gets 404 error)
#define MAX_LOAD_MISSING 10
// MAX_LOAD_ANY: give up serving any data if beyond this load (user gets 404 error)
#define MAX_LOAD_ANY 100
// Maximum tile age in seconds
// TODO: this mechanism should really be a hard cutoff on planet update time.
#define MAX_AGE (48 * 60 * 60)
// Typical interval between planet imports, used as basis for tile expiry times
#define PLANET_INTERVAL (7 * 24 * 60 * 60)
// Planet import should touch this file when complete
#define PLANET_TIMESTAMP "/tmp/planet-import-complete"
// Timeout before giving for a tile to be rendered
#define REQUEST_TIMEOUT (3)
#define FD_INVALID (-1)
#define MIN(x,y) ((x)<(y)?(x):(y))
#define MAX(x,y) ((x)>(y)?(x):(y))
enum tileState { tileMissing, tileOld, tileCurrent };
static int error_message(request_rec *r, const char *format, ...)
__attribute__ ((format (printf, 2, 3)));
static int error_message(request_rec *r, const char *format, ...)
{
va_list ap;
va_start(ap, format);
int len;
char *msg;
len = vasprintf(&msg, format, ap);
if (msg) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "%s", msg);
r->content_type = "text/plain";
if (!r->header_only)
ap_rputs(msg, r);
free(msg);
}
return OK;
}
int socket_init(request_rec *r)
{
const char *spath = RENDER_SOCKET;
int fd;
struct sockaddr_un addr;
//fprintf(stderr, "Starting rendering client\n");
fd = socket(PF_UNIX, SOCK_STREAM, 0);
if (fd < 0) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "failed to create unix socket");
return FD_INVALID;
}
bzero(&addr, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, spath, sizeof(addr.sun_path));
if (connect(fd, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "socket connect failed for: %s", spath);
close(fd);
return FD_INVALID;
}
return fd;
}
int request_tile(request_rec *r, int x, int y, int z, const char *filename, int dirtyOnly)
{
struct protocol cmd;
//struct pollfd fds[1];
static int fd = FD_INVALID;
int ret = 0;
if (fd == FD_INVALID) {
fd = socket_init(r);
if (fd == FD_INVALID) {
//fprintf(stderr, "Failed to connect to renderer\n");
return 0;
} else {
ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "Connected to renderer");
}
}
bzero(&cmd, sizeof(cmd));
cmd.ver = PROTO_VER;
cmd.cmd = dirtyOnly ? cmdDirty : cmdRender;
cmd.z = z;
cmd.x = x;
cmd.y = y;
strcpy(cmd.path, filename);
//fprintf(stderr, "Requesting tile(%d,%d,%d)\n", z,x,y);
ret = send(fd, &cmd, sizeof(cmd), 0);
if (ret != sizeof(cmd)) {
if (errno == EPIPE) {
close(fd);
fd = FD_INVALID;
}
//perror("send error");
return 0;
}
if (!dirtyOnly) {
struct timeval tv = { REQUEST_TIMEOUT, 0 };
fd_set rx;
int s;
while (1) {
FD_ZERO(&rx);
FD_SET(fd, &rx);
s = select(fd+1, &rx, NULL, NULL, &tv);
if (s == 1) {
bzero(&cmd, sizeof(cmd));
ret = recv(fd, &cmd, sizeof(cmd), 0);
if (ret != sizeof(cmd)) {
if (errno == EPIPE) {
close(fd);
fd = FD_INVALID;
}
//perror("recv error");
break;
}
//fprintf(stderr, "Completed tile(%d,%d,%d)\n", z,x,y);
if (cmd.x == x && cmd.y == y && cmd.z == z) {
if (cmd.cmd == cmdDone)
return 1;
else
return 0;
}
} else if (s == 0) {
break;
} else {
if (errno == EPIPE) {
close(fd);
fd = FD_INVALID;
break;
}
}
}
}
return 0;
}
static int getPlanetTime(request_rec *r)
{
static time_t last_check;
static time_t planet_timestamp;
time_t now = time(NULL);
struct stat buf;
// Only check for updates periodically
if (now < last_check + 300)
return planet_timestamp;
last_check = now;
if (stat(PLANET_TIMESTAMP, &buf)) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Planet timestamp file " PLANET_TIMESTAMP " is missing");
// Make something up
planet_timestamp = now - 3 * 24 * 60 * 60;
} else {
if (buf.st_mtime != planet_timestamp) {
ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "Planet file updated at %s", ctime(&buf.st_mtime));
planet_timestamp = buf.st_mtime;
}
}
return planet_timestamp;
}
enum tileState tile_state(request_rec *r, const char *filename)
{
// FIXME: Apache already has most, if not all, this info recorded in r->fileinfo, use this instead!
struct stat buf;
if (stat(filename, &buf))
return tileMissing;
if (buf.st_mtime < getPlanetTime(r))
return tileOld;
return tileCurrent;
}
static apr_status_t expires_filter(ap_filter_t *f, apr_bucket_brigade *b)
{
request_rec *r = f->r;
apr_time_t expires, holdoff, nextPlanet;
apr_table_t *t = r->headers_out;
enum tileState state = tile_state(r, r->filename);
char *timestr;
/* Append expiry headers ... */
//ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "expires(%s), uri(%s), filename(%s), path_info(%s)\n",
// r->handler, r->uri, r->filename, r->path_info);
// current tiles will expire after next planet dump is due
// or after 1 hour if the planet dump is late or tile is due for re-render
nextPlanet = (state == tileCurrent) ? apr_time_from_sec(getPlanetTime(r) + PLANET_INTERVAL) : 0;
holdoff = r->request_time + apr_time_from_sec(60 * 60);
expires = MAX(holdoff, nextPlanet);
apr_table_mergen(t, "Cache-Control",
apr_psprintf(r->pool, "max-age=%" APR_TIME_T_FMT,
apr_time_sec(expires - r->request_time)));
timestr = apr_palloc(r->pool, APR_RFC822_DATE_LEN);
apr_rfc822_date(timestr, expires);
apr_table_setn(t, "Expires", timestr);
ap_remove_output_filter(f);
return ap_pass_brigade(f->next, b);
}
static int serve_blank(request_rec *r)
{
// Redirect request to blank tile
r->method = apr_pstrdup(r->pool, "GET");
r->method_number = M_GET;
apr_table_unset(r->headers_in, "Content-Length");
ap_internal_redirect_handler(IMG_PATH "/blank-000000.png", r);
return OK;
}
int get_load_avg(request_rec *r)
{
FILE *loadavg = fopen("/proc/loadavg", "r");
int avg = 1000;
if (!loadavg) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "failed to read /proc/loadavg");
return 1000;
}
if (fscanf(loadavg, "%d", &avg) != 1) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "failed to parse /proc/loadavg");
fclose(loadavg);
return 1000;
}
fclose(loadavg);
return avg;
}
static int tile_dirty(request_rec *r, int x, int y, int z, const char *path)
{
request_tile(r, x,y,z,path, 1);
return OK;
}
static int get_tile(request_rec *r, int x, int y, int z, const char *path)
{
int avg = get_load_avg(r);
enum tileState state;
if (avg > MAX_LOAD_ANY) {
// we're too busy to send anything now
return error_message(r, "error: Load above MAX_LOAD_ANY threshold %d > %d", avg, MAX_LOAD_ANY);
}
state = tile_state(r, path);
// Note: We rely on the default Apache handler to return the files from the filesystem
// hence we return DECLINED in order to return the tile to the client
// or OK if we want to send something else.
switch (state) {
case tileCurrent:
return DECLINED;
break;
case tileOld:
if (avg > MAX_LOAD_OLD) {
// Too much load to render it now, mark dirty but return old tile
tile_dirty(r, x, y, z, path);
return DECLINED;
}
break;
case tileMissing:
if (avg > MAX_LOAD_MISSING) {
tile_dirty(r, x, y, z, path);
return error_message(r, "error: File missing and load above MAX_LOAD_MISSING threshold %d > %d", avg, MAX_LOAD_MISSING);
}
break;
}
if (request_tile(r, x,y,z,path, 0)) {
// Need to make apache try accessing this tile again (since it may have been missing)
// TODO: Instead of redirect, maybe we can update fileinfo for new tile, but is this sufficient?
apr_table_unset(r->headers_in, "Content-Length");
ap_internal_redirect_handler(r->uri, r);
return OK;
}
return error_message(r, "rendering failed for %s", path);
}
static int tile_status(request_rec *r, int x, int y, int z, const char *path)
{
// FIXME: Apache already has most, if not all, this info recorded in r->fileinfo, use this instead!
struct stat buf;
time_t now;
int old;
char MtimeStr[32]; // At least 26 according to man ctime_r
char AtimeStr[32]; // At least 26 according to man ctime_r
char *p;
if (stat(path, &buf))
return error_message(r, "Unable to find a tile at %s", path);
now = time(NULL);
old = (buf.st_mtime < now - MAX_AGE);
MtimeStr[0] = '\0';
ctime_r(&buf.st_mtime, MtimeStr);
AtimeStr[0] = '\0';
ctime_r(&buf.st_atime, AtimeStr);
if ((p = strrchr(MtimeStr, '\n')))
*p = '\0';
if ((p = strrchr(AtimeStr, '\n')))
*p = '\0';
return error_message(r, "Tile is %s. Last rendered at %s. Last accessed at %s", old ? "due to be rendered" : "clean", MtimeStr, AtimeStr);
}
static int tile_handler(request_rec *r)
{
int x, y, z, n, limit;
char option[11];
int oob;
char path[PATH_MAX];
option[0] = '\0';
if(strcmp(r->handler, "tile"))
return DECLINED;
//ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "handler(%s), uri(%s), filename(%s), path_info(%s)",
// r->handler, r->uri, r->filename, r->path_info);
/* URI = .../<z>/<x>/<y>.png[/option] */
n = sscanf(r->uri, TILE_PATH "/%d/%d/%d.png/%10s", &z, &x, &y, option);
if (n < 3) {
//return error_message(r, "unable to process: %s", r->path_info);
return DECLINED;
}
// Generate the tile filename.
// This may differ from r->filename in some cases (e.g. if a parent directory is missing)
snprintf(path, PATH_MAX, WWW_ROOT TILE_PATH "/%d/%d/%d.png", z, x, y);
// Validate tile co-ordinates
oob = (z < 0 || z > MAX_ZOOM);
if (!oob) {
// valid x/y for tiles are 0 ... 2^zoom-1
limit = (1 << z) - 1;
oob = (x < 0 || x > limit || y < 0 || y > limit);
}
if (n == 3) {
ap_add_output_filter("MOD_TILE", NULL, r, r->connection);
return oob ? serve_blank(r) : get_tile(r, x, y, z, path);
}
if (n == 4) {
if (oob)
return error_message(r, "The tile co-ordinates that you specified are invalid");
if (!strcmp(option, "status"))
return tile_status(r, x, y, z, path);
if (!strcmp(option, "dirty"))
return tile_dirty(r, x, y, z, path);
return error_message(r, "Unknown option");
}
return DECLINED;
}
static void register_hooks(__attribute__((unused)) apr_pool_t *p)
{
ap_register_output_filter("MOD_TILE", expires_filter, NULL, APR_HOOK_MIDDLE);
ap_hook_handler(tile_handler, NULL, NULL, APR_HOOK_FIRST);
}
module AP_MODULE_DECLARE_DATA tile_module =
{
STANDARD20_MODULE_STUFF,
NULL, /* dir config creater */
NULL, /* dir merger --- default is to override */
NULL, /* server config */
NULL, /* merge server config */
NULL, /* command apr_table_t */
register_hooks /* register hooks */
};

9
mod_tile.conf Normal file
View File

@ -0,0 +1,9 @@
# This is the Apache server configuration file for providing OSM tile support
# through mod_tile
#
LoadModule tile_module modules/mod_tile.so
<Directory /var/www/html/osm_tiles2/>
SetHandler tile
</Directory>

13
modules.mk Normal file
View File

@ -0,0 +1,13 @@
#
# this is used/needed by the APACHE2 build system
#
MOD_TILE = mod_tile
mod_tile.la: ${MOD_TILE:=.slo}
$(SH_LINK) -rpath $(libexecdir) -module -avoid-version ${MOD_TILE:=.lo}
DISTCLEAN_TARGETS = modules.mk
shared = mod_tile.la

162
mysql2file.c Normal file
View File

@ -0,0 +1,162 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <limits.h>
#include <time.h>
#include <utime.h>
#include <mysql.h>
#include <mysqld_error.h>
#include <signal.h>
#include <stdarg.h>
#include <sslopt-vars.h>
#include <assert.h>
#define WWW_ROOT "/var/www/html"
// TILE_PATH must have tile z directory z(0..18)/x/y.png
#define TILE_PATH "/osm_tiles2"
// Build parent directories for the specified file name
// Note: the part following the trailing / is ignored
// e.g. mkdirp("/a/b/foo.png") == shell mkdir -p /a/b
static int mkdirp(const char *path) {
struct stat s;
char tmp[PATH_MAX];
char *p;
strncpy(tmp, path, sizeof(tmp));
// Look for parent directory
p = strrchr(tmp, '/');
if (!p)
return 0;
*p = '\0';
if (!stat(tmp, &s))
return !S_ISDIR(s.st_mode);
*p = '/';
// Walk up the path making sure each element is a directory
p = tmp;
if (!*p)
return 0;
p++; // Ignore leading /
while (*p) {
if (*p == '/') {
*p = '\0';
if (!stat(tmp, &s)) {
if (!S_ISDIR(s.st_mode))
return 1;
} else if (mkdir(tmp, 0777))
return 1;
*p = '/';
}
p++;
}
return 0;
}
void parseDate(struct tm *tm, const char *str)
{
// 2007-05-20 13:51:35
bzero(tm, sizeof(*tm));
int n = sscanf(str, "%d-%d-%d %d:%d:%d",
&tm->tm_year, &tm->tm_mon, &tm->tm_mday, &tm->tm_hour, &tm->tm_min, &tm->tm_sec);
if (n !=6)
printf("failed to parse date string, got(%d): %s\n", n, str);
tm->tm_year -= 1900;
}
int main(int argc, char **argv)
{
MYSQL mysql;
char query[255];
MYSQL_RES *res;
MYSQL_ROW row;
mysql_init(&mysql);
if (!(mysql_real_connect(&mysql,"","tile","tile","tile",MYSQL_PORT,NULL,0)))
{
fprintf(stderr,"%s: %s\n",argv[0],mysql_error(&mysql));
exit(1);
}
mysql.reconnect= 1;
snprintf(query, sizeof(query), "SELECT x,y,z,data,created_at FROM tiles");
if ((mysql_query(&mysql, query)) || !(res= mysql_use_result(&mysql)))
{
fprintf(stderr,"Cannot query tiles: %s\n", mysql_error(&mysql));
exit(1);
}
while ((row= mysql_fetch_row(res)))
{
ulong *lengths= mysql_fetch_lengths(res);
char path[PATH_MAX];
unsigned long int x,y,z,length;
time_t created_at;
const char *data;
struct tm date;
int fd;
struct utimbuf utb;
assert(mysql_num_fields(res) == 5);
//printf("x(%s) y(%s) z(%s) data_length(%lu): %s\n", row[0], row[1], row[2], lengths[3], row[4]);
x = strtoul(row[0], NULL, 10);
y = strtoul(row[1], NULL, 10);
z = strtoul(row[2], NULL, 10);
data = row[3];
length = lengths[3];
parseDate(&date, row[4]);
created_at = mktime(&date);
//printf("x(%lu) y(%lu) z(%lu) data_length(%lu): %s", x,y,z,length,ctime(&created_at));
if (!length) {
printf("skipping empty tile x(%lu) y(%lu) z(%lu) data_length(%lu): %s", x,y,z,length,ctime(&created_at));
continue;
}
snprintf(path, PATH_MAX, WWW_ROOT TILE_PATH "/%lu/%lu/%lu.png", z, x, y);
printf("%s\n", path);
mkdirp(path);
fd = open(path, O_CREAT | O_WRONLY, 0644);
if (fd <0) {
perror(path);
exit(1);
}
if (write(fd, data, length) != length) {
perror("writing tile");
exit(2);
}
close(fd);
utb.actime = created_at;
utb.modtime = created_at;
if (utime(path, &utb) < 0) {
perror("utime");
exit(3);
}
}
printf ("Number of rows: %lu\n", (unsigned long) mysql_num_rows(res));
mysql_free_result(res);
mysql_close(&mysql); /* Close & free connection */
return 0;
}

31
mysql2file.rb Executable file
View File

@ -0,0 +1,31 @@
#!/usr/bin/ruby
require 'mysql'
require 'date'
require 'time'
require 'fileutils'
dbh = nil
dbh = Mysql.real_connect('localhost', 'tile', 'tile', 'tile')
dbh.query_with_result = false
dbh.query("select x,y,z,data,created_at from tiles" )
res = dbh.use_result
while row = res.fetch_hash do
x = row['x']
y = row['y']
z = row['z']
created_at = Time.parse(row['created_at'])
path = "/var/www/html/osm_tiles2/#{z}/#{x}"
FileUtils.mkdir_p(path)
print "x(#{x}) y(#{y}) z(#{z}), created_at(#{created_at.to_i})\n"
f = File.new("#{path}/#{y}.png", "w")
f.print row['data']
f.close
File.utime(created_at,created_at,"#{path}/#{y}.png")
end
puts "Number of rows returned: #{res.num_rows}"
res.free

36
protocol.h Normal file
View File

@ -0,0 +1,36 @@
#ifndef PROTOCOL_H
#define PROTOCOL_H
#ifdef __cplusplus
extern "C" {
#endif
/* Protocol between client and render daemon
*
* ver = 1;
*
* cmdRender(z,x,y), response: {cmdDone(z,x,y), cmdBusy(z,x,y)}
* cmdDirty(z,x,y), no response
*
* A client may not bother waiting for a response if the render daemon is too slow
* causing responses to get slightly out of step with requests.
*/
#define TILE_PATH_MAX (256)
#define PROTO_VER (1)
#define RENDER_SOCKET "/tmp/osm-renderd"
enum protoCmd { cmdIgnore, cmdRender, cmdDirty, cmdDone, cmdNotDone };
struct protocol {
int ver;
enum protoCmd cmd;
int x;
int y;
int z;
char path[TILE_PATH_MAX]; // FIXME: this is a really bad idea since it allows wrties to arbitrrary stuff
};
#ifdef __cplusplus
}
#endif
#endif

139
readme.txt Normal file
View File

@ -0,0 +1,139 @@
mod_tile
========
A program to efficiently render and serve map tiles for
www.openstreetmap.org map using Apache and Mapnik.
Note: This program is very much still in development
it has numerous hard coded paths and options which need
to be made user configurable options. You will not
be able to use this program without modifying these to
fit your local environment.
Requirements
============
OSM map data imported into PostgreSQL using osm2pgsql
Mapnik renderer along with the OSM.xml file and map
symbols, world_boundaries shapefiles. Apache with
development headers for APR module development.
Tile Rendering
==============
The rendering is implemented in a multithreaded process
called renderd which opens a unix socket and listens for
requests to render tiles. It uses Mapnik to render tiles
using the rendering rules defined in osm-jb-merc.xml.
The render daemon implements a queueing mechanism which
can render foreground requests (for new tiles being viewed)
and background requests (updating tiles which have expired)
Tile serving
============
Tiles are served from the filesystem using Apache from
/var/www/html/osm_tiles2/[Z]/[X]/[Y].png
where X,Y,Z are the standard OSM tile co-ordinates.
An Apache module called mod_tile enhances the regular
Apache file serving mechanisms to provide:
1) Tile expiry. It estimates when the tile is next
likely to be rendered and adds the approriate HTTP
cache expiry headers
2) When tiles have expired it requests the rendering
daemon to render (or re-render) the tile.
There is an attempt to make the mod_tile code aware of
the load on the server so that it backs off the rendering
if the machine is under heavy load.
Setup
=====
Make sure you've read and implemented the things in the
requirements section. Edit the paths in the source to
match your local setup. Compile the code with make, and
then make install (as root, to copy the mod_tile to the
apache module directory).
Create a new apache config file to load the module,
e.g.
/etc/httpd/conf.d/mod_tile.conf
--------------
LoadModule tile_module modules/mod_tile.so
<Directory /var/www/html/osm_tiles2/>
SetHandler tile
</Directory>
--------------
Create the directory /var/www/html/osm_tiles2/
Run the rendering daemon 'renderd'
Make sure the osm_tiles2 directory is writeable by the
user running the renderd process.
Restart Aapche
Note: SELinux will prevent the mod_tile code from opening
the unix-socket to the render daemon so must be disabled.
Try loading a tile in your browser, e.g.
http://localhost/osm_tiles2/0/0/0.png
The render daemon should have produce a message like:
Got incoming connection, fd 7, number 1
Render z(0), x(0), y(0), path(/var/www/html/osm_tiles2/0/0/0.png)
After a few seconds you should see a tile of the world
in your browser window.
To get a complete slippy map you should install a copy
of the OpenLayers based OSM slippy map and point this to
fetch tiles from http://localhost/osm_tiles2
mysql2file
==========
This was written to export the existing OSM tiles from
the Mysql database to the filesystem.
Bugs
====
Too many hard coded options (need to be come module options or command
line options to renderd).
mod_tile uses many non-APR routines. It probably only works in Linux.
If rendering daemon dies then all queued rendering requests are lost.
Code has not been thoroughly tested.
Performance
===========
The existing tile serving based on Apache + mod_ruby + cat_tile.rb
+ Mysql manages to serve something in the region of 250 - 500 requests
per second. Apache + mod_tile manages 2000+ per second. Both these
figures are for tiles which have already been rendered.
Filesystem Issues
=================
The average tile size is currently somewhere in the region of 2.5kB.
(Based on a 20GB MySQL DB which contains 8M tiles). Typically
filesystems are not particularly efficient at storing large numbers
of small files. They often take a minimum of 4kB on the disk.
Unfortunately if you reduce the block size to 1 or 2kB then this also
has a significant impact on the maximum file system size and number of
inodes available.
The simple z/x/y.png filesystem layout means that at high zoom levels
there can be large numbers of files in a single directory
Zoom 18 = 2^18 = 256k files in a single directory.
If ext2/3 is being used then you really need to have directory indexing
enabled.