# # jVectorMap version 2.0.4 # # Copyright 2011-2013, Kirill Lebedev #

import sys import shapely.geometry import shapely.wkb import shapely.affinity from osgeo import ogr from osgeo import osr import json import codecs import copy

class Map:

def __init__(self, name, language):
  self.paths = {}
  self.name = name
  self.language = language
  self.width = 0
  self.height = 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, config):
  args = {
    'buffer_distance': -0.4,
    'simplify_tolerance': 0.2,
    'longitude0': 0,
    'projection': 'mill',
    'name': 'world',
    'width': 900,
    'language': 'en',
    'precision': 2,
    'insets': []
  }
  args.update(config)

  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'),
      'name_field': args.get('name_field'),
      'code_field': args.get('code_field'),
      'input_file_encoding': args.get('input_file_encoding')
    }]

  default_source = {
    'where': '',
    'name_field': 0,
    'code_field': 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 = float(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')
  self.for_each = args.get('for_each')
  self.emulate_longitude0 = args.get('emulate_longitude0')
  if args.get('emulate_longitude0') is None and (self.projection == 'merc' or self.projection =='mill') and self.longitude0 != 0:
    self.emulate_longitude0 = True

  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()
  projString = '+proj='+str(self.projection)+' +a=6381372 +b=6381372 +lat_0=0'
  if not self.emulate_longitude0:
    projString += ' +lon_0='+str(self.longitude0)
  self.spatialRef.ImportFromProj4(projString)

  # handle map insets
  if args.get('insets'):
    self.insets = 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

  transformation = osr.CoordinateTransformation( layer.GetSpatialRef(), self.spatialRef )
  if self.viewport:
    layer.SetSpatialFilterRect( *self.viewport )
    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()

  codes = {}

  if self.emulate_longitude0:
    meridian = -180 + self.longitude0
    p1 = transformation.TransformPoint(-180, 89)
    p2 = transformation.TransformPoint(meridian, -89)
    left = shapely.geometry.box(p1[0], p1[1], p2[0], p2[1])
    p3 = transformation.TransformPoint(meridian, 89)
    p4 = transformation.TransformPoint(180, -89)
    right = shapely.geometry.box(p3[0], p3[1], p4[0], p4[1])

  # load features
  nextCode = 0
  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:
        shapelyGeometry = shapelyGeometry.buffer(0, 1)

      if self.emulate_longitude0:
        leftPart = shapely.affinity.translate(shapelyGeometry.intersection(left), p4[0] - p3[0])
        rightPart = shapely.affinity.translate(shapelyGeometry.intersection(right), p1[0] - p2[0])
        shapelyGeometry = leftPart.buffer(0.1, 1).union(rightPart.buffer(0.1, 1)).buffer(-0.1, 1)

      if not shapelyGeometry.is_valid:
        shapelyGeometry = shapelyGeometry.buffer(0, 1)
      shapelyGeometry = self.applyFilters(shapelyGeometry)
      if shapelyGeometry:
        name = feature.GetFieldAsString(str(sourceConfig.get('name_field'))).decode(sourceConfig.get('input_file_encoding'))
        code = feature.GetFieldAsString(str(sourceConfig.get('code_field'))).decode(sourceConfig.get('input_file_encoding'))
        if code in codes:
          code = '_' + str(nextCode)
          nextCode += 1
        codes[code] = name
        self.features[code] = {"geometry": shapelyGeometry, "name": name, "code": code}
    else:
      raise Exception, "Wrong geometry type: "+geometryType

def convert(self, outputFile):
  print 'Generating '+outputFile

  self.loadData()

  codes = self.features.keys()
  main_codes = copy.copy(codes)
  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']:
      main_codes.remove(code)

  insetBbox = self.renderMapInset(main_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() )

  if self.for_each is not None:
    for code in codes:
      childConfig = copy.deepcopy(self.for_each)
      for param in ('input_file', 'output_file', 'where', 'name'):
        childConfig[param] = childConfig[param].replace('{{code}}', code.lower())
      converter = Converter(childConfig)
      converter.convert(childConfig['output_file'])

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*scale, 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)

args = {} if len(sys.argv) > 1:

paramsJson = open(sys.argv[1], 'r').read()

else:

paramsJson = sys.stdin.read()

paramsJson = json.loads(paramsJson)

converter = Converter(paramsJson) converter.convert(paramsJson)