{"id":192,"date":"2014-01-27T12:20:29","date_gmt":"2014-01-27T18:20:29","guid":{"rendered":"http:\/\/cartometric.com\/blog\/?p=192"},"modified":"2014-09-25T13:12:01","modified_gmt":"2014-09-25T18:12:01","slug":"postgresql-postgis-implementation-of-google-encoded-polyline-algorithm","status":"publish","type":"post","link":"https:\/\/elrobis.com\/blog\/2014\/01\/27\/postgresql-postgis-implementation-of-google-encoded-polyline-algorithm\/","title":{"rendered":"PostGREsql\/PostGIS Implementation of Google&#8217;s Encoded Polyline Algorithm"},"content":{"rendered":"<p><strong><span style=\"color: #ff0000;\">[Edit 30 Jan, 2014]<\/span><\/strong><\/p>\n<p><span style=\"color: #ff0000;\">I added an additional PostGREsql method to perform Polygon encoding by concatenating polygon geometries (delimiter: \u2020) and their inner rings (delimiter: \u2021) together into one massive encoded block of ring features. I also provided an example JavaScript method demonstrating how to bring the amalgamated polygon feature encodings into your Google Map.<\/span><\/p>\n<p>By some uncanny twist of the fates, I&#8217;ve <a title=\"Google Maps for Flash\/Flex: Dead Man Walking\" href=\"http:\/\/cartometric.com\/blog\/2011\/10\/16\/google-maps-for-flashflex-dead-man-walking\/\" target=\"_blank\">elected to use<\/a>, <a title=\"Decode Google Map encoded points as Well Known Text (WKT) with Python\" href=\"http:\/\/cartometric.com\/blog\/2012\/10\/20\/decode-google-map-encoded-points-as-well-known-text-wkt-with-python\/\" target=\"_blank\">had to use<\/a>, and\/or been asked to develop applications that use <a title=\"Encoded Polyline Algorithm Format\" href=\"https:\/\/developers.google.com\/maps\/documentation\/utilities\/polylinealgorithm\" target=\"_blank\">Google Maps ASCII Encoded Polyline<\/a> expressions. In previous encounters, I&#8217;ve used <a title=\"Gabriel Svennerberg (based on the work of Jim Hribar) | PolylineEncoder\" href=\"http:\/\/www.svennerberg.com\/examples\/polylines\/PolylineEncoder.php.txt\" target=\"_blank\">a PHP class<\/a> to handle the encoding task, and most recently I wrote a <a title=\"Decode Google Map encoded points as Well Known Text (WKT) with Python\" href=\"http:\/\/cartometric.com\/blog\/2012\/10\/20\/decode-google-map-encoded-points-as-well-known-text-wkt-with-python\/\" target=\"_blank\">Python method to decode these expressions<\/a> so that I could return a 3rd-party&#8217;s encoded geometries to WKT and import them into a spatially aware database.<\/p>\n<p>So far so good.<\/p>\n<p>However one thing has always bugged me about using the PHP solution&#8211;I don&#8217;t like using a piece of <a title=\"Middleware\" href=\"http:\/\/en.wikipedia.org\/wiki\/Middleware\" target=\"_blank\">middleware<\/a> to handle what I consider to be a responsibility of the data layer. <a title=\"Encoding polylines for Google Maps\" href=\"http:\/\/facstaff.unca.edu\/mcmcclur\/GoogleMaps\/EncodePolyline\/\" target=\"_blank\">Mark McClure&#8217;s page<\/a>, which is basically <a href=\"http:\/\/stackoverflow.com\/a\/10718902\/786131\" target=\"_blank\">the seminal authority on this topic<\/a>, provides external links to implementations for <a href=\"http:\/\/www.usnaviguide.com\/google-encode.htm\" target=\"_blank\">Perl<\/a>, <a href=\"http:\/\/facstaff.unca.edu\/mcmcclur\/GoogleMaps\/EncodePolyline\/gmap_polyline_encoder.rb.txt\" target=\"_blank\">Ruby<\/a>, <a href=\"http:\/\/facstaff.unca.edu\/mcmcclur\/GoogleMaps\/EncodePolyline\/PolylineEncoder.php.txt\" target=\"_blank\">PHP<\/a> (note: I prefer the PHP class linked, above), <a href=\"http:\/\/facstaff.unca.edu\/mcmcclur\/GoogleMaps\/EncodePolyline\/JavaPolylineEncoder.zip\" target=\"_blank\">Java<\/a>, and <a href=\"http:\/\/facstaff.unca.edu\/mcmcclur\/GoogleMaps\/GPXToGoogleMap\/\" target=\"_blank\">Mathematica<\/a>. Also, by searching Stack Overflow, you can find implementations of the algorithm in both <a href=\"http:\/\/stackoverflow.com\/a\/3852420\/786131\" target=\"_blank\">C#<\/a> and <a href=\"http:\/\/stackoverflow.com\/a\/13890455\/786131\" target=\"_blank\">VB.Net<\/a>. But for all my efforts searching, I could never dredge up an implementation for either MySQL or PostGREsql\/PostGIS. Bummer.<\/p>\n<p>Looking up, <a href=\"https:\/\/github.com\/postgis\/postgis\/pull\/9http:\/\/\" target=\"_blank\">it seems version 2.2 of PostGIS might include some built-in Google encoding conversion methods<\/a>. While this is cool enough for a hat tip, unfortunately, it&#8217;s too inconvenient to wait that long, and even then, there&#8217;s no guarantee the implementation will work the way I expect with complex Polygon geometries; for instance, maybe it will encode only the exterior ring of Polygons, ignoring MultiPolygons completely, etc. For that matter, it&#8217;s equally possible there could be some bugs. So with this said, and even though the previously-mentioned PHP implementation does the job, my boss was cool-enough to let me take a crack at implementing the algorithm as a PostGREsql\/PostGIS function, and then share the results with the world. Since some initial testing confirms my PostGIS implementation works, I&#8217;ll just post the code parts and hope others find it useful.<\/p>\n<p>For what it&#8217;s worth, if anyone finds a bug or has recommendations for improvements, please don&#8217;t hesitate to drop me a line.<\/p>\n<p>&nbsp;<\/p>\n<p><strong>Sample query calling the first encoding function on the EXTERIOR RING of Polygon geometries:<br \/>\n(Also works on single-part LINESTRING features.)<\/strong><\/p>\n<pre>\/************************************************************************\r\n * Note that the encoding method can accept a LINESTRING only, which\r\n * is the geometry type used to represent the ring parts of a Polygon.\r\n * To help understand this, and why, please see the trailing discussion\r\n * section, which elaborates on this situation further.\r\n ************************************************************************\/\r\nSELECT\r\n  GoogleEncodeLine(ST_ExteriorRing(wkb_geometry)) as Google\r\n  FROM polygons_wgs84\r\n  WHERE ST_GeometryType(wkb_geometry) = 'ST_Polygon'\r\n  LIMIT 10 ;<\/pre>\n<p>&nbsp;<\/p>\n<p><strong><span style=\"color: #ff0000;\">[Added 30 Jan, 2014]<\/span><\/strong><\/p>\n<p><strong>Sample query calling the second encoding function on Polygon and MultiPolygon geometries:<br \/>\n(Preserves multi-part polygons and their inner-ring parts, a.k.a. &#8220;holes&#8221;.)<br \/>\n<\/strong><\/p>\n<pre>\/************************************************************************\r\n * This encoding method will accept Polygon and MultiPolygon geom types.\r\n * The output returned is an amalgamation of Polyline encodings, where\r\n * individual geometries and their interior rings are concatenated\r\n * together using string delimiters, \u2020, and \u2021, respectively.\r\n ************************************************************************\/\r\nSELECT\r\n  GoogleEncodePolygon(wkb_geometry) as GooglePolygon\r\n  FROM polygons_wgs84\r\n  LIMIT 10 ;<\/pre>\n<p>&nbsp;<\/p>\n<p><strong>Implementation functions to execute\/save in your PostGREsql instance:<\/strong><\/p>\n<p><strong><span style=\"color: #ff0000;\">[Added 30 Jan, 2014]<\/span><\/strong><\/p>\n<pre>\/*************************************************************\r\n * Pass in either a Polygon or MultiPolygon geometry. Returns\r\n * an array of ASCII-encoded Polygon feature parts, including\r\n * multi-part geometries and their interior rings.\r\n ************************************************************\/\r\nCREATE OR REPLACE FUNCTION GoogleEncodePolygon\r\n(\r\n  g1 GEOMETRY\r\n)\r\nRETURNS TEXT AS $$\r\nDECLARE\r\n ng INT;        -- Store number of Geometries in the Polygon.\r\n g INT;         -- Counter for the current geometry number during outer loop.\r\n g2 GEOMETRY;   -- Current geometry feature isolated by the outer loop.\r\n nr INT;        -- Store number of internal ring parts in the Polygon.\r\n r INT;         -- Counter for the current inner-ring part.\r\n r1 GEOMETRY;   -- Exterior ring part isolated BEFORE the inner loop.\r\n r2 GEOMETRY;   -- Inner-ring part isolated within the inner loop.\r\n gEncoded TEXT; -- Completed Google Encoding.\r\nBEGIN\r\n gEncoded = '';\r\n ng = ST_NumGeometries(g1);\r\n g = 1;\r\n FOR g IN 1..ng BY 1 LOOP\r\n     g2 = ST_GeometryN(g1, g);\r\n     if g &gt; 1 then gEncoded = gEncoded || chr(8224); END IF;\r\n     -- Get ExteriorRing now; if there are any holes, get them later in the loop..\r\n     r1 = ST_ExteriorRing(g2);\r\n     gEncoded = gEncoded || GoogleEncodeLine(r1);\r\n     nr = ST_NRings(g2);\r\n     if nr &gt; 1 then\r\n       -- One (1) is because interior rings is one-based.\r\n       -- And nr-1 is because ring count includes the boundary.\r\n       FOR r IN 1..(nr-1) BY 1 LOOP\r\n         r2 = ST_InteriorRingN(g2, r);\r\n         gEncoded = gEncoded || chr(8225) || GoogleEncodeLine(r2);\r\n       END LOOP;\r\n     END IF;\r\n END LOOP;\r\n RETURN gEncoded;\r\nEnd\r\n$$ LANGUAGE plpgsql;<\/pre>\n<p>&nbsp;<\/p>\n<pre>\/*************************************************************\r\n * First of two methods. Pass in a geometry (LINESTRING only).\r\n * Returns ASCII-encoded point array for use in Google Maps.\r\n ************************************************************\/\r\nCREATE OR REPLACE FUNCTION GoogleEncodeLine\r\n(\r\n  g GEOMETRY\r\n)\r\nRETURNS TEXT AS $$\r\nDECLARE\r\n  pt1 GEOMETRY;\r\n  pt2 GEOMETRY;\r\n  p INT; np INT;\r\n  deltaX INT;\r\n  deltaY INT;\r\n  enX VARCHAR(255);\r\n  enY VARCHAR(255);\r\n  gEncoded TEXT;\r\nBEGIN\r\n  gEncoded = '';\r\n  np = ST_NPoints(g);\r\n\r\n  IF np &gt; 3 THEN\r\n    g = ST_SimplifyPreserveTopology(g, 0.00001);\r\n    np = ST_NPoints(g);\r\n  END IF;\r\n\r\n  pt1 = ST_SetSRID(ST_MakePoint(0, 0),4326);\r\n\r\n  FOR p IN 1..np BY 1 LOOP\r\n    pt2 = ST_PointN(g, p);\r\n    deltaX = (floor(ST_X(pt2)*1e5)-floor(ST_X(pt1)*1e5))::INT;\r\n    deltaY = (floor(ST_Y(pt2)*1e5)-floor(ST_Y(pt1)*1e5))::INT;\r\n    enX = GoogleEncodeSignedInteger(deltaX);\r\n    enY = GoogleEncodeSignedInteger(deltaY);\r\n    gEncoded = gEncoded || enY || enX;\r\n\r\n    pt1 = ST_SetSRID(ST_MakePoint(ST_X(pt2), ST_Y(pt2)),4326);\r\n  END LOOP;\r\nRETURN gEncoded;\r\nEnd\r\n$$ LANGUAGE plpgsql;<\/pre>\n<p>&nbsp;<\/p>\n<pre>\/**************************************************************\r\n * Second of two methods. Accepts a signed integer (LON or LAT\r\n * by 1e5) and returns an ASCII-encoded coordinate expression.\r\n *************************************************************\/\r\nCREATE OR REPLACE FUNCTION GoogleEncodeSignedInteger(c INT)\r\nRETURNS VARCHAR(255) AS $$\r\nDECLARE\r\n\u00a0 e VARCHAR(255);\r\n\u00a0 s BIT(32);\r\n\u00a0 b BIT(6);\r\n\u00a0 n INT;\r\nBEGIN\r\n\u00a0e = '';\r\n\u00a0s = (c::BIT(32))&lt;&lt;1;\r\n\r\n\u00a0IF s::INT &lt; 0 THEN\r\n\u00a0\u00a0 s = ~s;\r\n\u00a0\u00a0 END IF;\r\n\r\n\u00a0WHILE s::INT &gt;= B'100000'::INT LOOP\r\n\u00a0\u00a0 b = B'100000' | (('0'||substring(s, 28, 5))::BIT(6));\r\n\u00a0\u00a0 n = b::INT + 63;\r\n\u00a0\u00a0 e = e || chr(n);\r\n\u00a0\u00a0 s = s &gt;&gt; 5;\r\n\u00a0END LOOP;\r\n\u00a0e = e || chr(s::INT+63);\r\n\r\nRETURN e;\r\nEnd\r\n$$ LANGUAGE plpgsql;<\/pre>\n<p><span style=\"color: #ff0000;\">\u00a0<\/span><\/p>\n<p><strong><span style=\"color: #ff0000;\">[Added 30 Jan, 2014]<\/span><\/strong><\/p>\n<p><strong>JavaScript method demonstrating how to add Polygon encodings to a Google Map object:<br \/>\n(This client implementation works for either the single or the multi-part polygons.)<br \/>\n<\/strong><\/p>\n<pre>\/*************************************************************\r\n * JavaScript! Pass-in an encoded text block created by either\r\n * PostGIS method, GoogleEncodePolygon() or GoogleEncodeLine(),\r\n * and render it in your Google Map object. If you don't want\r\n * the map to zoom to each rendering, just remove the \"bounds\"\r\n * variable and any references to it.\r\n ************************************************************\/\r\nfunction renderEncoded(encoded_path)\r\n{\r\n   var bounds = new google.maps.LatLngBounds();\r\n   var $encodedGeoms = encoded_path.split(\"\u2020\");\r\n   for (var i=0; i&lt;$encodedGeoms.length; i++)\r\n   {\r\n       var encodedGeom = $encodedGeoms[i];\r\n       var $encodedRings = encodedGeom.split(\"\u2021\");\r\n       var polyPaths = [];\r\n       for (var j=0; j&lt;$encodedRings.length; j++)\r\n       {\r\n           var ptarray = google.maps.geometry.encoding.decodePath($encodedRings[j]);\r\n           polyPaths.push(ptarray);\r\n       }\r\n       var polygonObject = new google.maps.Polygon(\r\n       {\r\n         paths: polyPaths,\r\n         strokeColor: '#890000',\r\n         strokeOpacity: 1.0,\r\n         strokeWeight: 2\r\n       });\r\n       polygonObject.setMap(map);\r\n       polygonObject.getPath().forEach(function(e)\r\n       {\r\n           bounds.extend(e);\r\n       });\r\n   }\r\n   map.fitBounds(bounds);\r\n}<\/pre>\n<p>&nbsp;<\/p>\n<p><strong>And some additional discussion..<\/strong><\/p>\n<p>There are two &#8220;gotchas&#8221; when it comes to implementing the encoding algorithm with respect to Polygons:<\/p>\n<p>1) Polygons, as geometries, can be composed of many rings. The outer ring is considered to be the boundary, and various inner rings are often called &#8220;holes&#8221;. So this is a specified, understood, and accepted built-in many-to-one relationship between polygons and their internal ring geometries.<\/p>\n<p>And 2) It&#8217;s not rare to find polygon tables containing both Polygon and MultiPolygon data types. <a title=\"PostGIS: count all features of each GeometryType in a spatial table\" href=\"http:\/\/cartometric.com\/blog\/2012\/01\/21\/postgis-count-all-features-of-each-geometrytype-in-a-spatial-table\/\" target=\"_blank\">I think this happens because ESRI allows it,<\/a> and so in an effort to play well with others, other GIS systems have accommodated it. At least, I know this is true for MySQL and PostGIS.<\/p>\n<p>Here&#8217;s why this makes trouble&#8211;Google&#8217;s encoding algorithm is only intended to represent individual point arrays as a singular geometry. Basically, as long as your first point equals your last point, it&#8217;s considered to be a closed geometry, and you can add it and render it in a Google Map as a polygon. The algorithm itself isn&#8217;t designed to represent nested arrays, which would be necessary to render either a Polygon with &#8220;holes&#8221; or a MultiPolygon, which could potentially define many polygons with holes of their own! As such, I suspect there could be considerable disagreement as to how a Polygon-to-Google-Encoded method should actually handle Polygons..<\/p>\n<p>The only solutions I can imagine for this problem would require &#8220;faking&#8221; a one-to-many relationship by perhaps delimiting together several encodings to account for MultiPolygons and\/or single feature Polygons with interior rings. But this starts to get weird. So to keep things somewhat simple for the sake of the post, I chose to stay true to the algorithm&#8217;s intent and return a single encoded geometry expression from my method. And the sample query demonstrates this by calling the method against the outermost ring (i.e. the boundary) of a Polygon geometry type, which PostGREsql regards as a LineString, anyway.<\/p>\n<p><strong><span style=\"color: #ff0000;\">[Added 30 Jan, 2014]<\/span><\/strong><\/p>\n<p>Since I wanted to handle the more complex geometries, I wrote the wrapper method <code>GoogleEncodePolygon()<\/code>, to first iterate over <a href=\"http:\/\/postgis.net\/docs\/ST_NumGeometries.html\" target=\"_blank\"><code>ST_NumGeometries()<\/code><\/a> and gain access to any multi-part features, then second, iterate over <a href=\"http:\/\/postgis.net\/docs\/ST_NRings.html\" target=\"_blank\"><code>ST_NRings()<\/code><\/a> using <a href=\"http:\/\/postgis.net\/docs\/ST_InteriorRingN.html\" target=\"_blank\"><code>ST_InteriorRingN()<\/code><\/a>&#8211;you could also do this using <a href=\"http:\/\/postgis.net\/docs\/ST_DumpRings.html\" target=\"_blank\"><code>ST_DumpRings()<\/code><\/a>&#8211;and gain access to any interior rings of the Polygon geometries, themselves. Then, for each ring part, I call <code>GoogleEncodeLine()<\/code>, and concatenate together all those expressions into one massive block of &#8220;compound&#8221; expressions. I chose to delimit each geometry encoding using an extra-special character that would never be used by Google&#8217;s algorithm; for example <code>chr(8224)<\/code>, which corresponds to &#8220;\u2020&#8221;. I then further delimit the internal ring parts using another special character, <code>chr(8225)<\/code>,\u00a0which corresponds to &#8220;\u2021&#8221;, and return all these concatenated together as a compound encoding expression. Then, on the client-side (a JavaScript example is provided above), I merely split the compound expression against my delimiters, loop over the various expressions, and add them to the map individually. Note if you are attaching attributes to your features, you&#8217;ll need to remember to include them explicitly to each unique Polygon added to your map.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>[Edit 30 Jan, 2014] I added an additional PostGREsql method to perform Polygon encoding by concatenating polygon geometries (delimiter: \u2020) and their inner rings (delimiter: \u2021) together into one massive encoded block of ring features. I also provided an example JavaScript method demonstrating how to bring the amalgamated polygon feature encodings into your Google Map. [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":[],"categories":[27,26,24,42,45,23],"tags":[18,5,48,17,16,49],"_links":{"self":[{"href":"https:\/\/elrobis.com\/blog\/wp-json\/wp\/v2\/posts\/192"}],"collection":[{"href":"https:\/\/elrobis.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/elrobis.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/elrobis.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/elrobis.com\/blog\/wp-json\/wp\/v2\/comments?post=192"}],"version-history":[{"count":11,"href":"https:\/\/elrobis.com\/blog\/wp-json\/wp\/v2\/posts\/192\/revisions"}],"predecessor-version":[{"id":256,"href":"https:\/\/elrobis.com\/blog\/wp-json\/wp\/v2\/posts\/192\/revisions\/256"}],"wp:attachment":[{"href":"https:\/\/elrobis.com\/blog\/wp-json\/wp\/v2\/media?parent=192"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/elrobis.com\/blog\/wp-json\/wp\/v2\/categories?post=192"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/elrobis.com\/blog\/wp-json\/wp\/v2\/tags?post=192"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}