# # 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['output_file'])