Geoproj does not have a library dependency for GEOS.

(:
 : Copyright 2006-2009 The FLWOR Foundation.
 :
 : Licensed under the Apache License, Version 2.0 (the "License");
 : you may not use this file except in compliance with the License.
 : You may obtain a copy of the License at
 :
 : http://www.apache.org/licenses/LICENSE-2.0
 :
 : Unless required by applicable law or agreed to in writing, software
 : distributed under the License is distributed on an "AS IS" BASIS,
 : WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 : See the License for the specific language governing permissions and
 : limitations under the License.
:)

(:~
 : Geographic projection module.

 : Forward and inverse projection from WGS84 lat-long coordinates to Oblique Mercator x-y projection.

 : Oblique Mercator projection is a sphere to cylinder projection.

 : This projection results in a conformal output, meaning the shape of small areas is preserved,
 : no matter the distance from the origin. But it is not equal area, meaning the area size increases heavily
 : when getting closer to North or South. The area size increases with the same amount on x and y axes,
 : so the relative shape remains almost the same.

 : Mercator projection is the oldest projection, and it is still widely used because it produces a rectangular map.
 : This projection is used in Google Maps because of its conformal output.

 : For military or measurements purposes the UTM projection is used (or variants). This splits the Earth into small
 : areas and computes the cartesian coordinates relative to each area.

 :

 : Here we use Oblique Mercator projection. Its advantage over the normal Mercator is that you can set the tangent point
 : between the cylinder and the sphere to be anywhere on Earth. So you can set the center of the map to be close
 : to the area you want projected and be able to measure accurately the distances between points and lines.
 : The map deformation is minimal close to the center point and close to the "equator" line.

 : The advantage over the UTM projection is that it can also produce a global rectangular map, like Mercator,
 : which is great for viewing.

 : The disadvantage over Mercator is that it needs more processing power.

 :

 : WGS84 is the ellipsoid aproximation of the Earth, with big radius of 6,378,137 m and small radius of 6,356,752.3 m.
 : The geographic coordinates expressed for this ellipsoid are widely used today in maps and gps coordinates.
 : It is the default standard for representing geographic coordinates.

 :

 : The purpose of this module is to provide convertion from polar to cartesian coordinates, so you can
 : process the geographic data with the Simple Features API functions implemented in the geo module.
 : That module works only with cartesian coordinates, but most maps have polar coordinates.

 :

 : The projection formulas are taken from lib_proj library and implemented in XQuery.
 :
 : @author Daniel Turcanu
 :)
module namespace geoproj = "http://www.zorba-xquery.com/modules/geoproj";
import module namespace math = "http://www.zorba-xquery.com/modules/math";

(:~
 : Import module for checking if geoproj parameters are validated.
 :)
import module namespace zorba-schema = "http://www.zorba-xquery.com/modules/schema";

(:~
 : Contains the definitions of the geoproj parameters.
:)
import schema namespace geoproj-param = "http://www.zorba-xquery.com/modules/geoproj-param";

declare namespace gml="http://www.opengis.net/gml";

(:~
 : Convert angle from degrees to radians.

 : The parameter is first reduced to value range of (-360, 360).
 :
 : @param $deg angle in  degrees
 : @return value in radians (-2PI, 2PI)
 :)
declare function geoproj:deg-to-rad($deg as xs:double) as xs:double
{
  ($deg mod 360) * 2 * math:pi() div 360
};

(:~
 : Convert angle from radians to degrees.

 :
 : @param $rad value in radians
 : @return value in degrees (-360, 360)
 :)
declare function geoproj:rad-to-deg($rad as xs:double) as xs:double
{
  ($rad * 360 div 2 div math:pi()) mod 360
};

(:~
 : Compute the isometric latitude of $phi latitude.
 :
 : @param $phi a latitude
 : @return isometric latitude in radians
 :)
declare %private function geoproj:proj-tsfn($phi as xs:double) as xs:double
{
   let $e as xs:double := 0.0818192E0
   return
   math:tan(math:pi() div 4 - $phi div 2) div math:pow((1 - $e * math:sin($phi)) div (1 + $e * math:sin($phi)), $e div 2)
};

