#

# jVectorMap version 1.2.2

#

# Copyright 2011-2013, Kirill Lebedev

# Licensed under the MIT license.

#



import argparse

import sys

from osgeo import ogr

from osgeo import osr

import json

import shapely.geometry

import codecs



class Map:

  def __init__(self, name, language):

    self.paths = {}

    self.name = name

    self.language = language

    self.width = 0

    self.heoght = 0

    self.bbox = []



  def addPath(self, path, code, name):

    self.paths[code] = {"path": path, "name": name}



  def getJSCode(self):

    map = {"paths": self.paths, "width": self.width, "height": self.height, "insets": self.insets, "projection": self.projection}

    return "jQuery.fn.vectorMap('addMap', '"+self.name+"_"+self.projection['type']+"_"+self.language+"',"+json.dumps(map)+');'





class Converter:

  def __init__(self, args):

    self.map = Map(args['name'], args.get('language'))



    if args.get('sources'):

      self.sources = args['sources']

    else:

      self.sources = [{

        'input_file': args.get('input_file'),

        'where': args.get('where'),

        'codes_file': args.get('codes_file'),

        'country_name_index': args.get('country_name_index'),

        'country_code_index': args.get('country_code_index'),

        'input_file_encoding': args.get('input_file_encoding')

      }]



    default_source = {

      'where': '',

      'codes_file': '',

      'country_name_index': '0',

      'country_code_index': '1',

      'input_file_encoding': 'iso-8859-1'

    }



    for index in range(len(self.sources)):

      for key in default_source:

        if self.sources[index].get(key) is None:

          self.sources[index][key] = default_source[key]



    self.features = {}

    self.width = args.get('width')

    self.minimal_area = args.get('minimal_area')

    self.longitude0 = args.get('longitude0')

    self.projection = args.get('projection')

    self.precision = args.get('precision')

    self.buffer_distance = args.get('buffer_distance')

    self.simplify_tolerance = args.get('simplify_tolerance')

    if args.get('viewport'):

      self.viewport = map(lambda s: float(s), args.get('viewport').split(' '))

    else:

      self.viewport = False





    # spatial reference to convert to

    self.spatialRef = osr.SpatialReference()

    self.spatialRef.ImportFromProj4('+proj='+self.projection+' +a=6381372 +b=6381372 +lat_0=0 +lon_0='+str(self.longitude0))



    # handle map insets

    if args.get('insets'):

      self.insets = json.loads(args.get('insets'))

    else:

      self.insets = []



  def loadData(self):

    for sourceConfig in self.sources:

      self.loadDataSource( sourceConfig )



  def loadDataSource(self, sourceConfig):

    source = ogr.Open( sourceConfig['input_file'] )

    layer = source.GetLayer(0)

    layer.SetAttributeFilter( sourceConfig['where'].encode('ascii') )

    self.viewportRect = False



    if self.viewport:

      layer.SetSpatialFilterRect( *sourceConfig.get('viewport') )

      transformation = osr.CoordinateTransformation( layer.GetSpatialRef(), self.spatialRef )

      point1 = transformation.TransformPoint(self.viewport[0], self.viewport[1])

      point2 = transformation.TransformPoint(self.viewport[2], self.viewport[3])

      self.viewportRect = shapely.geometry.box(point1[0], point1[1], point2[0], point2[1])



    layer.ResetReading()



    # load codes from external tsv file if present or geodata file otherwise

    codes = {}

    if sourceConfig.get('codes_file'):

      for line in codecs.open(sourceConfig.get('codes_file'), 'r', "utf-8"):

        row = map(lambda s: s.strip(), line.split('\t'))

        codes[row[1]] = row[0]

    else:

      nextCode = 0

      for feature in layer:

        code = feature.GetFieldAsString(sourceConfig.get('country_code_index'))

        if code == '-99':

          code = '_'+str(nextCode)

          nextCode += 1

        name = feature.GetFieldAsString(sourceConfig.get('country_name_index')).decode(sourceConfig.get('input_file_encoding'))

        codes[name] = code

      layer.ResetReading()



    # load features

    for feature in layer:

      geometry = feature.GetGeometryRef()

      geometryType = geometry.GetGeometryType()



      if geometryType == ogr.wkbPolygon or geometryType == ogr.wkbMultiPolygon:

        geometry.TransformTo( self.spatialRef )

        shapelyGeometry = shapely.wkb.loads( geometry.ExportToWkb() )

        if not shapelyGeometry.is_valid:

          #buffer to fix selfcrosses

          shapelyGeometry = shapelyGeometry.buffer(0, 1)

        shapelyGeometry = self.applyFilters(shapelyGeometry)

        if shapelyGeometry:

          name = feature.GetFieldAsString(sourceConfig.get('country_name_index')).decode(sourceConfig.get('input_file_encoding'))

          code = codes[name]

          self.features[code] = {"geometry": shapelyGeometry, "name": name, "code": code}

      else:

        raise Exception, "Wrong geometry type: "+geometryType





  def convert(self, outputFile):

    self.loadData()



    codes = self.features.keys()

    self.map.insets = []

    envelope = []

    for inset in self.insets:

      insetBbox = self.renderMapInset(inset['codes'], inset['left'], inset['top'], inset['width'])

      insetHeight = (insetBbox[3] - insetBbox[1]) * (inset['width'] / (insetBbox[2] - insetBbox[0]))

      self.map.insets.append({

        "bbox": [{"x": insetBbox[0], "y": -insetBbox[3]}, {"x": insetBbox[2], "y": -insetBbox[1]}],

        "left": inset['left'],

        "top": inset['top'],

        "width": inset['width'],

        "height": insetHeight

      })

      envelope.append(

        shapely.geometry.box(

          inset['left'], inset['top'], inset['left'] + inset['width'], inset['top'] + insetHeight

        )

      )

      for code in inset['codes']:

        codes.remove(code)



    insetBbox = self.renderMapInset(codes, 0, 0, self.width)

    insetHeight = (insetBbox[3] - insetBbox[1]) * (self.width / (insetBbox[2] - insetBbox[0]))



    envelope.append( shapely.geometry.box( 0, 0, self.width, insetHeight ) )

    mapBbox = shapely.geometry.MultiPolygon( envelope ).bounds



    self.map.width = mapBbox[2] - mapBbox[0]

    self.map.height = mapBbox[3] - mapBbox[1]

    self.map.insets.append({

      "bbox": [{"x": insetBbox[0], "y": -insetBbox[3]}, {"x": insetBbox[2], "y": -insetBbox[1]}],

      "left": 0,

      "top": 0,

      "width": self.width,

      "height": insetHeight

    })

    self.map.projection = {"type": self.projection, "centralMeridian": float(self.longitude0)}



    open(outputFile, 'w').write( self.map.getJSCode() )





  def renderMapInset(self, codes, left, top, width):

    envelope = []

    for code in codes:

      envelope.append( self.features[code]['geometry'].envelope )



    bbox = shapely.geometry.MultiPolygon( envelope ).bounds



    scale = (bbox[2]-bbox[0]) / width



    # generate SVG paths

    for code in codes:

      feature = self.features[code]

      geometry = feature['geometry']

      if self.buffer_distance:

        geometry = geometry.buffer(self.buffer_distance*scale, 1)

      if geometry.is_empty:

        continue

      if self.simplify_tolerance:

        geometry = geometry.simplify(self.simplify_tolerance, preserve_topology=True)

      if isinstance(geometry, shapely.geometry.multipolygon.MultiPolygon):

        polygons = geometry.geoms

      else:

        polygons = [geometry]

      path = ''

      for polygon in polygons:

        rings = []

        rings.append(polygon.exterior)

        rings.extend(polygon.interiors)

        for ring in rings:

          for pointIndex in range( len(ring.coords) ):

            point = ring.coords[pointIndex]

            if pointIndex == 0:

              path += 'M'+str( round( (point[0]-bbox[0]) / scale + left, self.precision) )

              path += ','+str( round( (bbox[3] - point[1]) / scale + top, self.precision) )

            else:

              path += 'l' + str( round(point[0]/scale - ring.coords[pointIndex-1][0]/scale, self.precision) )

              path += ',' + str( round(ring.coords[pointIndex-1][1]/scale - point[1]/scale, self.precision) )

          path += 'Z'

      self.map.addPath(path, feature['code'], feature['name'])

    return bbox





  def applyFilters(self, geometry):

    if self.viewportRect:

      geometry = self.filterByViewport(geometry)

      if not geometry:

        return False

    if self.minimal_area:

      geometry = self.filterByMinimalArea(geometry)

      if not geometry:

        return False

    return geometry





  def filterByViewport(self, geometry):

    try:

      return geometry.intersection(self.viewportRect)

    except shapely.geos.TopologicalError:

      return False





  def filterByMinimalArea(self, geometry):

    if isinstance(geometry, shapely.geometry.multipolygon.MultiPolygon):

      polygons = geometry.geoms

    else:

      polygons = [geometry]

    polygons = filter(lambda p: p.area > self.minimal_area, polygons)

    return shapely.geometry.multipolygon.MultiPolygon(polygons)





