トップPython > タイル画像作成サーバ

タイル画像作成サーバ

1.タイル画像作成サーバ

Pythonプログラムにかなり慣れてきたため、C#で記述した地図システムから プロセス間通信でリクエストを受けて該当するタイル画像を作成するサーバ(下記)を作成した。 リクエストで zoom および x, y 座標値を受け取ると、パスを Dir/{zoom}/{x}/{y}.png として、 pngファイルを作成する。このとき、ディレクトリが存在しない場合には、まず、ディレクトリを作成する。

#!/usr/bin/env python
# -*- coding: shift_jis -

try:
    import mapnik2 as mapnik
except:
    import mapnik
import sys, os
import threading
from math import pi,exp,atan
from Queue import Queue
from socket import socket, AF_INET, SOCK_STREAM
mapnik.register_fonts('c:/windows/fonts/')

PORT = 2345
HOST = 'localhost'
RAD_TO_DEG = 180/pi
NUM_THREADS = 4
MAX_ZOOM = 20

request_queue = Queue(1025)

class GoogleProjection:
    def __init__(self):
        self.Bc = []
        self.Cc = []
        self.zc = []
        self.Ac = []
        c = 256
        for d in range(0,MAX_ZOOM+1):
            e = c/2;
            self.Bc.append(c/360.0)
            self.Cc.append(c/(2 * pi))
            self.zc.append((e,e))
            self.Ac.append(c)
            c *= 2
                
    def fromPixelToLL(self,px,zoom):
         e = self.zc[zoom]
         f = (px[0] - e[0])/self.Bc[zoom]
         g = (px[1] - e[1])/-self.Cc[zoom]
         h = RAD_TO_DEG * ( 2 * atan(exp(g)) - 0.5 * pi)
         return (f,h)

# タイル作成スレッド
class RenderThread:
    def __init__(self, mapfile, q, tile_dir):
        self.q = q
        self.tile_dir = tile_dir
        self.m = mapnik.Map(256, 256)

        # Load style XML
        mapnik.load_map(self.m, mapfile, True)

        self.prj = mapnik.Projection(self.m.srs)
        self.tileproj = GoogleProjection()

    def render_tile(self, z_x_y):
        items = z_x_y.split('_')
        if (len(items) == 3):
            zoom = items[0]
            str_x = items[1]
            str_y = items[2]
            z = int(zoom)
            x = int(str_x)
            y = int(str_y)
        else:
            print "Error: <" + z_x_y + ">"
            return

        # Calculate pixel positions of bottom-left & top-right
        p0 = (x * 256, (y + 1) * 256)
        p1 = ((x + 1) * 256, y * 256)

        # Convert to LatLong (EPSG:4326)
        l0 = self.tileproj.fromPixelToLL(p0, z);
        l1 = self.tileproj.fromPixelToLL(p1, z);

        # Convert to map projection (e.g. mercator co-ords EPSG:900913)
        c0 = self.prj.forward(mapnik.Coord(l0[0],l0[1]))
        c1 = self.prj.forward(mapnik.Coord(l1[0],l1[1]))

        # Bounding box for the tile
        bbox = mapnik.Box2d(c0.x,c0.y, c1.x,c1.y)

        self.m.resize(256, 256)
        self.m.zoom_to_box(bbox)
        if(self.m.buffer_size < 128): self.m.buffer_size = 128

        # Render image with default Agg renderer
        im = mapnik.Image(256, 256)
        mapnik.render(self.m, im)

        if not os.path.isdir(self.tile_dir + zoom + '/' + str_x):
            os.makedirs(self.tile_dir + zoom + '/' + str_x, 0755)

        tile_uri = self.tile_dir + '/' + zoom + '/' + str_x + '/' + str_y + ".png"
        if not os.path.isfile(tile_uri):
            im.save(tile_uri, "png256")
        send_response(z_x_y)

    def loop(self):
        while True:
            # Fetch a tile from the request_queue and render it
            req = self.q.get()
            self.render_tile(req)
            self.q.task_done()

def start_renderers(mapfile, tile_dir):
    print "start renderers"
    renderers = {}
    for i in range(NUM_THREADS):
        renderer = RenderThread(mapfile, request_queue, tile_dir)
        render_thread = threading.Thread(target=renderer.loop)
        render_thread.start()
        renderers[i] = render_thread

def server():
    sock = socket(AF_INET, SOCK_STREAM)
    sock.bind((HOST, PORT)) # アドレスとポートを指定. 
    sock.listen(5)          # 同時に接続できる個数
    while True:
        conn, addr = sock.accept()
        request = conn.recv(2048)
        request_queue.put(request)
    conn.close()

def send_response(response):
    sock = socket(AF_INET, SOCK_STREAM)
    sock.connect((HOST, PORT+1))
    sock.send(response.encode())
    sock.close()

if __name__ == "__main__":
    mapfile  = "c:/gis/mapnik/my_osm.xml"
    tile_dir = "c:/gis/tiles/osm/"
    sthread = threading.Thread(target=server)
    sthread.daemon = False
    sthread.start()
    start_renderers(mapfile, tile_dir)

このディレクトリ作成に問題があるようで、ときたま次のようなエラーが出る。 発生頻度は少ないので、おそらく、マルチスレッド処理に問題があるのであろう。 ほぼ同時に二つ以上のスレッドがリクエストの処理を始めた場合、 以下のコードで、同じディレクトリが存在しないと判断して、makedirs を実行するケースがある。 最初のスレッドがディレクトリを作成するので、次のスレッドが同じディレクトリを作ろうとして エラーを起こすのであろう。

この二行の処理にロックをかけて排他制御すれば解決するであろう。

        if not os.path.isdir(self.tile_dir + zoom + '/' + str_x):
            os.makedirs(self.tile_dir + zoom + '/' + str_x, 0755)

ちらっと読んだ限りでは、Python 2.7 と Python 3 の排他制御は異なり、 また、プロセス間の排他制御とスレッド間の排他制御は異なるようだ。

もう少し調べてみるが、Python の排他制御は C# の排他制御より相当面倒なようならば、 ディレクトリの作成は予め C# 側で行っておく方がいいだろう。

Pythonプログラムで対応する場合、(zoom, x, y) への分解をリクエストを受け付けるシリアル処理に 移して、ここで makedirs を行えば、排他制御はいらなくなる。C#の前処理にするよりはこの方がいいだろう。

まずは、文献[1] にある Lockオブジェクトを使ってみる。 グローバルオブジェクトとして global_lock を宣言する。

global_lock = threading.Lock()   # LOCK OBJECT

ディレクトリチェック・生成処理を global_lock.acquire() と global_lock.release() でくくる。

        # LOCK
        global_lock.acquire()

        if not os.path.isdir(self.tile_dir + zoom + '/' + str_x):
            os.makedirs(self.tile_dir + zoom + '/' + str_x, 0755)

        # RELEASE
        global_lock.release()

まずは、以上の修正[2016.12.27]でエラーがでなくなるか様子をみよう。 しばらく動かした限りではエラーは出なくなった。

A.リファレンス

[1] Pythonで学ぶ 基礎からのプログラミング入門  33 マルチスレッド処理を理解しよう(後編)