Skip to main content

SVG-to-WKT: Converting vector graphics into spatial coordinates

Feb 18, 2013 • dm4fn

[Cross-posted with dclure.org]

Last week, I wrote about the some of the new functionality in Neatline that makes it possible to take SVG documents created in vector-editing programs like Adobe Illustrator and drag them out as spatial geometry on the map. Under the hood, this involves converting the raw SVG markup – which encodes geometry relative to a “document” space (think of pixels in a Photoshop file) – into latitude/longitude coordinates that can be rendered dynamically on the map. Specifically, I needed to generate Well-Known Text (WKT), the serialization format used by spatially-enabled relational databases like PostGIS and MySQL.

It turned out that there wasn’t any pre-existing utility for this, so I wrote a little library called SVG-to-WKT that does the conversion.

The top-level convert method takes a raw SVG document and spits back the equivalent WKT GEOMETRYCOLLECTION:

SVGtoWKT.convert('<svg><polygon points="1,2 3,4 5,6" /><line x1="7" y1="8" x2="9" y2="10" /></svg>');
>>> "GEOMETRYCOLLECTION(POLYGON((1 -2,3 -4,5 -6,1 -2)),LINESTRING(7 -8,9 -10))"

The library supports all SVG elements that directly encode geometry information, and exposes the individual helper methods that handle each of the elements:

line

SVGtoWKT.line(1, 2, 3, 4);
>>> "LINESTRING(1 -2,3 -4)"

polyline

SVGtoWKT.polyline('1,2 3,4');
>>> "LINESTRING(1 -2,3 -4)"

polygon

SVGtoWKT.polygon('1,2 3,4');
>>> "POLYGON((1 -2,3 -4,1 -2))"

rect

SVGtoWKT.rect(1, 2, 3, 4);
>>> "POLYGON((1 -2,4 -2,4 -6,1 -6,1 -2))"

circle

SVGtoWKT.circle(0, 0, 10);
>>> "POLYGON((10 0,9.95 -0.996,9.802 -1.981,9.556 -2.948,9.215 -3.884,8.782 -4.783,8.262 -5.633,7.66 -6.428,6.982 -7.159,6.235 -7.818,5.425 -8.4,4.562 -8.899,3.653 -9.309,2.708 -9.626,1.736 -9.848,0.747 -9.972,-0.249 -9.997,-1.243 -9.922,-2.225 -9.749,-3.185 -9.479,-4.113 -9.115,-5 -8.66,-5.837 -8.119,-6.617 -7.498,-7.331 -6.802,-7.971 -6.038,-8.533 -5.214,-9.01 -4.339,-9.397 -3.42,-9.691 -2.468,-9.888 -1.49,-9.988 -0.498,-9.988 0.498,-9.888 1.49,-9.691 2.468,-9.397 3.42,-9.01 4.339,-8.533 5.214,-7.971 6.038,-7.331 6.802,-6.617 7.498,-5.837 8.119,-5 8.66,-4.113 9.115,-3.185 9.479,-2.225 9.749,-1.243 9.922,-0.249 9.997,0.747 9.972,1.736 9.848,2.708 9.626,3.653 9.309,4.562 8.899,5.425 8.4,6.235 7.818,6.982 7.159,7.66 6.428,8.262 5.633,8.782 4.783,9.215 3.884,9.556 2.948,9.802 1.981,9.95 0.996,10 0))"

ellipse

