#!/usr/bin/env python
"""
Zoomify Google Earth
--------------------
For given KML file and URL to Zoomify structure generates KMZ with
SuperOverlay, so this KMZ can be loaded into Google Earth and tiles
used by Zoomify pyramid are get on request by need from orignal
position of zoomify images.

Tries to find embeded zoomify image inside of given web page (url),
if not direct url to Zoomify metadata file (ImageProperties.xml) is not
specified.

KML should contain only one Image Overlay (<GroundOverlay>) with
georeference. Use original zoomify master image (probably downscaled)
for georeference in Google Earth, insert it using "Add Image Overlay",
rotate, pan, and position on the right place on planet, then right click
on it in list of "Places" and choose "Save As...".
Select format KML (not KMZ)!

(C) Copyright 2006 - Klokan Petr Pridal (www.klokan.cz)
"""

import sys, os
import re
import Image, StringIO
from urllib2 import urlopen, HTTPError
from urlparse import urljoin
from optparse import OptionParser
from shutil import rmtree
from zipfile import ZipFile, ZIP_DEFLATED

#####
#from zoomifygeo import ZoomifyGEOPyramid

from math import ceil
from pprint import pformat

class ZoomifyLevel( object ):
    """One level of zoomify pyramid"""

    def __init__(self, width, height, tilesize=256 ):
        self.width = width
        self.height = height
        self.xtiles = int( ceil( width / float(tilesize) ) )
        self.ytiles = int( ceil( height / float(tilesize) ) )

    def __len__(self):
        return self.xtiles * self.ytiles

    def __repr__(self):
        return "Image: %s x %s pixels; %s tiles in %s x %s" % (
            self.width, self.height, len(self), self.xtiles, self.ytiles )

    def tiles(self):
        """Count of tiles in this pyramid level"""
        return len(self)

class ZoomifyPyramid( object ):
    """Zoomify pyramid"""

    def __init__(self, width, height, tilesize=256 ):
        self.width = width
        self.height = height
        self.tilesize = tilesize
        self._pyramid = []
        level = ZoomifyLevel( width, height, tilesize )
        while level.width > tilesize or level.height > tilesize:
            self._pyramid.append(level)
            level = ZoomifyLevel( level.width / 2, level.height / 2, tilesize )
            # floor() not needed. Integer arithmetic
        self._pyramid.append(level)
        self._pyramid.reverse()

    def __len__(self):
        return len( self._pyramid )

    def __repr__(self):
        return pformat(self._pyramid)

    def __getitem__(self, index):
        return self._pyramid[ index ]

    def tiles_upto_level(self, level):
        """Number of tiles up to given level of pyramid"""
        return sum( map( len, self._pyramid[:level] ) )

    def levels(self):
        """Number of levels in pyramid. Available also as len(pyramid)"""
        return len( self )

    def tiles(self):
        """Total count of tiles in pyramid"""
        return self.tiles_upto_level( len( self._pyramid ) )

    def tile_index(self, level, x_coordinate, y_coordinate):
        """Index of tile at given coordinates"""
        return (x_coordinate +
            y_coordinate * self._pyramid[level].xtiles +
            self.tiles_upto_level( level ))

    def tile_filename(self, level, x_coordinate, y_coordinate):
        """Filename of tile at given coordinates"""
        return "TileGroup%s/%s-%s-%s.jpg" % (
            self.tile_index( level, x_coordinate, y_coordinate) / 256,
            level, x_coordinate, y_coordinate )
#####

class ZoomifyGEOPyramid( ZoomifyPyramid ):
    """Zoomify pyramid with georeference"""

    def __init__(self, width, height, tilesize=256,
            north=0, south=0, east=0, west=0, rotation = 0):
        ZoomifyPyramid.__init__(self, width, height, tilesize)
        self.north = north
        self.south = south
        self.east = east
        self.west = west
        self.rotation = rotation
        self.xcenter = west + (east - west) / 2.0
        self.ycenter = south + (north - south) / 2.0

    def tile_georeference(self, level, x_coordinate, y_coordinate):
        """Returns tuple of [north, south, east, west, rotation] for given tile"""
        # It's always true: north > south, east > west
        xpixelsize = ((500 + self.east) - (500 + self.west)) / float( self._pyramid[ level ].width )
        ypixelsize = ((500 + self.north) - (500 + self.south)) / float( self._pyramid[ level ].height )
        north = self.north - ypixelsize * self.tilesize * y_coordinate
        south = self.north - ypixelsize * min( self._pyramid[ level ].height, (y_coordinate+1) * self.tilesize )
        west = self.west + xpixelsize * self.tilesize * x_coordinate
        east = self.west + xpixelsize * min( self._pyramid[ level ].width, (x_coordinate+1) * self.tilesize )

        return [north, south, east, west, self.rotation ]

    def list_subtiles(self, level, x_coordinate, y_coordinate):
        """Returns list of tiles as [level, x, y] which cover same area like given tile but one level down"""
    
        # bottom level
        if level + 2 > self.levels():
            return []

        ret = [ [level + 1, x_coordinate * 2, y_coordinate * 2] ]
        if x_coordinate * 2 + 1 < self[level+1].xtiles:
            ret.append([level + 1, x_coordinate * 2 + 1, y_coordinate * 2])
        if y_coordinate * 2 + 1 < self[level+1].ytiles:
            ret.append([level + 1, x_coordinate * 2, y_coordinate * 2 + 1])
        if x_coordinate * 2 + 1 < self[level+1].xtiles and y_coordinate * 2 + 1 < self[level+1].ytiles:
            ret.append([level + 1, x_coordinate * 2 + 1, y_coordinate * 2 + 1])

        return ret

