#!/usr/bin/python
# -*- coding: utf-8 -*-
from math import pi,cos,sin,log,exp,atan
from subprocess import call
from threading import Thread, Lock
from datetime import datetime
from time import sleep
import sys, os

os.environ['LD_LIBRARY_PATH'] = '/home/blackhex/local/Mapnik/lib'
path = ['/home/blackhex/local/Mapnik/lib/python2.6/site-packages']
path.extend(sys.path)
sys.path = path

DEG_TO_RAD = pi/180
RAD_TO_DEG = 180/pi

fsLock = Lock()
stdoutLock = Lock()

def minmax (a,b,c):
    a = max(a,b)
    a = min(a,c)
    return a

class GoogleProjection:
    def __init__(self,levels=18):
        self.Bc = []
        self.Cc = []
        self.zc = []
        self.Ac = []
        c = 256
        for d in range(0,levels):
            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 fromLLtoPixel(self,ll,zoom):
         d = self.zc[zoom]
         e = round(d[0] + ll[0] * self.Bc[zoom])
         f = minmax(sin(DEG_TO_RAD * ll[1]),-0.9999,0.9999)
         g = round(d[1] + 0.5*log((1+f)/(1-f))*-self.Cc[zoom])
         return (e,g)

    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)

from mapnik import *

def render_tiles(mapFile, threadId, bbox, tileSQDir, tileHQDir, minZoom = 1,
  maxZoom = 18, name = "unknown"):
    global fsLock, stdoutLock, finish
    print "Rendering: ", bbox, minZoom, maxZoom, name

    # Setup Mapnik
    gprj = GoogleProjection(maxZoom + 1) 
    m = Map(2 * 256, 2 * 256)
    load_map(m, mapFile)
    prj = Projection("+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0"
      " +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs +over")

    ll0 = (bbox[0],bbox[3])
    ll1 = (bbox[2],bbox[1])

    for z in range(minZoom,maxZoom + 1):
        px0 = gprj.fromLLtoPixel(ll0, z)
        px1 = gprj.fromLLtoPixel(ll1, z)

        # Check if we have directories in place.
        zoom = "%s" % z
        fsLock.acquire()
        if not os.path.exists(os.path.join(tileSQDir, zoom)):
            os.mkdir(os.path.join(tileSQDir, zoom))
        if not os.path.exists(os.path.join(tileHQDir, zoom)):
            os.mkdir(os.path.join(tileHQDir, zoom))
        fsLock.release()

        for x in range(int(px0[0]/256.0),int(px1[0]/256.0)+1):
            # Check if we have directories in place
            str_x = "%s" % x
            fsLock.acquire()
            if not os.path.exists(os.path.join(tileSQDir, zoom, str_x)):
                os.mkdir(os.path.join(tileSQDir, zoom, str_x))
            if not os.path.exists(os.path.join(tileHQDir, zoom, str_x)):
                os.mkdir(os.path.join(tileHQDir, zoom, str_x))
            fsLock.release()

            for y in range(int(px0[1]/256.0),int(px1[1]/256.0)+1):
                start_time = datetime.now()

                p0 = gprj.fromPixelToLL((x * 256.0, (y+1) * 256.0),z)
                p1 = gprj.fromPixelToLL(((x+1) * 256.0, y * 256.0),z)

                # render a new tile and store it on filesystem
                c0 = prj.forward(Coord(p0[0],p0[1]))
                c1 = prj.forward(Coord(p1[0],p1[1]))

                bbox = Envelope(c0.x,c0.y,c1.x,c1.y)
                bbox.width(bbox.width() * 2)
                bbox.height(bbox.height() * 2)
                m.zoom_to_box(bbox)

                str_y = "%s" % y

                tileHQFile = os.path.join(tileHQDir, zoom, str_x, str_y + '.png')
                tileSQFile = os.path.join(tileSQDir, zoom, str_x, str_y + '.png')

                exists = ""
                if os.path.exists(tileHQFile):
                    exists = "exists"
                else:
                    im = Image(512, 512)
                    render(m, im)
                    view = im.view(128,128,256,256) # x,y,width,height

                    fsLock.acquire()
                    # We should check file existance aggain for atomicity.
                    if not os.path.exists(tileHQFile):
                        view.save(tileHQFile,'png')
                    fsLock.release()

                    # This is not thread safe.
                    if not os.path.exists(tileSQFile):
                        call("convert -colors 255 %s %s" % (tileHQFile,
                          tileSQFile), shell = True)

                bytes = os.stat(tileHQFile)[6]
                empty = ""
                if bytes == 137:
                    empty = "empty tile"

                # Determine time of rendering.
                end_time = datetime.now()
                time_delta = end_time - start_time

                stdoutLock.acquire()
                print "Thread #" + str(threadId), time_delta, name, "[", \
                  minZoom, "-", maxZoom, "]: ", z, x, y, "p:", p0, p1, exists, \
                  empty
                stdoutLock.release()

                # Terminate thread if requested to finish.
                if finish:
                   return

class RenderThread(Thread):
    def __init__(self, mapFile, threadId, bbox, tileSQDir, tileHQDir, minZoom,
      maxZoom):
        Thread.__init__(self)
        self.mapFile = mapFile
        self.threadId = threadId
        self.bbox = bbox
        self.tileSQDir = tileSQDir
        self.tileHQDir = tileHQDir
        self.minZoom = minZoom
        self.maxZoom = maxZoom

    def run(self):
        render_tiles(self.mapFile, self.threadId, self.bbox, self.tileSQDir,
          self.tileHQDir, self.minZoom, self.maxZoom, "OpenStreetMap")

if __name__ == "__main__":
    try:
        mapFile = os.environ['MAPNIK_MAP_FILE']
    except KeyError:
        sys.stderr.write('MAPNIK_MAP_FILE environment variable is not set!')
        exit(1)
    try:
        tileSQDir = os.environ['MAPNIK_TILE_SQ_DIR']
    except KeyError:
        sys.stderr.write('MAPNIK_TILE_SQ_DIR environment variable is not set!')
        exit(1)
    try:
        tileHQDir = os.environ['MAPNIK_TILE_HQ_DIR']
    except KeyError:
        sys.stderr.write('MAPNIK_TILE_HQ_DIR environment variable is not set!')
        exit(1)
    try:
        numThreads = int(os.environ['MAPNIK_NUM_THREADS'])
    except KeyError:
        numThreads = 1

    # Czech Republic
    minZoom = 0
    maxZoom = 14
    bbox = (12.10, 48.55, 18.86, 51.06)

    # Create target directories.
    if not os.path.exists(tileSQDir):
        os.mkdir(tileSQDir)
    if not os.path.exists(tileHQDir):
        os.mkdir(tileHQDir)

    # Create and setup threads.
    finish = False
    threads = []
    threadLon = (bbox[2] - bbox[0]) / numThreads
    for threadId in range(numThreads):
       threadBbox = (bbox[0] + threadLon * threadId, bbox[1], bbox[0] +
         threadLon * (threadId + 1), bbox[3])
       thread = RenderThread(mapFile, threadId, threadBbox, tileSQDir, tileHQDir,
         minZoom, maxZoom)
       threads.append(thread)

    # Start the threads,
    for thread in threads:
        thread.start()

    # Wait for threads to finish.
    try:
        while not finish:
            for thread in threads:
                thread.join(1)
            my_finish = True
            for thread in threads:
                if thread.isAlive():
                    my_finish = False
                        break;
            finish = my_finish
    except KeyboardInterrupt:
        finish = True
        for thread in threads:
            thread.join()