SVGtoWKT.ellipse(0, 0, 10, 20);
>>> "POLYGON((10 0,9.98 -1.268,9.92 -2.532,9.819 -3.785,9.679 -5.023,9.501 -6.241,9.284 -7.433,9.029 -8.596,8.738 -9.724,8.413 -10.813,8.053 -11.858,7.66 -12.856,7.237 -13.802,6.785 -14.692,6.306 -15.523,5.801 -16.292,5.272 -16.995,4.723 -17.629,4.154 -18.193,3.569 -18.683,2.969 -19.098,2.358 -19.436,1.736 -19.696,1.108 -19.877,0.476 -19.977,-0.159 -19.997,-0.792 -19.937,-1.423 -19.796,-2.048 -19.576,-2.665 -19.277,-3.271 -18.9,-3.863 -18.447,-4.441 -17.92,-5 -17.321,-5.539 -16.651,-6.056 -15.915,-6.549 -15.115,-7.015 -14.254,-7.453 -13.335,-7.861 -12.363,-8.237 -11.341,-8.58 -10.274,-8.888 -9.165,-9.161 -8.019,-9.397 -6.84,-9.595 -5.635,-9.754 -4.406,-9.874 -3.16,-9.955 -1.901,-9.995 -0.635,-9.995 0.635,-9.955 1.901,-9.874 3.16,-9.754 4.406,-9.595 5.635,-9.397 6.84,-9.161 8.019,-8.888 9.165,-8.58 10.274,-8.237 11.341,-7.861 12.363,-7.453 13.335,-7.015 14.254,-6.549 15.115,-6.056 15.915,-5.539 16.651,-5 17.321,-4.441 17.92,-3.863 18.447,-3.271 18.9,-2.665 19.277,-2.048 19.576,-1.423 19.796,-0.792 19.937,-0.159 19.997,0.476 19.977,1.108 19.877,1.736 19.696,2.358 19.436,2.969 19.098,3.569 18.683,4.154 18.193,4.723 17.629,5.272 16.995,5.801 16.292,6.306 15.523,6.785 14.692,7.237 13.802,7.66 12.856,8.053 11.858,8.413 10.813,8.738 9.724,9.029 8.596,9.284 7.433,9.501 6.241,9.679 5.023,9.819 3.785,9.92 2.532,9.98 1.268,10 0))"

path

SVGtoWKT.path('M10 10 C 20 20, 40 20, 50 10Z');
>>> "POLYGON((10 -10,10.722 -10.689,11.474 -11.344,12.255 -11.964,13.062 -12.551,13.894 -13.102,14.747 -13.62,15.62 -14.103,16.51 -14.552,17.417 -14.968,18.339 -15.35,19.273 -15.7,20.219 -16.018,21.175 -16.304,22.139 -16.558,23.112 -16.782,24.09 -16.974,25.075 -17.137,26.064 -17.269,27.056 -17.371,28.051 -17.443,29.048 -17.486,30.045 -17.5,31.043 -17.484,32.04 -17.438,33.035 -17.363,34.027 -17.258,35.015 -17.123,35.999 -16.958,36.977 -16.763,37.949 -16.536,38.913 -16.279,39.868 -15.99,40.813 -15.67,41.746 -15.317,42.666 -14.931,43.571 -14.512,44.461 -14.06,45.332 -13.574,46.183 -13.053,47.012 -12.498,47.817 -11.909,48.595 -11.285,49.345 -10.627,49.909 -10,48.911 -10,47.914 -10,46.916 -10,45.918 -10,44.92 -10,43.923 -10,42.925 -10,41.927 -10,40.929 -10,39.932 -10,38.934 -10,37.936 -10,36.939 -10,35.941 -10,34.943 -10,33.945 -10,32.948 -10,31.95 -10,30.952 -10,29.954 -10,28.957 -10,27.959 -10,26.961 -10,25.964 -10,24.966 -10,23.968 -10,22.97 -10,21.973 -10,20.975 -10,19.977 -10,18.98 -10,17.982 -10,16.984 -10,15.986 -10,14.989 -10,13.991 -10,12.993 -10,11.995 -10,10.998 -10,10 -10))"

If you look at the output strings, you’ll notice that the Y-axis coordinates in the WKT are inverted relative to the input: SVGtoWKT.polyline('1,2 3,4') returns LINESTRING(1 -2,3 -4), not LINESTRING(1 2,3 4). This is because the Y-axis “grows” in the opposite direction on maps as it does in document space. In Illustrator, the coordinate grid starts at the top left corner, and the Y-axis increases as you move down on the page; on maps, the Y-axis increases as you move “up,” to the north. SVG-to-WKT just flips the Y-axis coordinates to make the orientation correct on the map.

TODO

  • Make it work in Node.js. This is actually a bit trickier that I thought it would be, because Node doesn’t implement the browser-native methods that jQuery’s parseXML uses. It may make sense to move to a generic XML parser that works in Node, which would be lighter-weight than jQuery anyway.

  • Instead of just being purely functional (SVG in, WKT out), it might be useful to return some sort of SVGDocument object that could then be used to generate specific WKT strings at different density levels, orientations, etc. This would have come in handy while writing the custom OpenLayers handler that Neatline uses to actually position the generated WKT on the map (more on this later).

  • Get rid of the Underscore.js dependency.