Как переупорядочить токенизированный список в XSLT и прочитать из него два значения за раз?

У меня есть код (из GeoNetwork), который должен преобразовать язык разметки географии (в XML) в GeoJSON. В настоящее время я пытаюсь добавить функциональность для чтения многоугольника, сформированного из posList, но мне трудно концептуализировать/набросать то, что мне нужно сделать.

«Ввод» — это в основном строка, состоящая из набора координат. Так что это может выглядеть примерно так

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

(Заимствовано из образца Википедии). Я могу разбить это на XSLT, используя что-то вроде

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

что должно дать мне Temp =

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

Проблема 1: GeoJSON хочет, чтобы все было в WGS 84 (EPSG 4326) и в порядке (долгота, широта), но строгое соблюдение правил WGS 84 (которым, как я ожидаю, следует gml) означает, что координаты находятся в порядке (широта, долгота) - поэтому список нужно переупорядочить. (Я думаю - это меня все еще очень смущает)

Проблема 2: GeoJSON хочет пары координат, но у меня есть только список координат.

Моя текущая идея состоит в том, чтобы сделать что-то вроде этого:

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

но я не уверен, позволит ли XSL постоянно писать такую ​​переменную, и может ли быть лучшее/более эффективное решение проблемы. (У меня большой опыт работы с MATLAB, где я бы решил это быстро, если не эффективно, используя циклы for)

В идеале я бы получил результат, похожий на

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

(Я думаю, что есть еще целая куча червей, чтобы выяснить, является ли многоугольник правым или левым, я думаю)


person KDM    schedule 11.04.2019    source источник
comment
Какую версию XSLT вы можете использовать? 1.0, 2.0 или 3.0? Если вы можете использовать версию 3.0, этот ответ может решить вашу проблему.   -  person zx485    schedule 12.04.2019
comment
Интересный вопрос   -  person Alejandro    schedule 12.04.2019


Ответы (3)


Эта таблица стилей с любым вводом (не используется)

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

Выход

88.56 45.67 88.56 55.56 89.44 55.56 89.44 45.67

Я действительно не понимаю, почему вы сериализуете JSON вместо использования хорошо документированной библиотеки, такой как функции в XSLT 3.0... Но просто для удовольствия эта таблица стилей

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

Выход

[[88.56,45.67],[88.56,55.56],[89.44,55.56],[89.44,45.67]]
person Alejandro    schedule 11.04.2019
comment
Я действительно не понимаю, почему вы сериализуете JSON вместо использования хорошо документированной библиотеки, такой как функции в XSLT 3.0. Ну, по большей части это потому, что я на самом деле не знаю об этих функциях! На данный момент я собрал все, что знаю о xslt, из чтения кода GeoNetwork... - person KDM; 12.04.2019
comment
Извиняюсь за задержку, но в итоге это был ответ, который я использовал, с некоторыми изменениями: основная причина заключалась в том, что он поддерживал произвольное количество точек вместе с использованием XSLT 2.0 (из-за ограничений GeoNetwork я не могу использовать XSLT 3.0). В конце концов я ошибся в том, что нужно поменять местами точки, поэтому я смог несколько упростить это, и мне пришлось проделать некоторые махинации с соединением строк, потому что я не мог вызвать шаблон в той части кода, над которой я работал. Надеюсь, это поможет, если кто-то еще столкнется с подобной проблемой. - person KDM; 27.09.2019
comment
Я рад, что смог помочь. - person Alejandro; 27.09.2019

XSLT 3 с поддержкой XPath 3.1 может представлять JSON в виде карт/массивов и сериализовать их как JSON, чтобы вы могли вычислить карту XPath из вашей последовательности координат:

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

Чтобы получить числа JSON в массивах, используйте let $seq := tokenize(., '\s+')!number() вместо let $seq := tokenize(gml:LinearRing/gml:posList, '\s+').

Если у вас есть доступ к процессору XSLT 3, такому как Saxon PE или EE или Altova, поддерживающему функции более высокого порядка, вы можете сократить его до

        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()
          }
        )
person Martin Honnen    schedule 12.04.2019
comment
Хороший ответ. О функциях высокого порядка: рекомендуется протестировать Zorba, процессор XQuery с открытым исходным кодом поддерживает HOF. Существует также демонстрация. - person Alejandro; 12.04.2019
comment
@Alejandro, BaseX также является процессором XQuery с открытым исходным кодом и поддерживает XQuery 3.1 с HOF, картами и массивами, в то время как Zorba выбрал JSONiq вместо XQuery 3.0. eXist-db — это еще один процессор XQuery 3 с поддержкой карт и массивов, демонстрационная онлайн-версия которого доступна по адресу demo.exist-db.org/exist/apps/eXide/index.html, где приведенный выше код работает нормально. - person Martin Honnen; 12.04.2019
comment
Это действительно умный ответ, но я думаю (пока не уверен на 100%), что наш процессор поддерживает только XSLT 2.0. Я постараюсь подтвердить, какую версию мы поддерживаем, и, если возможно, отредактировать вопрос, но спасибо за ваше время! - person KDM; 15.04.2019

Вы можете использовать следующую таблицу стилей XSLT-2.0, чтобы получить желаемый результат. Он использует функцию xsl:analyze-string для разделения значений в двух кортежах. Шаблон включает обработку ошибок и удаляет целевое пространство имен gml из выходных данных с помощью exclude-result-prefixes="gml". Возможно, вам придется настроить пути XML шаблона и выражение xsl:analyze-string. Но я думаю, что ты справишься с этим.

<?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>

Вывод:

<geom>
{"type": "Polygon",
"coordinates": [
  [
    [88.56, 45.67]
    [88.56, 55.56]
    [89.44, 55.56]
    [89.44, 45.67]
  ]
]
}
</geom>% 
person zx485    schedule 11.04.2019
comment
Глядя на этот ответ, кажется, что предполагается, что в posList ровно 4 записи. К сожалению, posList состоит как минимум из 4 записей — я выбрал posList из 4 записей только для простоты. - person KDM; 15.04.2019