Search code examples
xsltgeojsongml-geographic-markup-lan

How to re-order a tokenized list in XSLT and read two values from it at a time?


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)


Solution

  • 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]]