svgtogeojson.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. #!/usr/bin/python
  2. import sys
  3. import json,copy,re
  4. from xml.etree import ElementTree as ET
  5. import numpy as np
  6. from more_itertools import peekable
  7. np.cosd = lambda x: np.cos(x*np.pi/180)
  8. np.sind = lambda x: np.sin(x*np.pi/180)
  9. xscale = 0.01
  10. yscale = 0.01
  11. geodata = { "type": "FeatureCollection", "features": [] }
  12. featuretemplate = {
  13. "type": "Feature",
  14. "properties": { "tags": { "name": "default room name" },
  15. "relations": [ { "reltags": { "height": "4", "level": "0", "type": "level" } } ],
  16. "meta": {}
  17. },
  18. "geometry": {
  19. "type": "Polygon",
  20. "coordinates": [[]]
  21. }
  22. }
  23. def SVGTransforms(s,pts):
  24. pts = [ np.reshape(p+[1],(3,1)) for p in pts ]
  25. transforms = re.findall("[a-zA-Z]+\([^\)]+\)",s.get('transform') or '')
  26. # reverse order because SVG transforms apply right-to-left
  27. transforms.reverse()
  28. mat = np.identity(3)
  29. for t in transforms:
  30. if t.startswith("matrix"):
  31. t = re.sub("[a-zA-Z]+\(([^\)]+)\)","\\1",t)
  32. mat = matrixTransform( [ float(n) for n in re.split("[ ,]",t) ], mat )
  33. # TODO: translate, skewX, skewY transforms
  34. elif t.startswith("translate"):
  35. pass
  36. elif t.startswith("skew"):
  37. pass
  38. elif t.startswith("scale"):
  39. t = re.sub("[a-zA-Z]+\(([^\)]+)\)","\\1",t)
  40. n = re.split("[ ,]",t)
  41. sx = float(n[0])
  42. sy = float(n[1]) if len(n)>1 else sx # y scale is optional
  43. mat = matrixTransform( [ sx,0,0,sy,0,0 ], mat )
  44. elif t.startswith("rotate"):
  45. # TODO: rotate about a given point using translate() and this code
  46. t = re.sub("[a-zA-Z]+\(([^\)]+)\)","\\1",t)
  47. n = re.split("[ ,]",t)
  48. a = float(n[0])
  49. x = float(n[1]) if len(n)>1 else None
  50. y = float(n[2]) if len(n)>1 else None
  51. mat = matrixTransform( [ np.cosd(a), np.sind(a), -np.sind(a), np.cosd(a) ], mat )
  52. # this is gross
  53. return [ np.dot(mat,p)[0:2].transpose().tolist()[0] for p in pts ]
  54. def transformPoint(pt):
  55. # Inkscape uses +y up coordinates internally
  56. # this doesn't invert the y-axis for now, but might at some point...
  57. return np.add(np.multiply(pt,[1*xscale,1*yscale]),[0,0]).tolist()
  58. def matrixTransform(L,R):
  59. # L is given in SVG order, abcdef, zero-padded if 6 elements not given
  60. L += ([0]*(6-len(L)) if len(L) < 6 else [])
  61. L = np.vstack( [ np.reshape( L, (3,2) ).transpose(), [0,0,1] ] )
  62. return np.dot( L, R )
  63. def getCoord(iterable):
  64. return float(iterable.next())
  65. def main():
  66. ns="http://www.w3.org/2000/svg"
  67. if len(sys.argv) > 1 :
  68. try:
  69. xmltree = ET.parse(sys.argv[1])
  70. except IOError:
  71. print("Error, not a valid file")
  72. sys.exit(1)
  73. except:
  74. print("Unknown error")
  75. sys.exit(1)
  76. else:
  77. print("No SVG to parse specified!")
  78. print("Usage: python svgRectToGeoJSON.py file.svg")
  79. sys.exit(1)
  80. shapes = []
  81. shapes += xmltree.getroot().findall("./{%s}g/{%s}rect"%(ns,ns))
  82. shapes += xmltree.getroot().findall("./{%s}g/{%s}path"%(ns,ns))
  83. for s in shapes:
  84. shapeid = s.get('id')
  85. if ( s.tag == '{%s}rect'%ns ):
  86. x = float(s.get('x'))
  87. y = float(s.get('y'))
  88. w = float(s.get('width'))
  89. h = float(s.get('height'))
  90. pts = [ [x,y],[x+w,y],[x+w,y+h],[x,y+h],[x,y] ]
  91. pts = SVGTransforms(s,pts)
  92. geomObject(pts,name=shapeid)
  93. elif ( s.tag == '{%s}path'%ns ):
  94. pathdata = re.split("([a-zA-Z, ])",s.get('d'))
  95. # split ops and their arguments, discard whitespace and commas
  96. # peekable() is used instead of iter() so we can peek() at next value
  97. ops = peekable( filter( (lambda str: not str.isspace() and str not in ['',',']), pathdata))
  98. spot = [0,0]
  99. pts = []
  100. for o in ops:
  101. if( o.isalpha() ):
  102. if ( o == 'Z' or o == 'z' ): # close path command
  103. pts.append( pts[0] )
  104. break
  105. elif ( o == 'M' or o == 'm' ):
  106. pts = []
  107. while not ops.peek().isalpha():
  108. if ( o == 'M' ):
  109. spot = [getCoord(ops),getCoord(ops)]
  110. elif ( o == 'm' ):
  111. spot[0] += getCoord(ops)
  112. spot[1] += getCoord(ops)
  113. pts.append(spot)
  114. print >> sys.stderr, "Path pts %s"%pts
  115. pts = SVGTransforms(s,pts)
  116. geomObject(pts,name=shapeid)
  117. elif ( o == 'H' ): # horizontal line command
  118. spot[0] = getCoord(ops)
  119. elif ( o == 'h' ):
  120. spot[0] += getCoord(ops)
  121. elif ( o == 'V' ): # vertical line command
  122. spot[1] = getCoord(ops)
  123. elif ( o == 'v' ):
  124. spot[1] += getCoord(ops)
  125. elif ( o == 'L' ): # line command
  126. spot[0] = getCoord(ops)
  127. spot[1] = getCoord(ops)
  128. elif ( o == 'l' ):
  129. spot[0] += getCoord(ops)
  130. spot[1] += getCoord(ops)
  131. # the only non-drawing directives M and Z will skip this bit
  132. pts.append(spot)
  133. else:
  134. print >> sys.stderr, "Unexpected <path> operation %s, ignoring..." % o
  135. geomObject(pts,name=shapeid)
  136. else:
  137. print >> sys.stderr, "Unhandled tag %s encountered, skipping..." % s.tag
  138. print(json.dumps(geodata, indent=3))
  139. def geomObject(pts,name="default room"):
  140. f = copy.deepcopy(featuretemplate)
  141. for pt in pts:
  142. f['geometry']['coordinates'][0].append( transformPoint(pt) )
  143. f['properties']['tags']['name'] = name
  144. geodata['features'].append(f)
  145. if __name__ == "__main__":
  146. main()