converter.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. #
  2. # jVectorMap version 1.2.2
  3. #
  4. # Copyright 2011-2013, Kirill Lebedev
  5. # Licensed under the MIT license.
  6. #
  7. import argparse
  8. import sys
  9. from osgeo import ogr
  10. from osgeo import osr
  11. import json
  12. import shapely.geometry
  13. import codecs
  14. class Map:
  15. def __init__(self, name, language):
  16. self.paths = {}
  17. self.name = name
  18. self.language = language
  19. self.width = 0
  20. self.heoght = 0
  21. self.bbox = []
  22. def addPath(self, path, code, name):
  23. self.paths[code] = {"path": path, "name": name}
  24. def getJSCode(self):
  25. map = {"paths": self.paths, "width": self.width, "height": self.height, "insets": self.insets, "projection": self.projection}
  26. return "jQuery.fn.vectorMap('addMap', '"+self.name+"_"+self.projection['type']+"_"+self.language+"',"+json.dumps(map)+');'
  27. class Converter:
  28. def __init__(self, args):
  29. self.map = Map(args['name'], args.get('language'))
  30. if args.get('sources'):
  31. self.sources = args['sources']
  32. else:
  33. self.sources = [{
  34. 'input_file': args.get('input_file'),
  35. 'where': args.get('where'),
  36. 'codes_file': args.get('codes_file'),
  37. 'country_name_index': args.get('country_name_index'),
  38. 'country_code_index': args.get('country_code_index'),
  39. 'input_file_encoding': args.get('input_file_encoding')
  40. }]
  41. default_source = {
  42. 'where': '',
  43. 'codes_file': '',
  44. 'country_name_index': '0',
  45. 'country_code_index': '1',
  46. 'input_file_encoding': 'iso-8859-1'
  47. }
  48. for index in range(len(self.sources)):
  49. for key in default_source:
  50. if self.sources[index].get(key) is None:
  51. self.sources[index][key] = default_source[key]
  52. self.features = {}
  53. self.width = args.get('width')
  54. self.minimal_area = args.get('minimal_area')
  55. self.longitude0 = args.get('longitude0')
  56. self.projection = args.get('projection')
  57. self.precision = args.get('precision')
  58. self.buffer_distance = args.get('buffer_distance')
  59. self.simplify_tolerance = args.get('simplify_tolerance')
  60. if args.get('viewport'):
  61. self.viewport = map(lambda s: float(s), args.get('viewport').split(' '))
  62. else:
  63. self.viewport = False
  64. # spatial reference to convert to
  65. self.spatialRef = osr.SpatialReference()
  66. self.spatialRef.ImportFromProj4('+proj='+self.projection+' +a=6381372 +b=6381372 +lat_0=0 +lon_0='+str(self.longitude0))
  67. # handle map insets
  68. if args.get('insets'):
  69. self.insets = json.loads(args.get('insets'))
  70. else:
  71. self.insets = []
  72. def loadData(self):
  73. for sourceConfig in self.sources:
  74. self.loadDataSource( sourceConfig )
  75. def loadDataSource(self, sourceConfig):
  76. source = ogr.Open( sourceConfig['input_file'] )
  77. layer = source.GetLayer(0)
  78. layer.SetAttributeFilter( sourceConfig['where'].encode('ascii') )
  79. self.viewportRect = False
  80. if self.viewport:
  81. layer.SetSpatialFilterRect( *sourceConfig.get('viewport') )
  82. transformation = osr.CoordinateTransformation( layer.GetSpatialRef(), self.spatialRef )
  83. point1 = transformation.TransformPoint(self.viewport[0], self.viewport[1])
  84. point2 = transformation.TransformPoint(self.viewport[2], self.viewport[3])
  85. self.viewportRect = shapely.geometry.box(point1[0], point1[1], point2[0], point2[1])
  86. layer.ResetReading()
  87. # load codes from external tsv file if present or geodata file otherwise
  88. codes = {}
  89. if sourceConfig.get('codes_file'):
  90. for line in codecs.open(sourceConfig.get('codes_file'), 'r', "utf-8"):
  91. row = map(lambda s: s.strip(), line.split('\t'))
  92. codes[row[1]] = row[0]
  93. else:
  94. nextCode = 0
  95. for feature in layer:
  96. code = feature.GetFieldAsString(sourceConfig.get('country_code_index'))
  97. if code == '-99':
  98. code = '_'+str(nextCode)
  99. nextCode += 1
  100. name = feature.GetFieldAsString(sourceConfig.get('country_name_index')).decode(sourceConfig.get('input_file_encoding'))
  101. codes[name] = code
  102. layer.ResetReading()
  103. # load features
  104. for feature in layer:
  105. geometry = feature.GetGeometryRef()
  106. geometryType = geometry.GetGeometryType()
  107. if geometryType == ogr.wkbPolygon or geometryType == ogr.wkbMultiPolygon:
  108. geometry.TransformTo( self.spatialRef )
  109. shapelyGeometry = shapely.wkb.loads( geometry.ExportToWkb() )
  110. if not shapelyGeometry.is_valid:
  111. #buffer to fix selfcrosses
  112. shapelyGeometry = shapelyGeometry.buffer(0, 1)
  113. shapelyGeometry = self.applyFilters(shapelyGeometry)
  114. if shapelyGeometry:
  115. name = feature.GetFieldAsString(sourceConfig.get('country_name_index')).decode(sourceConfig.get('input_file_encoding'))
  116. code = codes[name]
  117. self.features[code] = {"geometry": shapelyGeometry, "name": name, "code": code}
  118. else:
  119. raise Exception, "Wrong geometry type: "+geometryType
  120. def convert(self, outputFile):
  121. self.loadData()
  122. codes = self.features.keys()
  123. self.map.insets = []
  124. envelope = []
  125. for inset in self.insets:
  126. insetBbox = self.renderMapInset(inset['codes'], inset['left'], inset['top'], inset['width'])
  127. insetHeight = (insetBbox[3] - insetBbox[1]) * (inset['width'] / (insetBbox[2] - insetBbox[0]))
  128. self.map.insets.append({
  129. "bbox": [{"x": insetBbox[0], "y": -insetBbox[3]}, {"x": insetBbox[2], "y": -insetBbox[1]}],
  130. "left": inset['left'],
  131. "top": inset['top'],
  132. "width": inset['width'],
  133. "height": insetHeight
  134. })
  135. envelope.append(
  136. shapely.geometry.box(
  137. inset['left'], inset['top'], inset['left'] + inset['width'], inset['top'] + insetHeight
  138. )
  139. )
  140. for code in inset['codes']:
  141. codes.remove(code)
  142. insetBbox = self.renderMapInset(codes, 0, 0, self.width)
  143. insetHeight = (insetBbox[3] - insetBbox[1]) * (self.width / (insetBbox[2] - insetBbox[0]))
  144. envelope.append( shapely.geometry.box( 0, 0, self.width, insetHeight ) )
  145. mapBbox = shapely.geometry.MultiPolygon( envelope ).bounds
  146. self.map.width = mapBbox[2] - mapBbox[0]
  147. self.map.height = mapBbox[3] - mapBbox[1]
  148. self.map.insets.append({
  149. "bbox": [{"x": insetBbox[0], "y": -insetBbox[3]}, {"x": insetBbox[2], "y": -insetBbox[1]}],
  150. "left": 0,
  151. "top": 0,
  152. "width": self.width,
  153. "height": insetHeight
  154. })
  155. self.map.projection = {"type": self.projection, "centralMeridian": float(self.longitude0)}
  156. open(outputFile, 'w').write( self.map.getJSCode() )
  157. def renderMapInset(self, codes, left, top, width):
  158. envelope = []
  159. for code in codes:
  160. envelope.append( self.features[code]['geometry'].envelope )
  161. bbox = shapely.geometry.MultiPolygon( envelope ).bounds
  162. scale = (bbox[2]-bbox[0]) / width
  163. # generate SVG paths
  164. for code in codes:
  165. feature = self.features[code]
  166. geometry = feature['geometry']
  167. if self.buffer_distance:
  168. geometry = geometry.buffer(self.buffer_distance*scale, 1)
  169. if geometry.is_empty:
  170. continue
  171. if self.simplify_tolerance:
  172. geometry = geometry.simplify(self.simplify_tolerance, preserve_topology=True)
  173. if isinstance(geometry, shapely.geometry.multipolygon.MultiPolygon):
  174. polygons = geometry.geoms
  175. else:
  176. polygons = [geometry]
  177. path = ''
  178. for polygon in polygons:
  179. rings = []
  180. rings.append(polygon.exterior)
  181. rings.extend(polygon.interiors)
  182. for ring in rings:
  183. for pointIndex in range( len(ring.coords) ):
  184. point = ring.coords[pointIndex]
  185. if pointIndex == 0:
  186. path += 'M'+str( round( (point[0]-bbox[0]) / scale + left, self.precision) )
  187. path += ','+str( round( (bbox[3] - point[1]) / scale + top, self.precision) )
  188. else:
  189. path += 'l' + str( round(point[0]/scale - ring.coords[pointIndex-1][0]/scale, self.precision) )
  190. path += ',' + str( round(ring.coords[pointIndex-1][1]/scale - point[1]/scale, self.precision) )
  191. path += 'Z'
  192. self.map.addPath(path, feature['code'], feature['name'])
  193. return bbox
  194. def applyFilters(self, geometry):
  195. if self.viewportRect:
  196. geometry = self.filterByViewport(geometry)
  197. if not geometry:
  198. return False
  199. if self.minimal_area:
  200. geometry = self.filterByMinimalArea(geometry)
  201. if not geometry:
  202. return False
  203. return geometry
  204. def filterByViewport(self, geometry):
  205. try:
  206. return geometry.intersection(self.viewportRect)
  207. except shapely.geos.TopologicalError:
  208. return False
  209. def filterByMinimalArea(self, geometry):
  210. if isinstance(geometry, shapely.geometry.multipolygon.MultiPolygon):
  211. polygons = geometry.geoms
  212. else:
  213. polygons = [geometry]
  214. polygons = filter(lambda p: p.area > self.minimal_area, polygons)
  215. return shapely.geometry.multipolygon.MultiPolygon(polygons)
  216. parser = argparse.ArgumentParser(conflict_handler='resolve')
  217. parser.add_argument('input_file')
  218. parser.add_argument('output_file')
  219. parser.add_argument('--country_code_index', type=int)
  220. parser.add_argument('--country_name_index', type=int)
  221. parser.add_argument('--codes_file', type=str)
  222. parser.add_argument('--where', type=str)
  223. parser.add_argument('--width', type=float)
  224. parser.add_argument('--insets', type=str)
  225. parser.add_argument('--minimal_area', type=float)
  226. parser.add_argument('--buffer_distance', type=float)
  227. parser.add_argument('--simplify_tolerance', type=float)
  228. parser.add_argument('--viewport', type=str)
  229. parser.add_argument('--longitude0', type=str)
  230. parser.add_argument('--projection', type=str)
  231. parser.add_argument('--name', type=str)
  232. parser.add_argument('--language', type=str)
  233. parser.add_argument('--input_file_encoding', type=str)
  234. parser.add_argument('--precision', type=int)
  235. args = vars(parser.parse_args())
  236. default_args = {
  237. 'buffer_distance': -0.4,
  238. 'longitude0': '0',
  239. 'projection': 'mill',
  240. 'name': 'world',
  241. 'language': 'en',
  242. 'precision': 2,
  243. 'insets': ''
  244. }
  245. if args['input_file'][-4:] == 'json':
  246. args.update( json.loads( open(args['input_file'], 'r').read() ) )
  247. for key in default_args:
  248. if default_args.get(key) and args.get(key) is None:
  249. args[key] = default_args[key]
  250. converter = Converter(args)
  251. converter.convert(args['output_file'])