1

I have some code (from GeoNetwork) which needs to convert Geography Markup Language (in XML) into GeoJSON. I'm currently trying to add functionality to read a polygon formed from a posList, but I'm having a hard time conceptualizing/drafting out what I would need to do.

The 'input' is basically a string consisting of a bunch of coordinates. So it might look something like this

<gml:LinearRing gml:id="p21" srsName="http://www.opengis.net/def/crs/EPSG/0/4326">
    <gml:posList srsDimension="2">45.67 88.56 55.56 88.56 55.56 89.44 45.67 89.44</gml:posList>
 </gml:LinearRing >

(Borrowed from Wikipedia's sample). I can chunk this up in XSLT using something like

<xsl:variable name="temp" as="xs:string*" select="tokenize(gml:LinearRing/gml:posList))" '\s'/>

which should give me Temp =

('45.67', '88.56', '55.56', '88.56', '55.56', '89.44', '45.67', '89.44')

Problem 1: GeoJSON wants everything in WGS 84 (EPSG 4326) and in the (long, lat) order - but strict adherence to WGS 84 rules (which I expect gml follows) means the coordinates are in (lat, long) order - so the list needs to be re-ordered. (I think - this is very confusing to me still)

Problem 2: GeoJSON wants coordinate pairs, but I just have a list of coordinates.

My current idea is to do something like this:

<geom>
<xsl:text>{"type": "Polygon",</xsl:text>
<xsl:text>"coordinates": [
[</xsl:text>

<xsl:variable name="temp" as="xs:string*" select="tokenize(gml:LinearRing/gml:posList))" '\s'/>
<xsl:for-each select="$temp">
  <xsl:if test="position() mod 2 = 0">
    <xsl:value-of select="concat('[', $saved, ', ', ., ']')" separator=","/>
  </xsl:if>
  <xsl:variable name="saved" value="."/>
</xsl:for-each>
<xsl:text>]
] 
}</xsl:text>
</geom>

but I'm unsure whether XSL will let me continuously write a variable like this, and whether there might be a better/more-efficient solution to the problem. (I have a lot of experience in MATLAB, where I would solve this quickly, if not efficiently, using for-loops)

Ideally I would get output similar to

<geom>
{"type": "Polygon",
"coordinates": [
  [
  [88.56, 45.67],
  [88.56, 55.56],
  [89.44, 55.56],
  [89.44, 45.67]
  ]
]
}
</geom>

(There's a whole other can-of-worms to be had with figuring out whether the polygon is right or left handed, I think)

Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
KDM
  • 81
  • 7

3 Answers3

2

This stylesheet with any input (not used)

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0"
                xmlns:my="dummy"
                exclude-result-prefixes="my">
   <xsl:template match="/">
      <xsl:sequence select="
            my:reverseByTuple(
                  ('45.67', '88.56', '55.56', '88.56', '55.56', '89.44', '45.67', '89.44')
            )"/>
   </xsl:template> 
   <xsl:function name="my:reverseByTuple">
        <xsl:param name="items"/>
        <xsl:sequence 
            select="if (empty($items))
                    then ()
                    else ($items[2], $items[1], my:reverseByTuple($items[position()>2]))"
                    />
    </xsl:function>
</xsl:stylesheet>

Output

88.56 45.67 88.56 55.56 89.44 55.56 89.44 45.67

I really don't understand why you are serializating the JSON instead of ussing a well documented library like the functions in XSLT 3.0... But just for fun, this stylesheet

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0"
                xmlns:my="dummy"
                exclude-result-prefixes="my">
   <xsl:template match="/">
      <xsl:value-of 
        select="
          my:encloseWithBracket(
            my:reverseByTupleEncloseWithBracket(
              ('45.67', '88.56', '55.56', '88.56', '55.56', '89.44', '45.67', '89.44')
            )
          )"/>
   </xsl:template> 
   <xsl:function name="my:reverseByTupleEncloseWithBracket">
        <xsl:param name="items"/>
        <xsl:sequence 
            select="if (empty($items))
                    then ()
                    else (my:encloseWithBracket(($items[2],$items[1])),
                          my:reverseByTupleEncloseWithBracket($items[position()>2]) )"
                    />
    </xsl:function>
   <xsl:function name="my:encloseWithBracket">
        <xsl:param name="items"/>
        <xsl:value-of select="concat('[',string-join($items,','),']')"/>
    </xsl:function>
</xsl:stylesheet>

Output

[[88.56,45.67],[88.56,55.56],[89.44,55.56],[89.44,45.67]]
Alejandro
  • 1,882
  • 6
  • 13
  • "I really don't understand why you are serializating the JSON instead of ussing a well documented library like the functions in XSLT 3.0" Well, for the most part it's because I am not actually aware of these functions! I've basically picked up everything I know about xslt from reading GeoNetwork code at this point... – KDM Apr 12 '19 at 15:26
  • Apologies for the delay, but this ended up being the answer I used, with some modifications: the main reasoning being that it supported an arbitrary number of points along with using XSLT 2.0 (due to the limitations of GeoNetwork, I can't use XSLT 3.0). I ended up being wrong about needing to reverse the points, so I was able to simplify this somewhat, and I had to do some string-join shenanigans because I couldn't call a template in the part of the code I was working in. Hopefully this helps if someone else encounters a similar problem. – KDM Sep 27 '19 at 19:13
  • I’m glad I was able to help. – Alejandro Sep 27 '19 at 20:09
1

XSLT 3 with XPath 3.1 support can represent JSON as maps/arrays and serialize them as JSON so you could compute an XPath map from your sequence of coordinates:

serialize(
    map { 
      'type' : 'polygon', 
      'coordinates' : array { 
          let $seq := tokenize(gml:LinearRing/gml:posList, '\s+') 
          return $seq[position() mod 2 = 0]![., let $p := position() return $seq[($p - 1) * 2 + 1]] 
         }
    },
    map { 'method' : 'json', 'indent' : true() }
)

https://xsltfiddle.liberty-development.net/gWvjQfu/1

To get JSON numbers in the arrays use let $seq := tokenize(., '\s+')!number() instead of let $seq := tokenize(gml:LinearRing/gml:posList, '\s+').

If you have access to an XSLT 3 processor like Saxon PE or EE or Altova supporting higher-order functions you could reduce that to

        serialize(
          map {
            'type': 'polygon',
            'coordinates': array {
                let $seq := tokenize(gml:LinearRing/gml:posList, '\s+'),
                    $odd := $seq[position() mod 2 = 1],
                    $even := $seq[position() mod 2 = 0]
                return
                    for-each-pair($odd, $even, function ($c1, $c2) {
                        [$c2, $c1]
                    })
            }
          }, 
          map {
            'method': 'json',
            'indent': true()
          }
        )
Martin Honnen
  • 160,499
  • 6
  • 90
  • 110
  • Good answer. About High Order Functions: a good recomendation is to test Zorba, an Open Source XQuery processor [supporting HOF](http://www.zorba.io/blog/68774681365/zorba-29--io). There is also a [live demo](http://try.zorba.io/) – Alejandro Apr 12 '19 at 13:27
  • 1
    @Alejandro, BaseX is also an open-source XQuery processor and it supports XQuery 3.1 with HOF and maps and arrays while Zorba has opted for JSONiq instead on top of XQuery 3.0. eXist-db is another XQuery 3 processor with maps and array support and has an online demo at http://demo.exist-db.org/exist/apps/eXide/index.html where above code runs fine. – Martin Honnen Apr 12 '19 at 13:48
  • This is a really clever answer but I think (not 100% sure yet) that our processor only supports XSLT 2.0. I'll try to confirm what version we support and if possible edit the question, but thank you for your time! – KDM Apr 15 '19 at 15:13
0

You can use the following XSLT-2.0 stylesheet to get your desired outcome. It makes use of the xsl:analyze-string function to separate the values in 2-tuples. The template includes error handling and removes the target namespace gml from the output with exclude-result-prefixes="gml". You may have to adjust the XML paths of the template and the xsl:analyze-string expression. But I guess that you can handle this.

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:gml="http://www.opengis.net/def/crs/EPSG/0/4326" exclude-result-prefixes="gml">
    <xsl:output method="xml" omit-xml-declaration="yes" />

    <xsl:template match="/">
<geom><xsl:text>
{"type": "Polygon",
"coordinates": [
  [
</xsl:text>
        <xsl:analyze-string select="gml:LinearRing/gml:posList" 
        regex="\s*(\d\d)\.(\d\d)\s+(\d\d)\.(\d\d)\s*"> 
            <xsl:matching-substring>
                <xsl:value-of select="concat('    [',regex-group(3),'.', regex-group(4),', ',regex-group(1),'.', regex-group(2),']&#xa;')"/>
            </xsl:matching-substring>
            <xsl:non-matching-substring>
                <xsl:message terminate="yes">=============================&#xA;=== ERROR: Invalid input! ===&#xA;=============================</xsl:message>
            </xsl:non-matching-substring>
        </xsl:analyze-string>
<xsl:text>  ]
]
}
</xsl:text>
</geom>
    </xsl:template>

</xsl:stylesheet>

Its output is:

<geom>
{"type": "Polygon",
"coordinates": [
  [
    [88.56, 45.67]
    [88.56, 55.56]
    [89.44, 55.56]
    [89.44, 45.67]
  ]
]
}
</geom>% 
zx485
  • 28,498
  • 28
  • 50
  • 59
  • Looking at this answer, it seems that this assumes the posList has exactly 4 entries. Unfortunately posList is merely a minimum of 4 entries - I chose a 4-entry posList just for simplicity. – KDM Apr 15 '19 at 15:11