parser = argparse.ArgumentParser(conflict_handler='resolve')

parser.add_argument('input_file')

parser.add_argument('output_file')

parser.add_argument('--country_code_index', type=int)

parser.add_argument('--country_name_index', type=int)

parser.add_argument('--codes_file', type=str)

parser.add_argument('--where', type=str)

parser.add_argument('--width', type=float)

parser.add_argument('--insets', type=str)

parser.add_argument('--minimal_area', type=float)

parser.add_argument('--buffer_distance', type=float)

parser.add_argument('--simplify_tolerance', type=float)

parser.add_argument('--viewport', type=str)

parser.add_argument('--longitude0', type=str)

parser.add_argument('--projection', type=str)

parser.add_argument('--name', type=str)

parser.add_argument('--language', type=str)

parser.add_argument('--input_file_encoding', type=str)

parser.add_argument('--precision', type=int)

args = vars(parser.parse_args())



default_args = {

  'buffer_distance': -0.4,

  'longitude0': '0',

  'projection': 'mill',

  'name': 'world',

  'language': 'en',

  'precision': 2,

  'insets': ''

}



if args['input_file'][-4:] == 'json':

  args.update( json.loads( open(args['input_file'], 'r').read() ) )



for key in default_args:

  if default_args.get(key) and args.get(key) is None:

    args[key] = default_args[key]



converter = Converter(args)

converter.convert(args['output_file'])