#####


# Version
__version__ = '0.1'
__revision__ = '2006-12-03'

if __name__ == '__main__':
    usage = "%prog <kml_file_with_georeference> <url_to_zoomify_webpage>"
    optparser = OptionParser(usage, version="%prog "+__version__)
    optparser.add_option("-r", "--relative",
        action="store_true", dest="relative", default=False,
        help="use relative links. KMZ should be in the same directory like ImageProperties.xml is.")
    (opts, args) = optparser.parse_args()
    #print opts.relative

    # No argumets: print usage

    if len(args) != 2:
        optparser.print_help()
        print __doc__
        sys.exit(1)

    # KML Parsing (first argument)
    # ----------------------------
    kmlfilename, zoomifyurl = args

    try:
        f = open( kmlfilename )
        s = f.read()
        f.close()
    except HTTPError, s:
        print kmlfilename
        print s
        sys.exit()

    # print s
    #from xml.dom.minidom import parseString
    #print parseString(s).documentElement.getAttribute('WIDTH')

    try:
        name = re.compile(r'<name>(.*?)</name>', re.I).search(s).group(1)
        north = float( re.compile(r'<north>(.*?)</', re.I).search(s).group(1) )
        south = float( re.compile(r'<south>(.*?)</', re.I).search(s).group(1) )
        east = float( re.compile(r'<east>(.*?)</', re.I).search(s).group(1) )
        west = float( re.compile(r'<west>(.*?)</', re.I).search(s).group(1) )
        rotation = re.compile(r'<rotation>(.*?)</', re.I).search(s)
        description = re.compile(r'<description>(.*?)</description>', re.I).search(s)
        if description:
            description = description.group(1)
        else:
            description = ""
        if rotation:
            rotation = float( rotation.group(1) )
        else:
            rotation = 0.0
    except AttributeError:
        print "KML file is not correct. Needed values (name and georeference) not found!"
        sys.exit()

    kmzfilename = os.path.splitext( kmlfilename )[0] + '.zip'

    # Zoomify parsing (second argument)
    # ---------------------------------

    if not zoomifyurl.endswith('ImageProperties.xml'):
        # Download and parse given url
        try:
            f = urlopen( zoomifyurl )
            s = f.read()
            f.close()
        except HTTPError, s:
            print args[0]
            print s
            sys.exit()

        print "Web page downloaded..."

        try: 
            m = re.compile(r'zoomifyImagePath=(.*?)"').search(s)
            zoomifyurl = urljoin( zoomifyurl, m.group(1) ).replace('&', '/', 1)
        except AttributeError:
            print args[0]
            print "Cannot find any Zoomify image on given page"
            sys.exit()
        
        print "Web page parsed..."

        zoomifyurl = urljoin(zoomifyurl, 'ImageProperties.xml')

    # Download and parse zoomify xml: ImageProperties.xml

    try:
        f = urlopen( zoomifyurl )
        s = f.read()
        f.close()
    except HTTPError, s:
        print zoomifyurl
        print s
        sys.exit()

    print "Zoomify metadata downloaded..."

    # print s
    #from xml.dom.minidom import parseString
    #print parseString(s).documentElement.getAttribute('WIDTH')

    try:
        width = int( re.compile(r'width="(.*?)"', re.I).search(s).group(1) )
        height = int( re.compile(r'height="(.*?)"', re.I).search(s).group(1) )
        tilesize = int( re.compile(r'tilesize="(.*?)"', re.I).search(s).group(1) )
        numtiles = int( re.compile(r'numtiles="(.*?)"', re.I).search(s).group(1) )
    except AttributeError:
        print "Zoomify ImageProperties.xml is not correct"
        sys.exit()

    # Create zoomify structure in memory
    # ----------------------------------

    pyramid = ZoomifyGEOPyramid( width, height, tilesize, north, south, east, west, rotation )
    print north, south, east, west, rotation
    print pyramid

    print "Number of levels: %s" % pyramid.levels()
    print "Number of tiles: %s" % pyramid.tiles()


    # Prepare templates
    # -----------------

    template_doc = """<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://earth.google.com/kml/2.1">
<Document>
<name>%s</name>

<NetworkLink>
	<open>1</open>
	<Region>
		<LatLonAltBox>
			<north>%s</north>
			<south>%s</south>
			<east>%s</east>
			<west>%s</west>
			<minAltitude>0</minAltitude>
			<maxAltitude>0</maxAltitude>
		</LatLonAltBox>
		<Lod>
			<minLodPixels>0</minLodPixels>
			<maxLodPixels>-1</maxLodPixels>
			<minFadeExtent>0</minFadeExtent>
			<maxFadeExtent>0</maxFadeExtent>
		</Lod>
	</Region>
	<Link>
		<href>TileGroup0/0-0-0.kml</href>
		<viewRefreshMode>onRegion</viewRefreshMode>
	</Link>
</NetworkLink>

</Document>
</kml>"""


    template_tile = """<?xml version="1.0"?>
<kml>
  <Document>
    <Region>
      <LatLonAltBox>
        <north>%s</north>
        <south>%s</south>
        <east>%s</east>
        <west>%s</west>
        <minAltitude>0.000000</minAltitude>
        <maxAltitude>0.000000</maxAltitude>
      </LatLonAltBox>
      <Lod>
        <minLodPixels>%s</minLodPixels>
        <maxLodPixels>-1</maxLodPixels>
        <minFadeExtent>0</minFadeExtent>
        <maxFadeExtent>0</maxFadeExtent>
      </Lod>
    </Region>

    <GroundOverlay>
      <drawOrder>%s</drawOrder>
      <Icon>
        <href>%s</href>
      </Icon>
      <LatLonBox>
        <north>%s</north>
        <south>%s</south>
        <east>%s</east>
        <west>%s</west>
        <rotation>%s</rotation>
      </LatLonBox>
    </GroundOverlay>

%s

  </Document>
</kml>
"""

    template_sub_tile = """
    <NetworkLink>
      <name>%s</name>
      <Region>
        <LatLonAltBox>
          <north>%s</north>
          <south>%s</south>
          <east>%s</east>
          <west>%s</west>
          <minAltitude>0.000000</minAltitude>
          <maxAltitude>0.000000</maxAltitude>
        </LatLonAltBox>
        <Lod>
          <minLodPixels>%s</minLodPixels>
          <maxLodPixels>-1</maxLodPixels>
          <minFadeExtent>0</minFadeExtent>
          <maxFadeExtent>0</maxFadeExtent>
        </Lod>
      </Region>
      <Link>
        <href>%s</href>
        <viewRefreshMode>onRegion</viewRefreshMode>
      </Link>
    </NetworkLink>
"""

    # Write main document KML
    # -----------------------

    kmz = ZipFile(kmzfilename, 'w', ZIP_DEFLATED)
    kmz.writestr('doc.kml', template_doc % (name, north, south, east, west) )

    i = 0
    minlodpixels = 0
    # Create KML file for each tile in zoomify structure
    for level in range(0, pyramid.levels()):
        print pyramid[ level ]
        for y in range(0, pyramid[level].ytiles):
            for x in range(0, pyramid[level].xtiles):
                #if i % 256 == 0:
                #    print "-" * 80, i / 256
                print pyramid.tile_index(level, x, y), pyramid.tile_filename(level, x, y)

                sub = ""
                for sl, sx, sy in pyramid.list_subtiles(level, x, y):
                    snorth, ssouth, seast, swest, srotation = pyramid.tile_georeference(sl, sx, sy)
                    sub += template_sub_tile % (
                        "%s-%s-%s" % (sl, sx, sy),
                        snorth, ssouth, seast, swest, tilesize,
                        "../TileGroup%s/%s-%s-%s.kml" % ( pyramid.tile_index(sl, sx, sy)/256, sl, sx, sy ) )

                north, south, east, west, rotation = pyramid.tile_georeference(level, x, y)
                image = urljoin(zoomifyurl, '.') + pyramid.tile_filename( level, x, y )
                if opts.relative:
                    image = os.path.split( pyramid.tile_filename( level, x, y ) )[1]

                kmz.writestr('TileGroup%s/%s-%s-%s.kml' % ( i/256, level, x, y) , 
                    template_tile % ( north, south, east, west, minlodpixels, 25+level, image, north, south, east, west, rotation, sub ) )

                if not minlodpixels:
                    minlodpixels = tilesize
                i += 1
    kmz.close()