declare %private function geoproj:wgs84-to-omerc-validated(
                                         $lat_0 as xs:double,
                                         $long_c as xs:double,
                                         $k0 as xs:double,
                                         $lat_long_degrees as element(geoproj-param:latlong, geoproj-param:latlongType)*) as element(geoproj-param:coord)*
{
  let $e as xs:double := 0.0818192E0
  let $e2 := $e*$e
  let $phi0 := geoproj:deg-to-rad($lat_0)
  let $lambda0 := geoproj:deg-to-rad($long_c)
  let $sin_phi0 := math:sin($phi0)
  let $sin2_phi0 := $sin_phi0 * $sin_phi0
  let $cos_phi0 := math:cos($phi0)
  let $aphi0 := abs($phi0)
  let $B := if ($aphi0 gt 1e-10) then math:sqrt(1 + $e2 div (1 - $e2) * math:pow($cos_phi0, 4)) else math:sqrt(1 - $e2)
  let $A := if ($aphi0 gt 1e-10) then $B * $k0 * math:sqrt(1 - $e2) div (1 - $e2*$sin2_phi0) else $k0
  let $t0 := geoproj:proj-tsfn($phi0)
  let $D := if ($aphi0 gt 1e-10) then $B * math:sqrt(1 - $e2) div ($cos_phi0 * math:sqrt(1 - $e2*$sin2_phi0)) else 1
  let $D := if ($D*$D < 1) then 1 else $D
  let $F := if ($aphi0 gt 1e-10) then if ($phi0 < 0) then $D - math:sqrt($D*$D - 1) else $D + math:sqrt($D*$D - 1) else 1
  let $E := if ($aphi0 gt 1e-10) then math:pow($t0, $B) * $F else 1

  let $G := ($F - 1 div $F) div 2
  let $uc := abs($A div $B * math:atan2(math:sqrt($D*$D - 1), 1))
  let $uc := if ($phi0 < 0) then -$uc else $uc

  for $latlong in $lat_long_degrees
  let $phi := geoproj:deg-to-rad($latlong/*:lat)
  let $lambda := geoproj:deg-to-rad($latlong/*:long)
  let $V := math:sin($B * ($lambda - $lambda0))
	return
    	if (abs($phi) lt (math:pi() div 2 - 1.e-10)) then
    	  let $Q := $E div math:pow(geoproj:proj-tsfn($phi) , $B)
    	  let $S := ($Q - 1 div $Q) div 2
    	  let $T := ($Q + 1 div $Q) div 2
    	  let $U := -$V div $T
          let $v := if (abs($U) ne 1) then $A div (2 * $B) * math:log((1 - $U) div (1 + $U)) else 1 div 0
          let $M := math:cos($B * ($lambda - $lambda0))
          let $u := if ($M gt 1e-7) then $A div $B * math:atan2($S, $M) else $A*$B*($lambda - $lambda0)
          let $u := $u - $uc
          let $x := $v
          let $x := $x * 6378137
          let $y := $u
          let $y := $y * 6378137
          return <geoproj-param:coord><geoproj-param:x>{$x}</geoproj-param:x> <geoproj-param:y>{$y}</geoproj-param:y></geoproj-param:coord>
        else
          let $v := $A div $B * -4.2897288031186085136750723197195
          let $u := $phi * $A div $B
          let $u := $u - $uc
          let $x := $v
          let $x := $x * 6378137
          let $y := $u
          let $y := $y * 6378137
          return <geoproj-param:coord><geoproj-param:x>{$x}</geoproj-param:x> <geoproj-param:y>{$y}</geoproj-param:y></geoproj-param:coord>
};

(:~
 : Forward projection from geographic coordinates lat-long on WGS84 ellipsoid to Oblique Mercator cylinder.

 : The Oblique Mercator projection is like the standard Mercator projection, but you can choose the point of origin.

 : Specify the coordinates of the center point somewhere near the points being projected,
 : so the projection deformation is small.

 : The azimuth in the center point, alpha, is hardcoded to zero, so the true north is preserved.
 : This is a simplification of the standard Oblique Mercator projection.

 : Gamma, the azimuth of the rectified bearing of center line is also zero, calculated from alpha.

 :

 : The radius of the Earth in WGS84 is 6378137 m.

 : Reverse flatening 298.257223563.

 : Eccentricity e 0.0818192.

 :

 : @param $lat_0 is the latitude for center point, in degrees (-90, 90)
 : @param $long_c is the longitude for center point, in degrees (-180, 180)
 : @param $k0 is the scale in the center point. The scale will increase when going far to north and south.
 :        Use value 1 to get the true distances between points, in meters.
 :        At equator, the distance for 1 degree is aproximately 110 km.
 : @param $lat_long_degrees a sequence of nodes of type

 :   <latlong><lat>latitude degree</lat><long>longitude degree</long></latlong>

 :   in namespace "http://www.zorba-xquery.com/modules/geoproj-param"
 : @return a sequence of x-y coordinates in format

 :   <coord><x>x</x><y>y</y></coord>

 :   in namespace "http://www.zorba-xquery.com/modules/geoproj-param"

 :   Note that the x coordinate corresponds to the longitude, and y coordinate to the latitude.

 :   The coordinates are expressed in meters.

 :   The coordinates are relative to the center point.
 : @example geoproj1.xq
 : @example geoproj5.xq
 : @example geoproj6.xq
 : @example geoproj8.xq
 : @example geoproj9.xq
 : @example geoproj10.xq
 : @example geoproj12.xq
 :)
declare function geoproj:wgs84-to-omerc( $lat-0 as xs:double,
                                         $long-c as xs:double,
                                         $k0 as xs:double,
                                         $lat-long-degrees as element(geoproj-param:latlong)*) as element(geoproj-param:coord)*
{
  let $validated-lat-long :=
  (for $lat-long-degree in $lat-long-degrees
  return
    if(empty($lat-long-degree)) then
      $lat-long-degrees
    else
      validate{$lat-long-degree} )
  return
  geoproj:wgs84-to-omerc-validated($lat-0, $long-c, $k0, $validated-lat-long)
};

(:~
 : Forward projection from geographic coordinates lat-long on WGS84 ellipsoid to Oblique Mercator cylinder.

 : This is an intermediate function for wgs84-to-omerc.

 : The difference is that it returns the x-y coordinates in gml:pos format,
 : gml being the prefix for the GML namespace "http://www.opengis.net/gml".

 :

 : @param $lat_0 is the latitude for center point, in degrees (-90, 90)
 : @param $long_c is the longitude for center point, in degrees (-180, 180)
 : @param $k0 is the scale in the center point.
 : @param $lat_long_degrees a sequence of nodes of type

 :   <latlong><lat>latitude degree</lat><long>longitude degree</long></latlong>

 :   in namespace "http://www.zorba-xquery.com/modules/geoproj-param"
 : @return a sequence of x-y coordinates in format

 :   <gml:pos>x y</gml:pos>

 :   in namespace "http://www.opengis.net/gml"
 : @example geoproj3.xq
 :)
declare function geoproj:wgs84-to-omerc-gmlpos( $lat-0 as xs:double,
                                         $long-c as xs:double,
                                         $k0 as xs:double,
                                         $lat-long-degrees as element(geoproj-param:latlong)*) as element(gml:pos)*
{
  let $validated-lat-long :=
  (for $lat-long-degree in $lat-long-degrees
  return
    if(empty($lat-long-degree)) then
      $lat-long-degrees
    else
      validate{$lat-long-degree} )
  return
  geoproj:wgs84-to-omerc-gmlpos-validated($lat-0, $long-c, $k0, $validated-lat-long);
};

declare %private function geoproj:wgs84-to-omerc-gmlpos-validated( $lat_0 as xs:double,
                                         $long_c as xs:double,
                                         $k0 as xs:double,
                                         $lat_long_degrees as element(geoproj-param:latlong, geoproj-param:latlongType)*) as element(gml:pos)*
{
  for $coord in geoproj:wgs84-to-omerc($lat_0, $long_c, $k0, $lat_long_degrees)
  return
    <gml:pos>{string($coord/*:x)}{" "}{string($coord/*:y)}</gml:pos>
};

(:~
 : Function for iterative computing of the inverse isometric latitude.
 :
 : @param $i the maximum iterations
 : @param $ts precomputed value
 : @param $e the Earth eccentricity. For WGS84 is hardcoded to 0.0818192.
 : @param $prev_phi previous computed inverse isometric latitude
 : @return isometric latitude in radians
 :)
declare %private function geoproj:proj-phi2-helper($i as xs:integer,
                                          $ts as xs:double , $e as xs:double,
                                          $prev_phi as xs:double) as xs:double
{
    if($i eq 0) then
      $prev_phi
    else
      let $phi := math:pi() div 2 - 2 * math:atan($ts * math:pow((1 - $e * math:sin($prev_phi[1])) div (1 + $e * math:sin($prev_phi[1])), $e div 2))
      return
        if (abs($prev_phi - $phi) le 1.0e-10) then
          $phi
        else
          geoproj:proj-phi2-helper($i - 1, $ts, $e, $phi)
};

(:~
 : Function for computing the inverse isometric latitude.
 :
 : @param $ts precomputed value, based on an initial latitude.
 : @param $e the Earth eccentricity. For WGS84 is hardcoded to 0.0818192.
 : @return inverse isometric latitude in radians
 :)
declare %private function geoproj:proj-phi2($ts as xs:double, $e as xs:double) as xs:double
{
  let $phi := math:pi() div 2 - 2 * math:atan($ts)
  return
     geoproj:proj-phi2-helper(15, $ts, $e, ($phi))
};

(:~
 : Inverse projection from cartesian coordinates on Oblique Mercator cylinder
 : to geographic coordinates lat-long on WGS84 ellipsoid.

 : The parameters for center point and scale should be the same as for the initial forward projection,
 : otherwise you will get wrong results.

 :

 :

 : @param $lat_0 is the latitude for center point, in degrees (-90, 90)
 : @param $long_c is the longitude for center point, in degrees (-180, 180)
 : @param $k0 is the scale in the center point.
 : @param $coords a sequence of nodes of type

 :   <coord><x>x</x><y>y</y></coord>

 :   in namespace "http://www.zorba-xquery.com/modules/geoproj-param"

 :   The coordinates are expressed in meters.
 : @return a sequence of geographic coordinates in format

 :   <latlong><lat>latitude degree</lat><long>longitude degree</long></latlong>

 :   in namespace "http://www.zorba-xquery.com/modules/geoproj-param"

 :   Note that the longitude corresponds to the x coordinate, and the latitude to the y coordinate.

 : @example geoproj2.xq
 : @example geoproj7.xq
 : @example geoproj11.xq
 :)
declare function geoproj:omerc-to-wgs84($lat-0 as xs:double,
                                         $long-c as xs:double,
                                         $k0 as xs:double,
                                         $coords as element(geoproj-param:coord)*) as element(geoproj-param:latlong)*
{
  let $validated-coords :=
  (for $coord in $coords
  return
    if(empty($coords)) then
      $coord
    else
      validate{$coord} )
  return
  geoproj:omerc-to-wgs84-validated($lat-0, $long-c, $k0, $validated-coords);
};

declare %private function geoproj:omerc-to-wgs84-validated($lat_0 as xs:double,
                                         $long_c as xs:double,
                                         $k0 as xs:double,
                                         $coords as element(geoproj-param:coord, geoproj-param:coordType)*) as element(geoproj-param:latlong)*
{
  let $e as xs:double := 0.0818192E0
  let $e2 := $e*$e
  let $phi0 := geoproj:deg-to-rad($lat_0)
  let $lambda0 := geoproj:deg-to-rad($long_c)
  let $sin_phi0 := math:sin($phi0)
  let $sin2_phi0 := $sin_phi0 * $sin_phi0
  let $cos_phi0 := math:cos($phi0)
  let $aphi0 := abs($phi0)
  let $B := if ($aphi0 gt 1.e-10) then math:sqrt(1 + $e2 div (1 - $e2) * math:pow($cos_phi0, 4)) else math:sqrt(1 - $e2)
  let $A := if ($aphi0 gt 1.e-10) then $B * $k0 * math:sqrt(1 - $e2) div (1 - $e2*$sin2_phi0) else $k0
  let $t0 := geoproj:proj-tsfn($phi0)
  let $D := if ($aphi0 gt 1.e-10) then $B * math:sqrt(1 - $e2) div ($cos_phi0 * math:sqrt(1 - $e2*$sin2_phi0)) else 1
  let $D := if ($D*$D < 1) then 1 else $D
  let $F := if ($aphi0 gt 1.e-10) then if ($phi0 < 0) then $D - math:sqrt($D*$D - 1) else $D + math:sqrt($D*$D - 1) else 1
  let $E := if ($aphi0 gt 1.e-10) then math:pow($t0, $B) * $F else 1

  let $G := ($F - 1 div $F) div 2
  let $uc := abs($A div $B * math:atan2(math:sqrt($D*$D - 1), 1))
  let $uc := if ($phi0 < 0) then -$uc else $uc

  for $coord in $coords
  let $x := $coord/*:x div 6378137
  let $y := $coord/*:y div 6378137
  let $v := $x
  let $u := $y + $uc
  let $Qp := math:exp(-$B * $v div $A)
  let $Sp := ($Qp - 1 div $Qp) div 2
  let $Tp := ($Qp + 1 div $Qp) div 2
  let $Vp := math:sin($B * $u div $A)
  let $Up := $Vp div $Tp
  return
    if(abs(abs($Up) - 1) lt 1e-10) then
      if ($Up gt 0) then
        <geoproj-param:latlong><geoproj-param:lat>90</geoproj-param:lat> <geoproj-param:long>{$long_c}</geoproj-param:long></geoproj-param:latlong>
      else
        <geoproj-param:latlong><geoproj-param:lat>-90</geoproj-param:lat> <geoproj-param:long>{$long_c}</geoproj-param:long></geoproj-param:latlong>
    else
      let $phi := $E div math:sqrt((1 + $Up) div (1 - $Up))
      let $phi := geoproj:proj-phi2(math:pow($phi, 1 div $B), $e)
      let $lambda := - 1 div $B * math:atan2($Sp, math:cos($B * $u div $A))
      return
        <geoproj-param:latlong><geoproj-param:lat>{geoproj:rad-to-deg($phi)}</geoproj-param:lat> <geoproj-param:long>{geoproj:rad-to-deg($lambda+$lambda0)}</geoproj-param:long></geoproj-param:latlong>
};

(:~
 : Inverse projection from cartesian coordinates on Oblique Mercator cylinder
 : to geographic coordinates lat-long on WGS84 ellipsoid.

 : This is an intermediate function for omerc-to-wgs84.

 : The difference is that it works with coordinates in gml:pos format,
 : gml being the prefix for the GML namespace "http://www.opengis.net/gml".

 :

 : @param $lat_0 is the latitude for center point, in degrees (-90, 90)
 : @param $long_c is the longitude for center point, in degrees (-180, 180)
 : @param $k0 is the scale in the center point.
 : @param $gmlposs a sequence of nodes of type

 :   <gml:pos>x y</gml:pos>

 :   in namespace "http://www.opengis.net/gml"
 : @return a sequence of geographic coordinates in format
v
 :   <latlong><lat>latitude degree</lat><long>longitude degree</long></latlong>

 :   in namespace "http://www.zorba-xquery.com/modules/geoproj-param"
 : @example geoproj4.xq
 :)
declare function geoproj:omerc-gmlpos-to-wgs84($lat_0 as xs:double,
                                         $long_c as xs:double,
                                         $k0 as xs:double,
                                         $gmlposs as element(gml:pos)*) as element(geoproj-param:latlong)*
{
  geoproj:omerc-to-wgs84($lat_0, $long_c, $k0,
                  (for $gmlpos in $gmlposs
                  let $xystring := normalize-space(fn:string($gmlpos))
                  let $xy := tokenize($xystring, "[ \t\r\n]+")
                  let $x := $xy[1]
                  let $y := $xy[2]
                  return <geoproj-param:coord><geoproj-param:x>{$x}</geoproj-param:x><geoproj-param:y>{$y}</geoproj-param:y></geoproj-param:coord>))
};

(:~
 : Convertion from Degrees-Minutes-Seconds (DMS) to Degrees.

 : The values for DMS can be like 11d12'13", meaning 11 degrees, 12 minutes and 13 seconds.

 : One degree has 60 minutes, and one minute has 60 seconds.

 : The separator for degrees can be one of the characters [dDoO].

 : The separator for minutes can be one of the characters ['m].

 : The separator for seconds can be " or nothing.

 : The seconds can be a floating point number.

 :

 : The seconds can be missing, and if it is missing, the minutes can be missing too.

 : The negative value can be expressed as -11d12'13" or 11d12'13"S or 11d12'13"W.
 : Values for N (North) and E (East) are positive, and S (South) and W (West) are negative.
 :

 : @param $dms the degree-minutes-seconds string expressed in the format described above
 : @return the value in degrees
 : @example dms1.xq
 :)
declare function geoproj:dms-to-deg($dms as xs:string) as xs:double
{
  let $dms := normalize-space($dms)
  let $dtok := tokenize($dms, "([DdOo])|([SNEW]$)")
  let $d := $dtok[1] cast as xs:double
  let $dsign := if($d < 0) then -1 else 1
  let $d := abs($d)
  return
    if(count($dtok) eq 1) then
      $dsign*$d
    else
      if(string-length($dtok[2]) eq 0) then
        if(matches($dms, "[SW]$")) then
          -$d
        else
          $dsign*$d
      else
        let $ms := substring($dms, string-length($dtok[1]) + 1 + 1)
        let $mtok := tokenize($ms, "(['m])|([SNEW]$)")
        let $m := abs($mtok[1] cast as xs:double)
        let $d := $d + $m div 60
        return
        if(count($mtok) eq 1) then
          $dsign*$d
        else
          if(string-length($mtok[2]) eq 0) then
            if(matches($dms, "[SW]$")) then
              -$d
            else
              $dsign*$d
          else
            let $ss := substring($ms, string-length($mtok[1]) + 1 + 1)
            let $stok := tokenize($ss, '"|([SNEW]$)')
            let $s := abs($stok[1] cast as xs:double)
            let $d := $d + $s div 3600
            return
              if(matches($dms, "[SW]$")) then
                -$d
              else
                $dsign*$d
};

(:~
 : Convertion from Degrees to Degrees-Minutes-Seconds (DMS).

 :

 : @param $deg the degree value
 : @return the value in DMS format, [-]degreedminutes'seconds
 : @example dms2.xq
 :)
declare function geoproj:deg-to-dms($deg as xs:double) as xs:string
{
  let $d := $deg cast as xs:integer
  let $ms := abs($deg - $d) * 60
  let $m := $ms cast as xs:integer
  let $s := $ms - $m
  let $s := $s * 60
  let $m := if($s ge (60 - 1e-10)) then $m + 1 else $m
  let $d := if($m ge (60 - 1e-10)) then $d + 1 else $d
  let $m := if($m ge (60 - 1e-10)) then $m - 60 else $m
  let $s := if($s ge (60 - 1e-10)) then 0 else $s
  return
    concat($d, "d", $m, "'", $s)
};

(:
 : Copyright 2006-2009 The FLWOR Foundation.
 :
 : Licensed under the Apache License, Version 2.0 (the "License");
 : you may not use this file except in compliance with the License.
 : You may obtain a copy of the License at
 :
 : http://www.apache.org/licenses/LICENSE-2.0
 :
 : Unless required by applicable law or agreed to in writing, software
 : distributed under the License is distributed on an "AS IS" BASIS,
 : WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 : See the License for the specific language governing permissions and
 : limitations under the License.
:)

(:~
 : Geographic projection module.

 : Forward and inverse projection from WGS84 lat-long coordinates to Oblique Mercator x-y projection.

 : Oblique Mercator projection is a sphere to cylinder projection.

 : This projection results in a conformal output, meaning the shape of small areas is preserved,
 : no matter the distance from the origin. But it is not equal area, meaning the area size increases heavily
 : when getting closer to North or South. The area size increases with the same amount on x and y axes,
 : so the relative shape remains almost the same.

 : Mercator projection is the oldest projection, and it is still widely used because it produces a rectangular map.
 : This projection is used in Google Maps because of its conformal output.

 : For military or measurements purposes the UTM projection is used (or variants). This splits the Earth into small
 : areas and computes the cartesian coordinates relative to each area.

 :

 : Here we use Oblique Mercator projection. Its advantage over the normal Mercator is that you can set the tangent point
 : between the cylinder and the sphere to be anywhere on Earth. So you can set the center of the map to be close
 : to the area you want projected and be able to measure accurately the distances between points and lines.
 : The map deformation is minimal close to the center point and close to the "equator" line.

 : The advantage over the UTM projection is that it can also produce a global rectangular map, like Mercator,
 : which is great for viewing.

 : The disadvantage over Mercator is that it needs more processing power.

 :

 : WGS84 is the ellipsoid aproximation of the Earth, with big radius of 6,378,137 m and small radius of 6,356,752.3 m.
 : The geographic coordinates expressed for this ellipsoid are widely used today in maps and gps coordinates.
 : It is the default standard for representing geographic coordinates.

 :

 : The purpose of this module is to provide convertion from polar to cartesian coordinates, so you can
 : process the geographic data with the Simple Features API functions implemented in the geo module.
 : That module works only with cartesian coordinates, but most maps have polar coordinates.

 :

 : The projection formulas are taken from lib_proj library and implemented in XQuery.
 :
 : @author Daniel Turcanu
 : @library GEOS (Geometry Engine - Open Source)
 :)
module namespace geoproj = "http://www.zorba-xquery.com/modules/geoproj";
import module namespace math = "http://www.zorba-xquery.com/modules/math";

(:~
 : Import module for checking if geoproj parameters are validated.
 :)
import module namespace zorba-schema = "http://www.zorba-xquery.com/modules/schema";

(:~
 : Contains the definitions of the geoproj parameters.
:)
import schema namespace geoproj-param = "http://www.zorba-xquery.com/modules/geoproj-param";

declare namespace gml="http://www.opengis.net/gml";

(:~
 : Convert angle from degrees to radians.

 : The parameter is first reduced to value range of (-360, 360).
 :
 : @param $deg angle in  degrees
 : @return value in radians (-2PI, 2PI)
 :)
declare function geoproj:deg-to-rad($deg as xs:double) as xs:double
{
  ($deg mod 360) * 2 * math:pi() div 360
};

(:~
 : Convert angle from radians to degrees.

 :
 : @param $rad value in radians
 : @return value in degrees (-360, 360)
 :)
declare function geoproj:rad-to-deg($rad as xs:double) as xs:double
{
  ($rad * 360 div 2 div math:pi()) mod 360
};

(:~
 : Compute the isometric latitude of $phi latitude.
 :
 : @param $phi a latitude
 : @return isometric latitude in radians
 :)
declare %private function geoproj:proj-tsfn($phi as xs:double) as xs:double
{
   let $e as xs:double := 0.0818192E0
   return
   math:tan(math:pi() div 4 - $phi div 2) div math:pow((1 - $e * math:sin($phi)) div (1 + $e * math:sin($phi)), $e div 2)
};

declare %private function geoproj:wgs84-to-omerc-validated(
                                         $lat_0 as xs:double,
                                         $long_c as xs:double,
                                         $k0 as xs:double,
                                         $lat_long_degrees as element(geoproj-param:latlong, geoproj-param:latlongType)*) as element(geoproj-param:coord)*
{
  let $e as xs:double := 0.0818192E0
  let $e2 := $e*$e
  let $phi0 := geoproj:deg-to-rad($lat_0)
  let $lambda0 := geoproj:deg-to-rad($long_c)
  let $sin_phi0 := math:sin($phi0)
  let $sin2_phi0 := $sin_phi0 * $sin_phi0
  let $cos_phi0 := math:cos($phi0)
  let $aphi0 := abs($phi0)
  let $B := if ($aphi0 gt 1e-10) then math:sqrt(1 + $e2 div (1 - $e2) * math:pow($cos_phi0, 4)) else math:sqrt(1 - $e2)
  let $A := if ($aphi0 gt 1e-10) then $B * $k0 * math:sqrt(1 - $e2) div (1 - $e2*$sin2_phi0) else $k0
  let $t0 := geoproj:proj-tsfn($phi0)
  let $D := if ($aphi0 gt 1e-10) then $B * math:sqrt(1 - $e2) div ($cos_phi0 * math:sqrt(1 - $e2*$sin2_phi0)) else 1
  let $D := if ($D*$D < 1) then 1 else $D
  let $F := if ($aphi0 gt 1e-10) then if ($phi0 < 0) then $D - math:sqrt($D*$D - 1) else $D + math:sqrt($D*$D - 1) else 1
  let $E := if ($aphi0 gt 1e-10) then math:pow($t0, $B) * $F else 1

  let $G := ($F - 1 div $F) div 2
  let $uc := abs($A div $B * math:atan2(math:sqrt($D*$D - 1), 1))
  let $uc := if ($phi0 < 0) then -$uc else $uc

  for $latlong in $lat_long_degrees
  let $phi := geoproj:deg-to-rad($latlong/*:lat)
  let $lambda := geoproj:deg-to-rad($latlong/*:long)
  let $V := math:sin($B * ($lambda - $lambda0))
	return
    	if (abs($phi) lt (math:pi() div 2 - 1.e-10)) then
    	  let $Q := $E div math:pow(geoproj:proj-tsfn($phi) , $B)
    	  let $S := ($Q - 1 div $Q) div 2
    	  let $T := ($Q + 1 div $Q) div 2
    	  let $U := -$V div $T
          let $v := if (abs($U) ne 1) then $A div (2 * $B) * math:log((1 - $U) div (1 + $U)) else 1 div 0
          let $M := math:cos($B * ($lambda - $lambda0))
          let $u := if ($M gt 1e-7) then $A div $B * math:atan2($S, $M) else $A*$B*($lambda - $lambda0)
          let $u := $u - $uc
          let $x := $v
          let $x := $x * 6378137
          let $y := $u
          let $y := $y * 6378137
          return <geoproj-param:coord><geoproj-param:x>{$x}</geoproj-param:x> <geoproj-param:y>{$y}</geoproj-param:y></geoproj-param:coord>
        else
          let $v := $A div $B * -4.2897288031186085136750723197195
          let $u := $phi * $A div $B
          let $u := $u - $uc
          let $x := $v
          let $x := $x * 6378137
          let $y := $u
          let $y := $y * 6378137
          return <geoproj-param:coord><geoproj-param:x>{$x}</geoproj-param:x> <geoproj-param:y>{$y}</geoproj-param:y></geoproj-param:coord>
};

(:~
 : Forward projection from geographic coordinates lat-long on WGS84 ellipsoid to Oblique Mercator cylinder.

 : The Oblique Mercator projection is like the standard Mercator projection, but you can choose the point of origin.

 : Specify the coordinates of the center point somewhere near the points being projected,
 : so the projection deformation is small.

 : The azimuth in the center point, alpha, is hardcoded to zero, so the true north is preserved.
 : This is a simplification of the standard Oblique Mercator projection.

 : Gamma, the azimuth of the rectified bearing of center line is also zero, calculated from alpha.

 :

 : The radius of the Earth in WGS84 is 6378137 m.

 : Reverse flatening 298.257223563.

 : Eccentricity e 0.0818192.

 :

 : @param $lat_0 is the latitude for center point, in degrees (-90, 90)
 : @param $long_c is the longitude for center point, in degrees (-180, 180)
 : @param $k0 is the scale in the center point. The scale will increase when going far to north and south.
 :        Use value 1 to get the true distances between points, in meters.
 :        At equator, the distance for 1 degree is aproximately 110 km.
 : @param $lat_long_degrees a sequence of nodes of type

 :   <latlong><lat>latitude degree</lat><long>longitude degree</long></latlong>

 :   in namespace "http://www.zorba-xquery.com/modules/geoproj-param"
 : @return a sequence of x-y coordinates in format

 :   <coord><x>x</x><y>y</y></coord>

 :   in namespace "http://www.zorba-xquery.com/modules/geoproj-param"

 :   Note that the x coordinate corresponds to the longitude, and y coordinate to the latitude.

 :   The coordinates are expressed in meters.

 :   The coordinates are relative to the center point.
 : @example geoproj1.xq
 : @example geoproj5.xq
 : @example geoproj6.xq
 : @example geoproj8.xq
 : @example geoproj9.xq
 : @example geoproj10.xq
 : @example geoproj12.xq
 :)
declare function geoproj:wgs84-to-omerc( $lat-0 as xs:double,
                                         $long-c as xs:double,
                                         $k0 as xs:double,
                                         $lat-long-degrees as element(geoproj-param:latlong)*) as element(geoproj-param:coord)*
{
  let $validated-lat-long :=
  (for $lat-long-degree in $lat-long-degrees
  return
    if(empty($lat-long-degree)) then
      $lat-long-degrees
    else
      validate{$lat-long-degree} )
  return
  geoproj:wgs84-to-omerc-validated($lat-0, $long-c, $k0, $validated-lat-long)
};

(:~
 : Forward projection from geographic coordinates lat-long on WGS84 ellipsoid to Oblique Mercator cylinder.

 : This is an intermediate function for wgs84-to-omerc.

 : The difference is that it returns the x-y coordinates in gml:pos format,
 : gml being the prefix for the GML namespace "http://www.opengis.net/gml".

 :

 : @param $lat_0 is the latitude for center point, in degrees (-90, 90)
 : @param $long_c is the longitude for center point, in degrees (-180, 180)
 : @param $k0 is the scale in the center point.
 : @param $lat_long_degrees a sequence of nodes of type

 :   <latlong><lat>latitude degree</lat><long>longitude degree</long></latlong>

 :   in namespace "http://www.zorba-xquery.com/modules/geoproj-param"
 : @return a sequence of x-y coordinates in format

 :   <gml:pos>x y</gml:pos>

 :   in namespace "http://www.opengis.net/gml"
 : @example geoproj3.xq
 :)
declare function geoproj:wgs84-to-omerc-gmlpos( $lat-0 as xs:double,
                                         $long-c as xs:double,
                                         $k0 as xs:double,
                                         $lat-long-degrees as element(geoproj-param:latlong)*) as element(gml:pos)*
{
  let $validated-lat-long :=
  (for $lat-long-degree in $lat-long-degrees
  return
    if(empty($lat-long-degree)) then
      $lat-long-degrees
    else
      validate{$lat-long-degree} )
  return
  geoproj:wgs84-to-omerc-gmlpos-validated($lat-0, $long-c, $k0, $validated-lat-long);
};

declare %private function geoproj:wgs84-to-omerc-gmlpos-validated( $lat_0 as xs:double,
                                         $long_c as xs:double,
                                         $k0 as xs:double,
                                         $lat_long_degrees as element(geoproj-param:latlong, geoproj-param:latlongType)*) as element(gml:pos)*
{
  for $coord in geoproj:wgs84-to-omerc($lat_0, $long_c, $k0, $lat_long_degrees)
  return
    <gml:pos>{string($coord/*:x)}{" "}{string($coord/*:y)}</gml:pos>
};

(:~
 : Function for iterative computing of the inverse isometric latitude.
 :
 : @param $i the maximum iterations
 : @param $ts precomputed value
 : @param $e the Earth eccentricity. For WGS84 is hardcoded to 0.0818192.
 : @param $prev_phi previous computed inverse isometric latitude
 : @return isometric latitude in radians
 :)
declare %private function geoproj:proj-phi2-helper($i as xs:integer,
                                          $ts as xs:double , $e as xs:double,
                                          $prev_phi as xs:double) as xs:double
{
    if($i eq 0) then
      $prev_phi
    else
      let $phi := math:pi() div 2 - 2 * math:atan($ts * math:pow((1 - $e * math:sin($prev_phi[1])) div (1 + $e * math:sin($prev_phi[1])), $e div 2))
      return
        if (abs($prev_phi - $phi) le 1.0e-10) then
          $phi
        else
          geoproj:proj-phi2-helper($i - 1, $ts, $e, $phi)
};

(:~
 : Function for computing the inverse isometric latitude.
 :
 : @param $ts precomputed value, based on an initial latitude.
 : @param $e the Earth eccentricity. For WGS84 is hardcoded to 0.0818192.
 : @return inverse isometric latitude in radians
 :)
declare %private function geoproj:proj-phi2($ts as xs:double, $e as xs:double) as xs:double
{
  let $phi := math:pi() div 2 - 2 * math:atan($ts)
  return
     geoproj:proj-phi2-helper(15, $ts, $e, ($phi))
};

(:~
 : Inverse projection from cartesian coordinates on Oblique Mercator cylinder
 : to geographic coordinates lat-long on WGS84 ellipsoid.

 : The parameters for center point and scale should be the same as for the initial forward projection,
 : otherwise you will get wrong results.

 :

 :

 : @param $lat_0 is the latitude for center point, in degrees (-90, 90)
 : @param $long_c is the longitude for center point, in degrees (-180, 180)
 : @param $k0 is the scale in the center point.
 : @param $coords a sequence of nodes of type

 :   <coord><x>x</x><y>y</y></coord>

 :   in namespace "http://www.zorba-xquery.com/modules/geoproj-param"

 :   The coordinates are expressed in meters.
 : @return a sequence of geographic coordinates in format

 :   <latlong><lat>latitude degree</lat><long>longitude degree</long></latlong>

 :   in namespace "http://www.zorba-xquery.com/modules/geoproj-param"

 :   Note that the longitude corresponds to the x coordinate, and the latitude to the y coordinate.

 : @example geoproj2.xq
 : @example geoproj7.xq
 : @example geoproj11.xq
 :)
declare function geoproj:omerc-to-wgs84($lat-0 as xs:double,
                                         $long-c as xs:double,
                                         $k0 as xs:double,
                                         $coords as element(geoproj-param:coord)*) as element(geoproj-param:latlong)*
{
  let $validated-coords :=
  (for $coord in $coords
  return
    if(empty($coords)) then
      $coord
    else
      validate{$coord} )
  return
  geoproj:omerc-to-wgs84-validated($lat-0, $long-c, $k0, $validated-coords);
};

declare %private function geoproj:omerc-to-wgs84-validated($lat_0 as xs:double,
                                         $long_c as xs:double,
                                         $k0 as xs:double,
                                         $coords as element(geoproj-param:coord, geoproj-param:coordType)*) as element(geoproj-param:latlong)*
{
  let $e as xs:double := 0.0818192E0
  let $e2 := $e*$e
  let $phi0 := geoproj:deg-to-rad($lat_0)
  let $lambda0 := geoproj:deg-to-rad($long_c)
  let $sin_phi0 := math:sin($phi0)
  let $sin2_phi0 := $sin_phi0 * $sin_phi0
  let $cos_phi0 := math:cos($phi0)
  let $aphi0 := abs($phi0)
  let $B := if ($aphi0 gt 1.e-10) then math:sqrt(1 + $e2 div (1 - $e2) * math:pow($cos_phi0, 4)) else math:sqrt(1 - $e2)
  let $A := if ($aphi0 gt 1.e-10) then $B * $k0 * math:sqrt(1 - $e2) div (1 - $e2*$sin2_phi0) else $k0
  let $t0 := geoproj:proj-tsfn($phi0)
  let $D := if ($aphi0 gt 1.e-10) then $B * math:sqrt(1 - $e2) div ($cos_phi0 * math:sqrt(1 - $e2*$sin2_phi0)) else 1
  let $D := if ($D*$D < 1) then 1 else $D
  let $F := if ($aphi0 gt 1.e-10) then if ($phi0 < 0) then $D - math:sqrt($D*$D - 1) else $D + math:sqrt($D*$D - 1) else 1
  let $E := if ($aphi0 gt 1.e-10) then math:pow($t0, $B) * $F else 1

  let $G := ($F - 1 div $F) div 2
  let $uc := abs($A div $B * math:atan2(math:sqrt($D*$D - 1), 1))
  let $uc := if ($phi0 < 0) then -$uc else $uc

  for $coord in $coords
  let $x := $coord/*:x div 6378137
  let $y := $coord/*:y div 6378137
  let $v := $x
  let $u := $y + $uc
  let $Qp := math:exp(-$B * $v div $A)
  let $Sp := ($Qp - 1 div $Qp) div 2
  let $Tp := ($Qp + 1 div $Qp) div 2
  let $Vp := math:sin($B * $u div $A)
  let $Up := $Vp div $Tp
  return
    if(abs(abs($Up) - 1) lt 1e-10) then
      if ($Up gt 0) then
        <geoproj-param:latlong><geoproj-param:lat>90</geoproj-param:lat> <geoproj-param:long>{$long_c}</geoproj-param:long></geoproj-param:latlong>
      else
        <geoproj-param:latlong><geoproj-param:lat>-90</geoproj-param:lat> <geoproj-param:long>{$long_c}</geoproj-param:long></geoproj-param:latlong>
    else
      let $phi := $E div math:sqrt((1 + $Up) div (1 - $Up))
      let $phi := geoproj:proj-phi2(math:pow($phi, 1 div $B), $e)
      let $lambda := - 1 div $B * math:atan2($Sp, math:cos($B * $u div $A))
      return
        <geoproj-param:latlong><geoproj-param:lat>{geoproj:rad-to-deg($phi)}</geoproj-param:lat> <geoproj-param:long>{geoproj:rad-to-deg($lambda+$lambda0)}</geoproj-param:long></geoproj-param:latlong>
};

(:~
 : Inverse projection from cartesian coordinates on Oblique Mercator cylinder
 : to geographic coordinates lat-long on WGS84 ellipsoid.

 : This is an intermediate function for omerc-to-wgs84.

 : The difference is that it works with coordinates in gml:pos format,
 : gml being the prefix for the GML namespace "http://www.opengis.net/gml".

 :

 : @param $lat_0 is the latitude for center point, in degrees (-90, 90)
 : @param $long_c is the longitude for center point, in degrees (-180, 180)
 : @param $k0 is the scale in the center point.
 : @param $gmlposs a sequence of nodes of type

 :   <gml:pos>x y</gml:pos>

 :   in namespace "http://www.opengis.net/gml"
 : @return a sequence of geographic coordinates in format
v
 :   <latlong><lat>latitude degree</lat><long>longitude degree</long></latlong>

 :   in namespace "http://www.zorba-xquery.com/modules/geoproj-param"
 : @example geoproj4.xq
 :)
declare function geoproj:omerc-gmlpos-to-wgs84($lat_0 as xs:double,
                                         $long_c as xs:double,
                                         $k0 as xs:double,
                                         $gmlposs as element(gml:pos)*) as element(geoproj-param:latlong)*
{
  geoproj:omerc-to-wgs84($lat_0, $long_c, $k0,
                  (for $gmlpos in $gmlposs
                  let $xystring := normalize-space(fn:string($gmlpos))
                  let $xy := tokenize($xystring, "[ \t\r\n]+")
                  let $x := $xy[1]
                  let $y := $xy[2]
                  return <geoproj-param:coord><geoproj-param:x>{$x}</geoproj-param:x><geoproj-param:y>{$y}</geoproj-param:y></geoproj-param:coord>))
};

(:~
 : Convertion from Degrees-Minutes-Seconds (DMS) to Degrees.

 : The values for DMS can be like 11d12'13", meaning 11 degrees, 12 minutes and 13 seconds.

 : One degree has 60 minutes, and one minute has 60 seconds.

 : The separator for degrees can be one of the characters [dDoO].

 : The separator for minutes can be one of the characters ['m].

 : The separator for seconds can be " or nothing.

 : The seconds can be a floating point number.

 :

 : The seconds can be missing, and if it is missing, the minutes can be missing too.

 : The negative value can be expressed as -11d12'13" or 11d12'13"S or 11d12'13"W.
 : Values for N (North) and E (East) are positive, and S (South) and W (West) are negative.
 :

 : @param $dms the degree-minutes-seconds string expressed in the format described above
 : @return the value in degrees
 : @example dms1.xq
 :)
declare function geoproj:dms-to-deg($dms as xs:string) as xs:double
{
  let $dms := normalize-space($dms)
  let $dtok := tokenize($dms, "([DdOo])|([SNEW]$)")
  let $d := $dtok[1] cast as xs:double
  let $dsign := if($d < 0) then -1 else 1
  let $d := abs($d)
  return
    if(count($dtok) eq 1) then
      $dsign*$d
    else
      if(string-length($dtok[2]) eq 0) then
        if(matches($dms, "[SW]$")) then
          -$d
        else
          $dsign*$d
      else
        let $ms := substring($dms, string-length($dtok[1]) + 1 + 1)
        let $mtok := tokenize($ms, "(['m])|([SNEW]$)")
        let $m := abs($mtok[1] cast as xs:double)
        let $d := $d + $m div 60
        return
        if(count($mtok) eq 1) then
          $dsign*$d
        else
          if(string-length($mtok[2]) eq 0) then
            if(matches($dms, "[SW]$")) then
              -$d
            else
              $dsign*$d
          else
            let $ss := substring($ms, string-length($mtok[1]) + 1 + 1)
            let $stok := tokenize($ss, '"|([SNEW]$)')
            let $s := abs($stok[1] cast as xs:double)
            let $d := $d + $s div 3600
            return
              if(matches($dms, "[SW]$")) then
                -$d
              else
                $dsign*$d
};

(:~
 : Convertion from Degrees to Degrees-Minutes-Seconds (DMS).

 :

 : @param $deg the degree value
 : @return the value in DMS format, [-]degreedminutes'seconds
 : @example dms2.xq
 :)
declare function geoproj:deg-to-dms($deg as xs:double) as xs:string
{
  let $d := $deg cast as xs:integer
  let $ms := abs($deg - $d) * 60
  let $m := $ms cast as xs:integer
  let $s := $ms - $m
  let $s := $s * 60
  let $m := if($s ge (60 - 1e-10)) then $m + 1 else $m
  let $d := if($m ge (60 - 1e-10)) then $d + 1 else $d
  let $m := if($m ge (60 - 1e-10)) then $m - 60 else $m
  let $s := if($s ge (60 - 1e-10)) then 0 else $s
  return
    concat($d, "d", $m, "'", $s)
};


Capture URI of content map in key space

Capture URI of content map in key space

(: =====================================================

   DITA Link Manager Model

   Model component of Model/View/Controller. Maintains
   the link management data.

   Provides queries to create and update resource use records,
   dependency indexes, etc.

   These queries are called whenever new content is added to
   given database.

   Author: W. Eliot Kimber

   Copyright (c) 2015 DITA For Small Teams
   Licensed under Apache License 2

   ===================================================== :)

module namespace lmm="http://dita-for-small-teams.org/xquery/modules/linkmgr-model";

declare namespace dfst="http://dita-for-small-teams.org";

import module namespace df="http://dita-for-small-teams.org/xquery/modules/dita-utils";
import module namespace bxutil="http://dita-for-small-teams.org/xquery/modules/basex-utils";
import module namespace lmutil="http://dita-for-small-teams.org/xquery/modules/linkmgmt-utils";
import module namespace dfstcnst="http://dita-for-small-teams.org/xquery/modules/dfst-constants";
import module namespace relpath="http://dita-for-small-teams.org/xquery/modules/relpath-utils";

declare %updating function lmm:createDirectLinkResourceRecords(
        $metadataDbName as xs:string,
        $directLinks as map(*)*,
        $logID as xs:string) {
    for $linkItem in $directLinks
       let $resolveResult := lmutil:resolveDirectLink($linkItem)
       return lmm:createOrUpdateResourceUseRecord(
                   $metadataDbName,
                   $linkItem,
                   $resolveResult,
                   $logID)

};

declare %updating function lmm:createIndirectLinkResourceRecords(
                               $metadataDbName as xs:string,
                               $indirectLinks as map(*)*,
                               $logID as xs:string) {
    for $linkItem in $indirectLinks
        let $resolveResult as map(*) := lmutil:resolveIndirectLink($linkItem)
        return lmm:createOrUpdateResourceUseRecord(
                   $metadataDbName,
                   $linkItem,
                   $resolveResult,
                   $logID)
};

(: Given a link and the resolved result, creates a resource use record for each
   target the link resolved to.

 :)
declare %updating function lmm:createOrUpdateResourceUseRecord(
                              $metadataDbName,
                              $linkItem as map(*),
                              $resolveResult as map(*),
                              $logID as xs:string) {
   let $resolveResult as map(*) := lmutil:resolveDirectLink($linkItem)
   let $targets := $resolveResult('target')
   for $target in $targets
       return lmm:createOrUpdateResourceUseRecordForLinkTarget(
                    $metadataDbName,
                    $linkItem,
                    $target,
                    $logID)
};

(:
 : Creates a use record reflecting the use of the target element by the document
 : that contains the link.
 :
 : The use records are organized into directories, one directory per used document,
 : where the directory name is the MD5 hash of the document URI (just to make a
 : shorter name that won't have any problematic characters).
 :
 : Each use record document's filename is the MD5 hash of the linking document's
 : URI plus the MD5 hash of the linking element.
 :
 : Potential problem: the same element could, in theory, link to the same target in
 : two different ways: as a link and as a conref, although this case is extremely
 : unlikely. Really need a way to distinguish each abstract link represented by
 : a given DITA link-establishing element but the current implementation doesn't
 : provide that abstraction. It could through a refactor. Keeping it simple for now.
 :)
declare %updating function lmm:createOrUpdateResourceUseRecordForLinkTarget(
           $metadataDbName as xs:string,
           $linkItem as map(*),
           $target as element(),
           $logID as xs:string) {
   let $targetDoc := root($target)
   let $link := $linkItem('link')
   let $containingDir := concat($dfstcnst:where-used-dir, '/',
                                lmutil:constructResourceKeyForElement($target),
                                '/')
   let $reskey := lmutil:constructResourceKeyForElement($link)
   let $recordFilename := concat('use-record_', $reskey, '.xml')
   let $format := if ($link/@format)
                     then string($link/@format)
                     else 'dita'
   let $scope := if ($link/@scope)
                     then string($link/@scope)
                     else 'local'
   let $linkContext := $linkItem('linkContext')

   (: df:getTitleForLinkElementContainer($link) :)
   let $useRecord :=
     <dfst:useRecord resourceKey="{$reskey}"
                     targetDoc="{document-uri($targetDoc)}"
                     usingDoc="{document-uri(root($link))}"
                     linkType="{df:getBaseLinkType($link)}"
                     linkClass="{string($link/@class)}"
                     linkContext="{$linkContext}"
                     format="{$format}"
                     scope="{$scope}"
     >
       <title>{if (df:class($link, 'map/topicref'))
                  then string((root($link)/*/*[df:class(., 'topic/title')] |
                        root($link)/*/@title)[1])
                  else string($link/ancestor::*[df:class(., 'topic/topic')][1]/*[df:class(., 'topic/title')])}</title>
     </dfst:useRecord>
    let $useRecordUri := relpath:newFile($containingDir, $recordFilename)
    return try {
       (
       if (db:exists($metadataDbName, $useRecordUri))
          then db:delete($metadataDbName, $useRecordUri)
          else(),
       db:replace($metadataDbName,
                  $useRecordUri,
                  $useRecord) (:,
       (: FIXME: Write record to log doc :)
       db:output(<info>Stored use record "{$useRecordUri}"</info>) :)
       )
    } catch * {
       (: FIXME: Write record to log doc :)
       error('LMI-UpdateUseRecord001',
             concat('Error storing use record to "', $useRecordUri, '": ', $err:description))
    }
};

(:~
 : Construct key spaces from root maps.
 :
 : @param contentDbName Name of datbase that contains the content to construct key spaces from
 : @param metadataDbName Name of metadata database to store construct key spaces in.
 :
 : Finds all the root maps (requires that all direct-link use records are up to date).
 :
 : For each root map, constructs and stores the resolved map.
 :
 :)
declare %updating function lmm:constructKeySpaces(
        $contentDbName as xs:string,
        $metadataDbName as xs:string,
        $logID as xs:string) {

  (: General process:

     1. Find all DITA maps

     2. For each map, construct a resolved map that preserves submap
        boundaries and original source map details.

     The resolved maps can then be used to determine the effective
     binding for any key without the need to separately construct
     key spaces. This technique was pioneered and suggested by
     Chris Nitchie, designer of the DITA 1.3 scoped keys mechanism.

     Note that there's no point in trying to determine if a given
     map is a "root map" as any map can, in theory, be used as a root
     map. Likewise, a map that happens to have no local-scope
     topicrefs to it may not be intended to be the root of a publication.

     One alternative to this approach is to only consider maps that have
     peer-scope topicrefs to them to be root maps (per the DITA 1.3
     meaning for peer-scope topicrefs to dita maps). In this case, there needs
     to be a "master map" that serves to simply identify the "top level
     root maps", that is, the maps that are considered by the document
     authors to be The root maps, that is, the maps they consider to
     be the roots of publications. Given this master map the system can
     then consider only peer-scope map references in order to determine
     the root maps and thus the maps for which key space construction
     is relevant.

     Key spaces are constructed from resolved maps and stored in the metadata
     database.

   :)

   let $contentMaps := collection($contentDbName)/*[df:class(., 'map/map')][lmutil:isRootMap(.)]
   (: Sequence of maps containing the things to be stored :)
   let $dataToStore as map(*)* := lmm:constructKeySpacesToBeStored($contentMaps)
   return
      for $obj in $dataToStore
            let $ditaMap := $obj('resolvedMapMap')('map')
            let $resolvedMapMap := $obj('resolvedMapMap')
            let $keySpace as element(keyspace) := $obj('keySpace')
            return ((: FIXME: Write to log. db:output(<info>Resolving map {document-uri(root($ditaMap))}...</info>), :)
                    lmm:storeResolvedMap($resolvedMapMap,
                                         $metadataDbName,
                                         $obj('log'),
                                         $logID),
                    lmm:storeKeySpace(
                                         $keySpace,
                                         $metadataDbName,
                                         $logID)
                     )
};

(:~
 : Constructs the XML key space documents to be stored for each content map.
 :
 : @param contentMaps Sequence of content (unresolved) map elements.
 : @return Sequence of maps with two items:
 :
 : 'resolvedMapMap' : The resolved map data map. Includes the content map
 :                    as an item.
 : 'keySpace' : The XML representation of the key space for the map
 :)
declare function lmm:constructKeySpacesToBeStored($contentMaps as element()*) as map(*)* {
  let $result :=
    for $ditaMap in $contentMaps
        let $resolvedMapMap as map(*) := lmm:resolveMap($ditaMap)
        let $dataMap := lmm:constructKeySpaceForResolvedMap($resolvedMapMap)
        return $dataMap
  return $result
};

(:~
 : Store a keyspace document in the metadata database.
 :
 : @param keySpace XML document representing the key space to be stored
 : @param metadataDbName Name of the metadata database to store the
 :        resolved map in
 : @logID ID of the log to write the messages to.
 :)
declare %updating function lmm:storeKeySpace(
                     $keySpace as element(keyspace),
                     $metadataDbName as xs:string,
                     $logID as xs:string) {
  let $keySpaceURI := lmutil:getKeySpaceURIForKeySpace($keySpace)
  return (db:replace($metadataDbName, $keySpaceURI, $keySpace) (: ,
          FIXME: Write to log doc.
          db:output((<info>Stored key space "{$keySpaceURI}"</info>)):)
          )
};

(:~
 : Given a resolved DITA map, construct the key space map
 : for it.
 :
 : The key space map reflects the hierarchy of key spaces defined
 : in the map. Every root map represents at least one, possibly
 : empty, key space.
 :
 : @param map Source content map (returned in the result map)
 : @param resolvedMap Resolved map constructed from the source map
 : @return Map containing the members:
 :
 :   map : The content map
 :   resolvedMap : The resolved map the key spaces were constructed from
 :   keySpace : The key space document rooted at the resolved map.
 :)
declare function lmm:constructKeySpaceForResolvedMap(
                       $resolvedMapMap as map(*)) as map(*) {
   let $ditaMap as element() := $resolvedMapMap('map')
   let $keyspace as element(keyspace) :=
         lmm:constructKeySpace(
                 $resolvedMapMap,
                 $resolvedMapMap('resolvedMap'))

   let $result :=
      map{ 'map' : $ditaMap,
           'resolvedMapMap' : $resolvedMapMap,
           'keySpace' : $keyspace
         }
   return $result
};

(:~
 : Given a content map element, return the corresponding keyspace document.
 : The key space must have already been constructed.
 :)
declare function lmm:getKeySpaceForMap($contentMap as element()) as element()? {
    let $resolvedMapURI := lmutil:getResolvedMapURIForMap($contentMap)
    let $metadataDbName as xs:string := bxutil:getMetadataDbNameForDoc(root($contentMap))

    let $keyspaceURI := lmutil:getKeySpaceURIForResolvedMapURI($resolvedMapURI)
    let $result := collection($metadataDbName || $keyspaceURI)/keyspace
    return $result
};

(:~
 : The identity of a key space is the element that defines it, either map
 : or a topicref. In order to resolve a key you must know the key space
 : hierarchy because you have to start with the root key space, see if
 : the key-as-referenced is defined in that space, then if not, walk
 : down the ancestor tree until you either find a match for the key
 : name or run out of options.
 :
 : Logically, an ancestor key space reflects in itself all the scope-qualified
 : names of the keys from its descentant key space that it does not
 : explicitly override, meaning that, starting from any descedant key space you
 : can resolve any fully-qualified key name.
 :
 : @param resolvedMapMap Data map that represents the resolved map.
 : @param spaceDefiner Element, map or topicref, that defines a new key space.
 :
 : @return Keyspace element, which will contain any descendant keyspaces
 :)
declare function lmm:constructKeySpace(
                      $resolvedMapMap as map(*),
                      $spaceDefiner as element()) as element() {
  let $keyspaceMap := lmm:constructKeySpaceMap($resolvedMapMap, $spaceDefiner, ())
  let $result := lmm:keySpaceMapToXML($keyspaceMap)
  return $result
};

(:~
 : Given a key space map, return an XML serialization of it suitalbe for storing
 : in the XML metadata store
 :)
declare function lmm:keySpaceMapToXML($keyspaceMap as map(*)) as element(keyspace) {

let $definer as element() := $keyspaceMap('definer')
let $scopeNames as xs:string* := $keyspaceMap('scopeNames')
let $keydefs as map(*)? := $keyspaceMap('keydefs')
let $childScopes as map(*)* := $keyspaceMap('childScopes')
let $resolvedMapMap as map(*) := $keyspaceMap('resolvedMapMap')
let $ditaMap := $resolvedMapMap('map')

let $result :=
<keyspace
  resolvedMap="{$resolvedMapMap('resolvedMapURI')}"
  contentMap="{document-uri(root($ditaMap))}"
>{
 <scopeNames>{
   for $name in $scopeNames
       return <scopeName>{$name}</scopeName>
 }</scopeNames>,
 <keys>{
 for $keyName in map:keys($keydefs) order by $keyName
     return <key name="{$keyName}">{
               $keydefs($keyName)
             }</key>
 }</keys>,
 <childScopes>{
 for $childScope in $childScopes
     return lmm:keySpaceMapToXML($childScope)
 }</childScopes>
}</keyspace>

return $result

};

(:~
 : Construct map object that reflects the key space rooted at the specified
 : key space defining element.
 :
 : The map is a map of key names to key definitions, where a given key name
 : may reflect multiple definitions. The key definitions are in precedence
 : order from highest to lowest.
 :
 : @param resolvedMapMap Data map representing the resolved map
 : @param spaceDefiner Element, map or topicref, that defines a new key space.
 : @parentSpace The parent key space (enables walking up the key space ancestry)
 : @return map object with the following members:
 :
 : 'scopeNames' : A sequence, possibly empty, of the scope names for the scope.
 :                (Only the root scope can have no name).
 : 'keydefs'     : Sequence of key-to-definition maps for each of the keys
 :                 defined in the key space or its descendant key spaces.
 : 'childSpaces' : Sequence of maps objects representing the child key spaces
 :                 of this key space.
 : 'parentSpace' : The parent key space's map. The root space has no parent.
 : 'resolvedMapMap' : The resolved map data map.
 :)
declare function lmm:constructKeySpaceMap(
                         $resolvedMapMap as map(*),
                         $spaceDefiner as element(),
                         $parentSpace as map(*)?
                        ) as map(*) {

   (: Walk the element tree, collecting key definitions and child key
      key spaces as we go.

      Result is the key space map.
      :)
   let $initialKeySpaceMap :=
           map{'definer' : $spaceDefiner,
               'scopeNames' : if ($spaceDefiner/@keyscopes)
                                 then tokenize($spaceDefiner/@keyscopes, ' ')
                                 else (),
               'parentSpace' : $parentSpace,
               'keydefs' : map {},
               'resolvedMapMap' : $resolvedMapMap
              }
   let $thisScopeKeyDefs :=
       $spaceDefiner//descendant-or-self::*[df:class(., 'map/topicref')][@keys]

   let $result := lmm:addKeyDefs($initialKeySpaceMap, $thisScopeKeyDefs)
   return $result
};

(:~
 : Add a set of key definitions to a key space, returning the updated
 : key space map.
 :
 : Calls itself recursively to process the list of elements.
 :
 : @param keySpace Key space map to add the key definitions to
 : @param elems Key-defining elements
 : @return New key space map with the key definitions added.
 :)
declare function lmm:addKeyDefs($keySpace, $elems as element()*) as map(*) {

let $result :=
    if (count($elems) = 0)
       then $keySpace
       else
         let $newKeySpace := lmm:addKeyDef($keySpace, $elems[1])
         return lmm:addKeyDefs($newKeySpace, tail($elems))

return $result
};

(:~
 : Adds a key-defining element's definition to the key space if
 : it actually defines a key (this function can be called on any
 : topicref)
 :
 : @param keySpace The key space to add the definition to
 : @param elem The key-defining element to add
 : @return Updated key space map.
 :)
declare function lmm:addKeyDef($keySpace, $elem as element()) as map(*) {
  let $keyDefs := $keySpace('keydefs') (: Map of key name to sequence of key defs :)

  let $keyNames := tokenize($elem/@keys, ' ')
  let $newKeyDefs :=
     map:merge((
       $keyDefs,
       for $keyName in $keyNames
           let $existingDefsForKey :=
                if (exists($keyDefs))
                   then $keyDefs($keyName)
                   else ()

           return map{$keyName :
                      ($existingDefsForKey, $elem)
                     }))
  let $result := map:put($keySpace, 'keydefs', $newKeyDefs)
  return $result
};

(:~
 : Store a resolved map in the metadata database.
 :
 : @param map DITA map that is the root of the resolved map.
 : @param resolvedMap The resolved map constructed from the content map
 : @param metadataDbName Name of the metadata database to store the
 :        resolved map in
 : @log Sequence, possibly empty, of log message elements.
 : @logID ID of the log to write the messages to.
 :)
declare %updating function lmm:storeResolvedMap(
                                    $dataMap as map(*),
                                    $metadataDbName as xs:string,
                                    $log as element()*,
                                    $logID as xs:string) {
  let $map := $dataMap('map')
  let $resolvedMap := $dataMap('resolvedMap')
  let $log := $dataMap('log')
  let $resolvedMapURI := $dataMap('resolvedMapURI')
  return (db:replace($metadataDbName, $resolvedMapURI, $resolvedMap) (:,
          FIXME: Write to log
          db:output(($log, <info>Stored resolved map "{$resolvedMapURI}"</info>)) :)
          )

};

(:~
 : Takes a root map and returns the resolved map within a map
 : containing the original map, resolved map, and any log messages:
 :
 : @param map Content map to be resolved.
 :
 : @return Map with the following members:
 :
 : map : The input map
 : resolvedMap : The resolved map
 : resolvedMapURI: The URI of the resolved map. This is the URI the map
 : will be stored at (but may not yet be stored at due to order of update
 : operations)
 : log : Sequence, possibly empty, of log entry elements.
 :
 : The resolution process preserves the original structure and
 : adds <topicgroup> elements to record submap boundaries.
 :
 : The resulting resolved map reflects all directly-referenced submaps.
 : Because the resolved map is used for constructing key spaces it is
 : not useful or necessary to resolve key-based map references as those
 : maps cannot contribute to the final set of effective key spaces.
 :
 :)
declare function lmm:resolveMap(
                     $map as element()) as map(*) {

    let $resolvedMap :=
          element {name($map)}
            {
              attribute origMapURI { document-uri(root($map)) },
              attribute origMapDB { db:name($map) },
              attribute xml:base { encode-for-uri(document-uri(root($map))) },
              $map/@*,
              for $node in $map/node()
                  return lmm:resolveMapHandleNode($node)
            }
    let $result :=
       map{
           'map' : $map,
           'resolvedMap' : $resolvedMap,
           'resolvedMapURI' : lmutil:getResolvedMapURIForMap($map),
           'log' : ()
          }
    return $result
};

(:~
 : Simple identity transform dispatch handler for nodes of any type.
 :)
declare function lmm:resolveMapHandleNode(
                     $node as node()) as node()* {
  typeswitch ($node)
    case element() return lmm:resolveMapHandleElement(
                              $node)
    default return $node
};

(:~
 : Apply identity transform to elements of any type.
 :)
declare function lmm:resolveMapHandleElement(
                       $elem as element()) as element()* {
   let $result :=
     if (df:class($elem, 'map/topicref'))
        then lmm:resolveMapHandleTopicref($elem)
        else lmm:resolveMapCopy($elem)
   return $result
};

(:~
 : Copy an element as for xsl:copy instruction.
 :)
declare function lmm:resolveMapCopy(
                    $elem as element()) as element()* {
   let $result :=
     element {name($elem)} {
        for $node in ($elem/@*, $elem/node())
            return lmm:resolveMapHandleNode($node)
     }
   return $result
};

(:~
 : Handle topicrefs. If the topicref is to a local-scope map, then resolve the topicref
 : to the map and include the map in the resolved result, otherwise apply normal
 : copy processing.
 :)
declare function lmm:resolveMapHandleTopicref(
                      $elem as element()) as element()* {
   let $result :=
     if ($elem/@format = ('ditamap') and df:getEffectiveScope($elem) = ('local'))
        then lmm:resolveMapHandleMapRef($elem)
        else lmm:resolveMapCopy($elem)
   return $result
};

(:~
 : Handle local-scope map references.
 :
 : Constructs a <dfst:submap> element that captures the details about the original
 : submap and then applies the identity tranform to the topicref and reltable
 : children of the submap.
 :
 : The dfst:submap element sets the xml:base attribute to the URI of the submap document
 : so that URI references copied from the included map will be correct.
 :
 :)
declare function lmm:resolveMapHandleMapRef(
                        $elem as element()) as element()* {
  let $resolutionMap as map(*) := df:resolveTopicRef($elem)
  let $submap := $resolutionMap('target')
  (: FIXME: add resolution messages to log once we get logging infrastructure in place :)
  return
    <dfst:submap
      xml:base="{document-uri(root($submap))}"
      origMapURI="{document-uri(root($submap))}"
      origMapClass="{string($submap/@class)}"
      class="+ map/topicref map-d/topicgroup dfst-d/submap "
    >
      {lmm:constructMergedKeyscopeAtt($elem, $submap)}
     <topicmeta class="- map/topicmeta ">
       <navtitle class="- map/navtitle ">{($submap/*[df:class(., 'topic/title')], string($submap/@title))[1]}</navtitle>
     </topicmeta>
      {for $e in $submap/*[df:class(., 'map/topicref') or df:class(., 'map/reltable')]
          return lmm:resolveMapHandleElement($e)
      }
    </dfst:submap>
};

(:~
 : Merge the key scope names from two elements that may both specify @keyscope
 :)
declare function lmm:constructMergedKeyscopeAtt(
                        $elem1 as element(),
                        $elem2 as element()) as attribute()? {
   let $scopeNames as xs:string? := lmm:mergeKeyScopeNames($elem1, $elem2)
   let $result :=
       if ($scopeNames)
          then attribute keyscope {$scopeNames}
          else ()
   return $result
};

(:~
 : Merge the key scope names from two elements that may both specify @keyscope
 :)
declare function lmm:mergeKeyScopeNames($elem1 as element(), $elem2 as element()) as xs:string? {

  let $scopeNames := (tokenize($elem1/@keyscope), tokenize($elem2/@keyscope))
  let $result :=
      if (count($scopeNames) > 0)
         then string-join(distinct-values($scopeNames), " ")
         else ()
  return $result
};

(:~
 : Expand the @keys value to add scope qualifications for all the scopes
 : @return keys attribute node with the expanded values.
 :)
declare function lmm:expandKeyNames($keysAtt as attribute(keys),
                                    $keyScopes as array(*)) as attribute() {

  let $baseKeyNames as xs:string* :=
                    tokenize(string($keysAtt), " ")
  let $expandedKeyNames :=
      for $keyName in $baseKeyNames
          return lmm:scopeQualifyKeyName(
                      $keyName,
                      $keyScopes)
  let $result := attribute {name($keysAtt)}
                {string-join($expandedKeyNames, ' ')}
  return $result
};

(:~
 : Given a key name and an array of key scope sequences returns the
 : names reflecting the qualification of the base key name using the
 : the scope hierarchy.
 :
 : @param keyName The base key name to be qualified
 : @param keyScopes Array of sequences of scope names, where each array member
 :                  reflects one level in the scope hierarchy, ordered from
 :                  highest (root scope, array index 1), lowest (array index last()).
 : @return List of qualified key names.
 :)
declare function lmm:scopeQualifyKeyName(
                     $keyName as xs:string,
                     $keyScopes as array(*)) as xs:string* {
  (: We process the key scopes from nearest (lowest) to
     farthest (highest) but the scopes come in ordered from
     highest to lowest. Reversing the scope array so that
     the called function can use array:head and array:tail
     on the array.
   :)
  let $result := ($keyName,
                 lmm:applyScopesToNames(
                           ($keyName),
                           array:reverse($keyScopes),
                           ()))
  return $result
};

(:~
 : Apply key scopes to a set of base names to produce
 : a set of scope-qualified names.
 :
 : @param baseNames Sequence of base names to be qualified.
 : @param keyScopes Array of sequences of key scope names.
 : @param qualifiedNames Sequence of qualified names
 :
 : @return Qualified names.
 :)
declare function lmm:applyScopesToNames(
                     $keyNames as xs:string+,
                     $keyScopes as array(*),
                     $qualifiedNames as xs:string*) as xs:string* {
  let $result :=
      if (array:size($keyScopes) = 0)
         then $qualifiedNames
         else
           let $newNames :=
               for $name in $keyNames
                   return for $scopeName in $keyScopes(1)
                          return $scopeName || "." || $name
           return lmm:applyScopesToNames(
                      $newNames,
                      array:tail($keyScopes),
                      ($qualifiedNames, $newNames)
                  )
  return $result
};

(: End of Module :)

(: =====================================================

   DITA Link Manager Model

   Model component of Model/View/Controller. Maintains
   the link management data.

   Provides queries to create and update resource use records,
   dependency indexes, etc.

   These queries are called whenever new content is added to
   given database.

   Author: W. Eliot Kimber

   Copyright (c) 2015 DITA For Small Teams
   Licensed under Apache License 2

   ===================================================== :)

module namespace lmm="http://dita-for-small-teams.org/xquery/modules/linkmgr-model";

declare namespace dfst="http://dita-for-small-teams.org";

import module namespace df="http://dita-for-small-teams.org/xquery/modules/dita-utils";
import module namespace bxutil="http://dita-for-small-teams.org/xquery/modules/basex-utils";
import module namespace lmutil="http://dita-for-small-teams.org/xquery/modules/linkmgmt-utils";
import module namespace dfstcnst="http://dita-for-small-teams.org/xquery/modules/dfst-constants";
import module namespace relpath="http://dita-for-small-teams.org/xquery/modules/relpath-utils";

declare %updating function lmm:createDirectLinkResourceRecords(
        $metadataDbName as xs:string,
        $directLinks as map(*)*,
        $logID as xs:string) {
    for $linkItem in $directLinks
       let $resolveResult := lmutil:resolveDirectLink($linkItem)
       return lmm:createOrUpdateResourceUseRecord(
                   $metadataDbName,
                   $linkItem,
                   $resolveResult,
                   $logID)

};

declare %updating function lmm:createIndirectLinkResourceRecords(
                               $metadataDbName as xs:string,
                               $indirectLinks as map(*)*,
                               $logID as xs:string) {
    for $linkItem in $indirectLinks
        let $resolveResult as map(*) := lmutil:resolveIndirectLink($linkItem)
        return lmm:createOrUpdateResourceUseRecord(
                   $metadataDbName,
                   $linkItem,
                   $resolveResult,
                   $logID)
};

(: Given a link and the resolved result, creates a resource use record for each
   target the link resolved to.

 :)
declare %updating function lmm:createOrUpdateResourceUseRecord(
                              $metadataDbName,
                              $linkItem as map(*),
                              $resolveResult as map(*),
                              $logID as xs:string) {
   let $resolveResult as map(*) := lmutil:resolveDirectLink($linkItem)
   let $targets := $resolveResult('target')
   for $target in $targets
       return lmm:createOrUpdateResourceUseRecordForLinkTarget(
                    $metadataDbName,
                    $linkItem,
                    $target,
                    $logID)
};

(:
 : Creates a use record reflecting the use of the target element by the document
 : that contains the link.
 :
 : The use records are organized into directories, one directory per used document,
 : where the directory name is the MD5 hash of the document URI (just to make a
 : shorter name that won't have any problematic characters).
 :
 : Each use record document's filename is the MD5 hash of the linking document's
 : URI plus the MD5 hash of the linking element.
 :
 : Potential problem: the same element could, in theory, link to the same target in
 : two different ways: as a link and as a conref, although this case is extremely
 : unlikely. Really need a way to distinguish each abstract link represented by
 : a given DITA link-establishing element but the current implementation doesn't
 : provide that abstraction. It could through a refactor. Keeping it simple for now.
 :)
declare %updating function lmm:createOrUpdateResourceUseRecordForLinkTarget(
           $metadataDbName as xs:string,
           $linkItem as map(*),
           $target as element(),
           $logID as xs:string) {
   let $targetDoc := root($target)
   let $link := $linkItem('link')
   let $containingDir := concat($dfstcnst:where-used-dir, '/',
                                lmutil:constructResourceKeyForElement($target),
                                '/')
   let $reskey := lmutil:constructResourceKeyForElement($link)
   let $recordFilename := concat('use-record_', $reskey, '.xml')
   let $format := if ($link/@format)
                     then string($link/@format)
                     else 'dita'
   let $scope := if ($link/@scope)
                     then string($link/@scope)
                     else 'local'
   let $linkContext := $linkItem('linkContext')

   (: df:getTitleForLinkElementContainer($link) :)
   let $useRecord :=
     <dfst:useRecord resourceKey="{$reskey}"
                     targetDoc="{document-uri($targetDoc)}"
                     usingDoc="{document-uri(root($link))}"
                     linkType="{df:getBaseLinkType($link)}"
                     linkClass="{string($link/@class)}"
                     linkContext="{$linkContext}"
                     format="{$format}"
                     scope="{$scope}"
     >
       <title>{if (df:class($link, 'map/topicref'))
                  then string((root($link)/*/*[df:class(., 'topic/title')] |
                        root($link)/*/@title)[1])
                  else string($link/ancestor::*[df:class(., 'topic/topic')][1]/*[df:class(., 'topic/title')])}</title>
     </dfst:useRecord>
    let $useRecordUri := relpath:newFile($containingDir, $recordFilename)
    return try {
       (
       if (db:exists($metadataDbName, $useRecordUri))
          then db:delete($metadataDbName, $useRecordUri)
          else(),
       db:replace($metadataDbName,
                  $useRecordUri,
                  $useRecord) (:,
       (: FIXME: Write record to log doc :)
       db:output(<info>Stored use record "{$useRecordUri}"</info>) :)
       )
    } catch * {
       (: FIXME: Write record to log doc :)
       error('LMI-UpdateUseRecord001',
             concat('Error storing use record to "', $useRecordUri, '": ', $err:description))
    }
};

(:~
 : Construct key spaces from root maps.
 :
 : @param contentDbName Name of datbase that contains the content to construct key spaces from
 : @param metadataDbName Name of metadata database to store construct key spaces in.
 :
 : Finds all the root maps (requires that all direct-link use records are up to date).
 :
 : For each root map, constructs and stores the resolved map.
 :
 :)
declare %updating function lmm:constructKeySpaces(
        $contentDbName as xs:string,
        $metadataDbName as xs:string,
        $logID as xs:string) {

  (: General process:

     1. Find all DITA maps

     2. For each map, construct a resolved map that preserves submap
        boundaries and original source map details.

     The resolved maps can then be used to determine the effective
     binding for any key without the need to separately construct
     key spaces. This technique was pioneered and suggested by
     Chris Nitchie, designer of the DITA 1.3 scoped keys mechanism.

     Note that there's no point in trying to determine if a given
     map is a "root map" as any map can, in theory, be used as a root
     map. Likewise, a map that happens to have no local-scope
     topicrefs to it may not be intended to be the root of a publication.

     One alternative to this approach is to only consider maps that have
     peer-scope topicrefs to them to be root maps (per the DITA 1.3
     meaning for peer-scope topicrefs to dita maps). In this case, there needs
     to be a "master map" that serves to simply identify the "top level
     root maps", that is, the maps that are considered by the document
     authors to be The root maps, that is, the maps they consider to
     be the roots of publications. Given this master map the system can
     then consider only peer-scope map references in order to determine
     the root maps and thus the maps for which key space construction
     is relevant.

     Key spaces are constructed from resolved maps and stored in the metadata
     database.

   :)

   let $contentMaps := collection($contentDbName)/*[df:class(., 'map/map')][lmutil:isRootMap(.)]
   (: Sequence of maps containing the things to be stored :)
   let $dataToStore as map(*)* := lmm:constructKeySpacesToBeStored($contentMaps)
   return
      for $obj in $dataToStore
            let $ditaMap := $obj('resolvedMapMap')('map')
            let $resolvedMapMap := $obj('resolvedMapMap')
            let $keySpace as element(keyspace) := $obj('keySpace')
            return ((: FIXME: Write to log. db:output(<info>Resolving map {document-uri(root($ditaMap))}...</info>), :)
                    lmm:storeResolvedMap($resolvedMapMap,
                                         $metadataDbName,
                                         $obj('log'),
                                         $logID),
                    lmm:storeKeySpace(
                                         $keySpace,
                                         $metadataDbName,
                                         $logID)
                     )
};

(:~
 : Constructs the XML key space documents to be stored for each content map.
 :
 : @param contentMaps Sequence of content (unresolved) map elements.
 : @return Sequence of maps with two items:
 :
 : 'resolvedMapMap' : The resolved map data map. Includes the content map
 :                    as an item.
 : 'keySpace' : The XML representation of the key space for the map
 :)
declare function lmm:constructKeySpacesToBeStored($contentMaps as element()*) as map(*)* {
  let $result :=
    for $ditaMap in $contentMaps
        let $resolvedMapMap as map(*) := lmm:resolveMap($ditaMap)
        let $dataMap := lmm:constructKeySpaceForResolvedMap($resolvedMapMap)
        return $dataMap
  return $result
};

(:~
 : Store a keyspace document in the metadata database.
 :
 : @param keySpace XML document representing the key space to be stored
 : @param metadataDbName Name of the metadata database to store the
 :        resolved map in
 : @logID ID of the log to write the messages to.
 :)
declare %updating function lmm:storeKeySpace(
                     $keySpace as element(keyspace),
                     $metadataDbName as xs:string,
                     $logID as xs:string) {
  let $keySpaceURI := lmutil:getKeySpaceURIForKeySpace($keySpace)
  return (db:replace($metadataDbName, $keySpaceURI, $keySpace) (: ,
          FIXME: Write to log doc.
          db:output((<info>Stored key space "{$keySpaceURI}"</info>)):)
          )
};

(:~
 : Given a resolved DITA map, construct the key space map
 : for it.
 :
 : The key space map reflects the hierarchy of key spaces defined
 : in the map. Every root map represents at least one, possibly
 : empty, key space.
 :
 : @param map Source content map (returned in the result map)
 : @param resolvedMap Resolved map constructed from the source map
 : @return Map containing the members:
 :
 :   map : The content map
 :   resolvedMap : The resolved map the key spaces were constructed from
 :   keySpace : The key space document rooted at the resolved map.
 :)
declare function lmm:constructKeySpaceForResolvedMap(
                       $resolvedMapMap as map(*)) as map(*) {
   let $ditaMap as element() := $resolvedMapMap('map')
   let $keyspace as element(keyspace) :=
         lmm:constructKeySpace(
                 $resolvedMapMap,
                 $resolvedMapMap('resolvedMap'))

   let $result :=
      map{ 'map' : $ditaMap,
           'resolvedMapMap' : $resolvedMapMap,
           'keySpace' : $keyspace
         }
   return $result
};

(:~
 : Given a content map element, return the corresponding keyspace document.
 : The key space must have already been constructed.
 :)
declare function lmm:getKeySpaceForMap($contentMap as element()) as element()? {
    let $resolvedMapURI := lmutil:getResolvedMapURIForMap($contentMap)
    let $metadataDbName as xs:string := bxutil:getMetadataDbNameForDoc(root($contentMap))

    let $keyspaceURI := lmutil:getKeySpaceURIForResolvedMapURI($resolvedMapURI)
    let $result := collection($metadataDbName || $keyspaceURI)/keyspace
    return $result
};

(:~
 : The identity of a key space is the element that defines it, either map
 : or a topicref. In order to resolve a key you must know the key space
 : hierarchy because you have to start with the root key space, see if
 : the key-as-referenced is defined in that space, then if not, walk
 : down the ancestor tree until you either find a match for the key
 : name or run out of options.
 :
 : Logically, an ancestor key space reflects in itself all the scope-qualified
 : names of the keys from its descentant key space that it does not
 : explicitly override, meaning that, starting from any descedant key space you
 : can resolve any fully-qualified key name.
 :
 : @param resolvedMapMap Data map that represents the resolved map.
 : @param spaceDefiner Element, map or topicref, that defines a new key space.
 :
 : @return Keyspace element, which will contain any descendant keyspaces
 :)
declare function lmm:constructKeySpace(
                      $resolvedMapMap as map(*),
                      $spaceDefiner as element()) as element() {
  let $keyspaceMap := lmm:constructKeySpaceMap($resolvedMapMap, $spaceDefiner, ())
  let $result := lmm:keySpaceMapToXML($keyspaceMap)
  return $result
};

(:~
 : Given a key space map, return an XML serialization of it suitalbe for storing
 : in the XML metadata store
 :)
declare function lmm:keySpaceMapToXML($keyspaceMap as map(*)) as element(keyspace) {

let $definer as element() := $keyspaceMap('definer')
let $scopeNames as xs:string* := $keyspaceMap('scopeNames')
let $keydefs as map(*)? := $keyspaceMap('keydefs')
let $childScopes as map(*)* := $keyspaceMap('childScopes')
let $resolvedMapMap as map(*) := $keyspaceMap('resolvedMapMap')

let $result :=
<keyspace
  resolvedMap="{$resolvedMapMap('resolvedMapURI')}"
>{
 <scopeNames>{
   for $name in $scopeNames
       return <scopeName>{$name}</scopeName>
 }</scopeNames>,
 <keys>{
 for $keyName in map:keys($keydefs) order by $keyName
     return <key name="{$keyName}">{
               $keydefs($keyName)
             }</key>
 }</keys>,
 <childScopes>{
 for $childScope in $childScopes
     return lmm:keySpaceMapToXML($childScope)
 }</childScopes>
}</keyspace>

return $result

};

(:~
 : Construct map object that reflects the key space rooted at the specified
 : key space defining element.
 :
 : The map is a map of key names to key definitions, where a given key name
 : may reflect multiple definitions. The key definitions are in precedence
 : order from highest to lowest.
 :
 : @param resolvedMapMap Data map representing the resolved map
 : @param spaceDefiner Element, map or topicref, that defines a new key space.
 : @parentSpace The parent key space (enables walking up the key space ancestry)
 : @return map object with the following members:
 :
 : 'scopeNames' : A sequence, possibly empty, of the scope names for the scope.
 :                (Only the root scope can have no name).
 : 'keydefs'     : Sequence of key-to-definition maps for each of the keys
 :                 defined in the key space or its descendant key spaces.
 : 'childSpaces' : Sequence of maps objects representing the child key spaces
 :                 of this key space.
 : 'parentSpace' : The parent key space's map. The root space has no parent.
 : 'resolvedMapMap' : The resolved map data map.
 :)
declare function lmm:constructKeySpaceMap(
                         $resolvedMapMap as map(*),
                         $spaceDefiner as element(),
                         $parentSpace as map(*)?
                        ) as map(*) {

   (: Walk the element tree, collecting key definitions and child key
      key spaces as we go.

      Result is the key space map.
      :)
   let $initialKeySpaceMap :=
           map{'definer' : $spaceDefiner,
               'scopeNames' : if ($spaceDefiner/@keyscopes)
                                 then tokenize($spaceDefiner/@keyscopes, ' ')
                                 else (),
               'parentSpace' : $parentSpace,
               'keydefs' : map {},
               'resolvedMapMap' : $resolvedMapMap
              }
   let $thisScopeKeyDefs :=
       $spaceDefiner//descendant-or-self::*[df:class(., 'map/topicref')][@keys]

   let $result := lmm:addKeyDefs($initialKeySpaceMap, $thisScopeKeyDefs)
   return $result
};

(:~
 : Add a set of key definitions to a key space, returning the updated
 : key space map.
 :
 : Calls itself recursively to process the list of elements.
 :
 : @param keySpace Key space map to add the key definitions to
 : @param elems Key-defining elements
 : @return New key space map with the key definitions added.
 :)
declare function lmm:addKeyDefs($keySpace, $elems as element()*) as map(*) {

let $result :=
    if (count($elems) = 0)
       then $keySpace
       else
         let $newKeySpace := lmm:addKeyDef($keySpace, $elems[1])
         return lmm:addKeyDefs($newKeySpace, tail($elems))

return $result
};

(:~
 : Adds a key-defining element's definition to the key space if
 : it actually defines a key (this function can be called on any
 : topicref)
 :
 : @param keySpace The key space to add the definition to
 : @param elem The key-defining element to add
 : @return Updated key space map.
 :)
declare function lmm:addKeyDef($keySpace, $elem as element()) as map(*) {
  let $keyDefs := $keySpace('keydefs') (: Map of key name to sequence of key defs :)

  let $keyNames := tokenize($elem/@keys, ' ')
  let $newKeyDefs :=
     map:merge((
       $keyDefs,
       for $keyName in $keyNames
           let $existingDefsForKey :=
                if (exists($keyDefs))
                   then $keyDefs($keyName)
                   else ()

           return map{$keyName :
                      ($existingDefsForKey, $elem)
                     }))
  let $result := map:put($keySpace, 'keydefs', $newKeyDefs)
  return $result
};

(:~
 : Store a resolved map in the metadata database.
 :
 : @param map DITA map that is the root of the resolved map.
 : @param resolvedMap The resolved map constructed from the content map
 : @param metadataDbName Name of the metadata database to store the
 :        resolved map in
 : @log Sequence, possibly empty, of log message elements.
 : @logID ID of the log to write the messages to.
 :)
declare %updating function lmm:storeResolvedMap(
                                    $dataMap as map(*),
                                    $metadataDbName as xs:string,
                                    $log as element()*,
                                    $logID as xs:string) {
  let $map := $dataMap('map')
  let $resolvedMap := $dataMap('resolvedMap')
  let $log := $dataMap('log')
  let $resolvedMapURI := $dataMap('resolvedMapURI')
  return (db:replace($metadataDbName, $resolvedMapURI, $resolvedMap) (:,
          FIXME: Write to log
          db:output(($log, <info>Stored resolved map "{$resolvedMapURI}"</info>)) :)
          )

};

(:~
 : Takes a root map and returns the resolved map within a map
 : containing the original map, resolved map, and any log messages:
 :
 : @param map Content map to be resolved.
 :
 : @return Map with the following members:
 :
 : map : The input map
 : resolvedMap : The resolved map
 : resolvedMapURI: The URI of the resolved map. This is the URI the map
 : will be stored at (but may not yet be stored at due to order of update
 : operations)
 : log : Sequence, possibly empty, of log entry elements.
 :
 : The resolution process preserves the original structure and
 : adds <topicgroup> elements to record submap boundaries.
 :
 : The resulting resolved map reflects all directly-referenced submaps.
 : Because the resolved map is used for constructing key spaces it is
 : not useful or necessary to resolve key-based map references as those
 : maps cannot contribute to the final set of effective key spaces.
 :
 :)
declare function lmm:resolveMap(
                     $map as element()) as map(*) {

    let $resolvedMap :=
          element {name($map)}
            {
              attribute origMapURI { document-uri(root($map)) },
              attribute origMapDB { db:name($map) },
              attribute xml:base { encode-for-uri(document-uri(root($map))) },
              $map/@*,
              for $node in $map/node()
                  return lmm:resolveMapHandleNode($node)
            }
    let $result :=
       map{
           'map' : $map,
           'resolvedMap' : $resolvedMap,
           'resolvedMapURI' : lmutil:getResolvedMapURIForMap($map),
           'log' : ()
          }
    return $result
};

(:~
 : Simple identity transform dispatch handler for nodes of any type.
 :)
declare function lmm:resolveMapHandleNode(
                     $node as node()) as node()* {
  typeswitch ($node)
    case element() return lmm:resolveMapHandleElement(
                              $node)
    default return $node
};

(:~
 : Apply identity transform to elements of any type.
 :)
declare function lmm:resolveMapHandleElement(
                       $elem as element()) as element()* {
   let $result :=
     if (df:class($elem, 'map/topicref'))
        then lmm:resolveMapHandleTopicref($elem)
        else lmm:resolveMapCopy($elem)
   return $result
};

(:~
 : Copy an element as for xsl:copy instruction.
 :)
declare function lmm:resolveMapCopy(
                    $elem as element()) as element()* {
   let $result :=
     element {name($elem)} {
        for $node in ($elem/@*, $elem/node())
            return lmm:resolveMapHandleNode($node)
     }
   return $result
};

(:~
 : Handle topicrefs. If the topicref is to a local-scope map, then resolve the topicref
 : to the map and include the map in the resolved result, otherwise apply normal
 : copy processing.
 :)
declare function lmm:resolveMapHandleTopicref(
                      $elem as element()) as element()* {
   let $result :=
     if ($elem/@format = ('ditamap') and df:getEffectiveScope($elem) = ('local'))
        then lmm:resolveMapHandleMapRef($elem)
        else lmm:resolveMapCopy($elem)
   return $result
};

(:~
 : Handle local-scope map references.
 :
 : Constructs a <dfst:submap> element that captures the details about the original
 : submap and then applies the identity tranform to the topicref and reltable
 : children of the submap.
 :
 : The dfst:submap element sets the xml:base attribute to the URI of the submap document
 : so that URI references copied from the included map will be correct.
 :
 :)
declare function lmm:resolveMapHandleMapRef(
                        $elem as element()) as element()* {
  let $resolutionMap as map(*) := df:resolveTopicRef($elem)
  let $submap := $resolutionMap('target')
  (: FIXME: add resolution messages to log once we get logging infrastructure in place :)
  return
    <dfst:submap
      xml:base="{document-uri(root($submap))}"
      origMapURI="{document-uri(root($submap))}"
      origMapClass="{string($submap/@class)}"
      class="+ map/topicref map-d/topicgroup dfst-d/submap "
    >
      {lmm:constructMergedKeyscopeAtt($elem, $submap)}
     <topicmeta class="- map/topicmeta ">
       <navtitle class="- map/navtitle ">{($submap/*[df:class(., 'topic/title')], string($submap/@title))[1]}</navtitle>
     </topicmeta>
      {for $e in $submap/*[df:class(., 'map/topicref') or df:class(., 'map/reltable')]
          return lmm:resolveMapHandleElement($e)
      }
    </dfst:submap>
};

(:~
 : Merge the key scope names from two elements that may both specify @keyscope
 :)
declare function lmm:constructMergedKeyscopeAtt(
                        $elem1 as element(),
                        $elem2 as element()) as attribute()? {
   let $scopeNames as xs:string? := lmm:mergeKeyScopeNames($elem1, $elem2)
   let $result :=
       if ($scopeNames)
          then attribute keyscope {$scopeNames}
          else ()
   return $result
};

(:~
 : Merge the key scope names from two elements that may both specify @keyscope
 :)
declare function lmm:mergeKeyScopeNames($elem1 as element(), $elem2 as element()) as xs:string? {

  let $scopeNames := (tokenize($elem1/@keyscope), tokenize($elem2/@keyscope))
  let $result :=
      if (count($scopeNames) > 0)
         then string-join(distinct-values($scopeNames), " ")
         else ()
  return $result
};

(:~
 : Expand the @keys value to add scope qualifications for all the scopes
 : @return keys attribute node with the expanded values.
 :)
declare function lmm:expandKeyNames($keysAtt as attribute(keys),
                                    $keyScopes as array(*)) as attribute() {

  let $baseKeyNames as xs:string* :=
                    tokenize(string($keysAtt), " ")
  let $expandedKeyNames :=
      for $keyName in $baseKeyNames
          return lmm:scopeQualifyKeyName(
                      $keyName,
                      $keyScopes)
  let $result := attribute {name($keysAtt)}
                {string-join($expandedKeyNames, ' ')}
  return $result
};

(:~
 : Given a key name and an array of key scope sequences returns the
 : names reflecting the qualification of the base key name using the
 : the scope hierarchy.
 :
 : @param keyName The base key name to be qualified
 : @param keyScopes Array of sequences of scope names, where each array member
 :                  reflects one level in the scope hierarchy, ordered from
 :                  highest (root scope, array index 1), lowest (array index last()).
 : @return List of qualified key names.
 :)
declare function lmm:scopeQualifyKeyName(
                     $keyName as xs:string,
                     $keyScopes as array(*)) as xs:string* {
  (: We process the key scopes from nearest (lowest) to
     farthest (highest) but the scopes come in ordered from
     highest to lowest. Reversing the scope array so that
     the called function can use array:head and array:tail
     on the array.
   :)
  let $result := ($keyName,
                 lmm:applyScopesToNames(
                           ($keyName),
                           array:reverse($keyScopes),
                           ()))
  return $result
};

(:~
 : Apply key scopes to a set of base names to produce
 : a set of scope-qualified names.
 :
 : @param baseNames Sequence of base names to be qualified.
 : @param keyScopes Array of sequences of key scope names.
 : @param qualifiedNames Sequence of qualified names
 :
 : @return Qualified names.
 :)
declare function lmm:applyScopesToNames(
                     $keyNames as xs:string+,
                     $keyScopes as array(*),
                     $qualifiedNames as xs:string*) as xs:string* {
  let $result :=
      if (array:size($keyScopes) = 0)
         then $qualifiedNames
         else
           let $newNames :=
               for $name in $keyNames
                   return for $scopeName in $keyScopes(1)
                          return $scopeName || "." || $name
           return lmm:applyScopesToNames(
                      $newNames,
                      array:tail($keyScopes),
                      ($qualifiedNames, $newNames)
                  )
  return $result
};

(: End of Module :)