http://mathling.com/svg/draw  library module

http://mathling.com/svg/draw


Module providing basic drawing functions: geo objects => SVG

Copyright© Mary Holstege 2020-2023
CC-BY (https://creativecommons.org/licenses/by/4.0/)

December 2021
Status: Stable, subject to additions

Imports

http://mathling.com/art/core
import module namespace core="http://mathling.com/art/core"
       at "../art/core.xqy"
http://mathling.com/geometric/edge
import module namespace edge="http://mathling.com/geometric/edge"
       at "../geo/edge.xqy"
http://mathling.com/core/utilities
import module namespace util="http://mathling.com/core/utilities"
       at "../core/utilities.xqy"
http://mathling.com/svg/gradients
import module namespace gradient="http://mathling.com/svg/gradients"
       at "../svg/gradients.xqy"
http://mathling.com/svg/effects
import module namespace effect="http://mathling.com/svg/effects"
       at "../svg/effects.xqy"
http://mathling.com/geometric/rectangle
import module namespace box="http://mathling.com/geometric/rectangle"
       at "../geo/rectangle.xqy"
http://mathling.com/geometric
import module namespace geom="http://mathling.com/geometric"
       at "../geo/euclidean.xqy"
http://mathling.com/core/config
import module namespace config="http://mathling.com/core/config"
       at "../core/config.xqy"
http://mathling.com/core/errors
import module namespace errors="http://mathling.com/core/errors"
       at "../core/errors.xqy"
http://mathling.com/geometric/point
import module namespace point="http://mathling.com/geometric/point"
       at "../geo/point.xqy"

Functions

Function: drawing-map
declare function drawing-map() as map(xs:string,function(*)?)


rendering-map()
Mapping from object kind to SVG rendering function

Returns
  • map(xs:string,function(*)?)
declare function this:drawing-map() as map(xs:string,function(*)?)
{
  util:merge-into((
    $geom:DRAWING-MAP,
    map {
      "def": function-lookup(QName("http://mathling.com/type/defref", "draw"), 3),
      "ref": function-lookup(QName("http://mathling.com/type/defref", "draw"), 3),
      "text": function-lookup(QName("http://mathling.com/type/text", "draw"), 3),
      (: Hooks to drawing special functions; callbacks for object drawing :)
      "draw:draw": this:draw#3,
      "draw:svg-style": this:svg-style#1,
      "draw:dump-dynamic-parameters": this:dump-dynamic-parameters#1,
      (: Default no-projection :)
      "draw:project": function ($regions as map(xs:string,item()*)*) as map(xs:string,item()*)* {$regions}
    }
  ))
}

Function: perspective-drawing-map
declare function perspective-drawing-map($canvas as map(xs:string,item()*), $p as xs:double, $q as xs:double, $r as xs:double) as map(xs:string,function(*)?)


perspective-drawing-map()
Drawing map for solid objects.

Params
  • canvas as map(xs:string,item()*): the canvas we are projecting on to; should have a "depth" key If it doesn't we'll use the max of height/width
  • p as xs:double: perspective x parameter
  • q as xs:double: perspective y parameter
  • r as xs:double: perspective z parameter
Returns
  • map(xs:string,function(*)?)
declare function this:perspective-drawing-map(
  $canvas as map(xs:string,item()*),
  $p as xs:double,
  $q as xs:double,
  $r as xs:double
) as map(xs:string,function(*)?)
{
  map {
    (: Special call back :)
    "draw:project": geom:project(?, $canvas, $p, $q, $r)
  }
}

Function: perspective-drawing-map
declare function perspective-drawing-map($canvas as map(xs:string,item()*)) as map(xs:string,function(*)?)


perspective-drawing-map()
Using default perspective parameters (0.5)

Params
  • canvas as map(xs:string,item()*)
Returns
  • map(xs:string,function(*)?)
declare function this:perspective-drawing-map(
  $canvas as map(xs:string,item()*)
) as map(xs:string,function(*)?)
{
  this:perspective-drawing-map(
    $canvas,
    0.5, 0.5, 0.5
  )
}

Function: perspective-drawing-map
declare function perspective-drawing-map() as map(xs:string,function(*)?)


perspective-drawing-map()
Using default perspective parameters (0.5), and medium resolution canvas size
(1600x1600x1600)

Returns
  • map(xs:string,function(*)?)
declare function this:perspective-drawing-map() as map(xs:string,function(*)?)
{
  this:perspective-drawing-map(
    core:canvas("medium")=>map:put("depth", core:width("medium")),
    0.5, 0.5, 0.5
  )
}

Function: draw
declare function draw($items as item()*, $properties as map(xs:string,item()*)?, $drawing as map(xs:string,function(*)?)) as item()*


draw()
Render a set of objects as SVG.

Params
  • items as item()*: the objects to render
  • properties as map(xs:string,item()*)?: styling properties to apply to the items (default=none)
  • drawing as map(xs:string,function(*)?): the map of drawing functions (default=this:drawing-map() merged with the default perspective map for solid objects)
Returns
  • item()*
declare function this:draw(
  $items as item()*,
  $properties as map(xs:string,item()*)?,
  $drawing as map(xs:string,function(*)?)
) as item()*
{
  let $properties := ($properties, map{})[1]
  for $item in $items return typeswitch ($item)
  case map(*) return (
    let $kind := $item("kind")
    return (
      if (empty($kind)) then (
        if (empty($item=>map:keys())) then ()
        else (
          let $draw := $item("draw")
          return (
            if (exists($draw) and ($draw instance of function(*)))
            then $draw($item, $properties, $drawing)
            else (
              let $uri := $item("uri")
              let $draw :=
                if (exists($uri)) then function-lookup(QName($uri, "draw"), 3) else ()
              return (
                if (exists($draw))
                then $draw($item, $properties, $drawing)
                else errors:error("ART-UNKNOWN", trace($item,"item"))
              )
            )
          )
        )
      ) else (
        let $draw := $drawing($kind)
        return (
          if (empty($draw)) then (
            let $draw := $item("draw")
            return (
              if (exists($draw) and ($draw instance of function(*)))
              then $draw($item, $properties, $drawing)
              else (
                let $uri := ($item("uri"),$config:TYPE-MAP($kind))[1]
                let $draw :=
                  if (exists($uri)) then function-lookup(QName($uri, "draw"), 3) else ()
                return (
                  if (exists($draw))
                  then $draw($item, $properties, $drawing)
                  else errors:error("ART-UNKNOWN", trace($item,"item"))
                )
              )
            )
          ) else (
            $draw($item, $properties, $drawing)
          )
        )
      )
    )
  )
  case element(art:component) return
    <svg:metadata>{$item}</svg:metadata>
  case element(art:dynamic-parameter) return
    <svg:metadata>{$item}</svg:metadata>
  case element(art:parameter) return
    <svg:metadata>{$item}</svg:metadata>
  case element(art:metadata) return
    <svg:metadata>{$item/*}</svg:metadata>
  case element() return (: SVG passthrough :)
    if (namespace-uri-from-QName(node-name($item))="http://mathling.com/art")
    then (
      comment { name($item) },
      this:draw(
        $item/node(),
        util:merge-into(this:as-properties($item/@*), $properties),
        $drawing
      ),
      comment { "/"||name($item) }
    ) else (
      element {node-name($item)} {
        this:merge-attributes(
          util:as-attributes($properties),
          $item/@*
        ), 
        this:draw($item/node(), $properties, $drawing)
      }
    )
  case text() return $item
  case document-node() return this:draw($item/node(), $properties, $drawing)
  default return errors:error("ART-UNKNOWN", trace($item,"item"))
}

Function: draw
declare function draw($items as item()*, $properties as map(xs:string,item()*)?) as item()*

Params
  • items as item()*
  • properties as map(xs:string,item()*)?
Returns
  • item()*
declare function this:draw(
  $items as item()*,
  $properties as map(xs:string,item()*)?
) as item()*
{
  this:draw($items, $properties,
    util:merge-into(
      this:drawing-map(),
      let $resolution := ($properties("resolution"),"medium")[1]
      let $p := ($properties("perspective.p"), 0.5)[1]
      let $q := ($properties("perspective.q"), 0.5)[1]
      let $r := ($properties("perspective.r"), 0.5)[1]
      let $canvas := core:canvas($resolution)
      return this:perspective-drawing-map($canvas, $p, $q, $r)
    )
  )
}

Function: draw
declare function draw($items as item()*) as item()*

Params
  • items as item()*
Returns
  • item()*
declare function this:draw($items as item()*) as item()*
{
  this:draw($items, map{},
    util:merge-into(
      this:drawing-map(),
      this:perspective-drawing-map()
    )
  )
}

Function: perspective-draw
declare function perspective-draw($items as item()*, $properties as map(xs:string,item()*)?, $canvas as map(xs:string,item()*)) as item()*

Params
  • items as item()*
  • properties as map(xs:string,item()*)?
  • canvas as map(xs:string,item()*)
Returns
  • item()*
declare function this:perspective-draw(
  $items as item()*,
  $properties as map(xs:string,item()*)?,
  $canvas as map(xs:string,item()*)
) as item()*
{
  this:draw($items, $properties, 
    util:merge-into(
      this:drawing-map(),
      let $p := ($properties("perspective.p"), 0.5)[1]
      let $q := ($properties("perspective.q"), 0.5)[1]
      let $r := ($properties("perspective.r"), 0.5)[1]
      return this:perspective-drawing-map($canvas, $p, $q, $r)
    )
  )
}

Function: colour-buckets
declare function colour-buckets($matrix as map(*), $colouring as function(item()*) as item(), $exclude as function(item()*) as xs:boolean) as map(*)


colour-buckets()
Segregate the points in the matrix into colour buckets based on the
colouring function, which should return a colour string or stop number.
Service function for draw-point-matrix()

Params
  • matrix as map(*): the point array (either core/matrix or core/array)
  • colouring as function(item()*)asitem(): a function that maps a matrix value to a colour, the colour can be either a colour name or RGB string or a gradient stop number, an index into to the colours in $properties("gradient")
  • exclude as function(item()*)asxs:boolean: return true() if we should skip this value
Returns
  • map(*)
declare function this:colour-buckets(
  $matrix as map(*),
  $colouring as function(item()*) as item(),
  $exclude as function(item()*) as xs:boolean
) as map(*)
{
  let $kind := $matrix("kind")
  (: Breaks encapsulation: if we changed keys we'd be in trouble here :)
  let $rows := $matrix("rows")
  let $columns := $matrix("columns")
  let $get := 
    switch ($kind)
    case "matrix" return
      function-lookup(QName("http://mathling.com/core/matrix", "get-if-set"), 3)
    case "array" return
      function-lookup(QName("http://mathling.com/core/array", "get"), 3)
    case "imagearray" return
      function-lookup(QName("http://mathling.com/image/matrix", "get"), 3)
    case "point-matrix" return
      let $get := function-lookup(QName("http://mathling.com/geometric/matrix", "get-if-set"), 3)
      return
        function ($matrix as map(*), $row as xs:integer, $col as xs:integer) as item()* {
          $get($matrix, $col, $row)
        }
    default return errors:error("ART-BADPARMS", ("matrix.kind", $kind))
    
  return (
    map:merge(
      for $row in 1 to $rows
      return map:merge(
        for $col in 1 to $columns
        let $val := $get($matrix, $row, $col)
        return (
          if (empty($val) or $exclude($val)) then ()
          else (
            let $colour := $colouring($val)
            return map {$colour: point:point($col, $row)}
          )
        ),
        map {"duplicates": "combine"}
      )
      ,
      map {"duplicates": "combine"}
    )
    (:
    fold-left(1 to $rows, map{},
      function($buckets as map(*), $row as xs:integer) as map(*) {
        fold-left(1 to $columns, $buckets,
          function($buckets as map(*), $col as xs:integer) as map(*) {
            let $val := $get($matrix, $row, $col)
            return (
              if (empty($val) or $exclude($val)) then $buckets
              else (
                let $colour := $colouring($val)
                return $buckets=>map:put($colour, ($buckets($colour), point:point($col, $row)))
              )
            )
          }
        )
      }
    )
    :)
  )
}

Function: colour-buckets
declare function colour-buckets($matrix as map(*), $colouring as function(item()*) as item()) as map(*)

Params
  • matrix as map(*)
  • colouring as function(item()*)asitem()
Returns
  • map(*)
declare function this:colour-buckets(
  $matrix as map(*),
  $colouring as function(item()*) as item()
) as map(*)
{
  this:colour-buckets($matrix, $colouring,
    function($val as item()*) as xs:boolean { false() }
  )
}

Function: draw-point-matrix
declare function draw-point-matrix($matrix as map(*), $colouring as function(item()*) as item(), $properties as map(xs:string,item()*)) as element()*


draw-point-matrix()
Efficient rendering of a point matrix, for when you have a matrix that
covers the whole canvas in a relatively small number of colours. This
converts each set of points in a single colour bucket into a path with
tiny lines at each point.

Params
  • matrix as map(*): a point matrix, in either core/matrix, core/array, or geometric/matrix format
  • colouring as function(item()*)asitem(): a function that maps a matrix value to a colour, the colour can be either a colour name or RGB string or a gradient stop number, an index into to the colours in $properties("gradient")
  • properties as map(xs:string,item()*): style properties plus "gradient": the gradient to index colour values into (default=none) "width": how wide/long a line to make (default=1)
Returns
  • element()*
declare function this:draw-point-matrix(
  $matrix as map(*),
  $colouring as function(item()*) as item(),
  $properties as map(xs:string,item()*)
) as element()*
{
  this:draw-colour-buckets(
    this:colour-buckets($matrix, $colouring),
    $properties
  )
}

Function: draw-colour-buckets
declare function draw-colour-buckets($buckets as map(*), $properties as map(xs:string,item()*)) as element()*


draw-colour-buckets()
Efficient rendering of a colour buckets; workhorse of draw-point-matrix()
If you use numeric colour keys, properties must include "colours" with
the colour palette

Params
  • buckets as map(*): colour buckets, e.g. output from colour-buckets()
  • properties as map(xs:string,item()*): style properties plus "gradient": the gradient to index colour values into (default=none) "width": how wide/long a line to make (default=1)
Returns
  • element()*
declare function this:draw-colour-buckets(
  $buckets as map(*),
  $properties as map(xs:string,item()*)
) as element()*
{
  let $gradient := $properties("gradient")
  let $colours := gradient:colours($gradient)
  return
  this:draw-colour-buckets(
    $buckets,
    function($key as item()) as node()* {
      let $colour := 
        if ($key instance of xs:numeric) 
        then $colours[$key cast as xs:integer]
        else string($key)
      return util:as-attributes(this:svg-style(map {"colour": $colour}))
    },
    $properties
  )
}

Function: draw-colour-buckets
declare function draw-colour-buckets($buckets as map(*), $keyfn as function(item()) as node()*, $properties as map(xs:string,item()*)) as element()*


draw-colour-buckets()
Efficient rendering of a colour buckets; workhorse of draw-point-matrix()

Params
  • buckets as map(*): colour buckets, e.g. output from colour-buckets()
  • keyfn as function(item())asnode()*: mapping of key to attributes
  • properties as map(xs:string,item()*): style properties plus "gradient": the gradient to index colour values into (default=none) "width": how wide/long a line to make (default=1)
Returns
  • element()*
declare function this:draw-colour-buckets(
  $buckets as map(*),
  $keyfn as function(item()) as node()*,
  $properties as map(xs:string,item()*)
) as element()*
{
  let $style-properties := util:exclude($properties,("gradient","width"))
  let $gradient := $properties("gradient")
  let $colours := gradient:colours($gradient)
  let $width := ($properties("width"),1)[1]
  for $key in $buckets=>map:keys()
  let $all-points := $buckets($key)
  let $chunks := 1 + count($all-points) idiv 10000
  for $chunk in 1 to $chunks
  let $points := $all-points[position() > ($chunk - 1)*10000 and position() <= $chunk*10000]
  let $path := 
    string-join(
      for $pt in $points return (
        edge:map-command('goto','absolute')||" "||
        point:px($pt)||" "||point:py($pt)||
        edge:map-command('line','absolute')||" "||
        (point:px($pt) + $width)||" "||(point:py($pt) + 0*$width)
      ),
      " "
    )
  where exists($points)
  return (
    <svg:path d="{$path}">{
      if (exists($style-properties("stroke-width"))) then ()
      else attribute stroke-width {$width}
      ,
      if (exists($style-properties("stroke-linecap"))) then ()
      else attribute stroke-linecap {"round"}
      ,
      util:as-attributes($style-properties),
      $keyfn($key)
    }</svg:path>
  )
}

Function: to-CSS
declare function to-CSS($name as xs:string, $properties as map(xs:string,item()*))


to-CSS()
Render a property bundle as a CSS class style.

Params
  • name as xs:string: the style name
  • properties as map(xs:string,item()*): the properties
declare function this:to-CSS(
  $name as xs:string,
  $properties as map(xs:string,item()*)
)
{
  let $style-properties := this:svg-style($properties)
  return (
"."||$name||" {
"||
    string-join((
      map:for-each($style-properties,
        function ($key as xs:string, $value as item()*) as xs:string {
          $key||": "||string-join($value!string(.)," ")
        }
      )
    ),";
")||
"
}
"
  )
}

Function: drawing
declare function drawing($canvas as map(xs:string,item()*), $metadata as element()*, $defs as element()*, $styles as xs:string*, $content as item()*)


drawing()
Render a complete SVG. The components here should have already been
drawn (i.e. rendered to SVG)

Params
  • canvas as map(xs:string,item()*): defines size of canvas
  • metadata as element()*: metadata elements
  • defs as element()*: definitions
  • styles as xs:string*: CSS styles
  • content as item()*: body content
declare function this:drawing(
  $canvas as map(xs:string,item()*),
  $metadata as element()*,
  $defs as element()*,
  $styles as xs:string*,
  $content as item()* 
)
{
  <svg:svg version="1.1"
             viewBox="0 0 {box:width($canvas)} {box:height($canvas)}"
  >
    <svg:metadata>{$metadata}</svg:metadata>
    <svg:defs>{$defs}</svg:defs>
    <svg:style type="text/css">{$styles}</svg:style>
    <svg:g>{$content}</svg:g>
  </svg:svg>
}

Function: merge-attributes
declare function merge-attributes($old as attribute(*)*, $new as attribute(*)*) as attribute(*)*

Params
  • old as attribute(*)*
  • new as attribute(*)*
Returns
  • attribute(*)*
declare function this:merge-attributes(
  $old as attribute(*)*,
  $new as attribute(*)*
) as attribute(*)*
{
  let $names := $new!node-name(.)
  return (
    $new, $old[not(node-name(.) = $names)]
  )
}

Function: as-properties
declare function as-properties($attributes as attribute(*)*) as map(xs:string,item()*)?


as-properties()
Convert attributes to a series of properties, for rendering.
Note: no validity checking here; assumes we have pre-sanitized.

Params
  • attributes as attribute(*)*
Returns
  • map(xs:string,item()*)?
declare function this:as-properties(
  $attributes as attribute(*)*
) as map(xs:string,item()*)?
{
  fold-left($attributes, map {},
    function ($map as map(xs:string,item()*), $attribute as attribute(*)) as map(xs:string,item()*) {
      $map=>map:put(name($attribute), data($attribute))
    }
  )
}

Function: svg-style
declare function svg-style($properties as map(xs:string,item()*)) as map(xs:string,item()*)


svg-style()
Mapping of art properties to SVG style properties

Params
  • properties as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:svg-style(
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $properties :=
    if (exists($properties("stroke"))) then (
      $properties=>map:put("stroke", gradient:ref($properties("stroke")))
    ) else $properties
  let $properties :=
    if (exists($properties("fill"))) then (
      $properties=>map:put("fill", gradient:ref($properties("fill")))
    ) else $properties
  let $properties :=
    if (exists($properties("colour"))) then (
      if (empty($properties("fill"))) then (
        $properties=>map:put("fill", gradient:ref($properties("colour")))
      ) else $properties
    ) else $properties
  let $properties := 
    if (exists($properties("colour"))) then (
      if (empty($properties("stroke"))) then (
        $properties=>map:put("stroke", gradient:ref($properties("colour")))
      ) else $properties
    ) else $properties
  let $properties :=
    if (exists($properties("filter"))) then (
      $properties=>map:put("filter", effect:ref($properties("filter")))
    ) else $properties
  let $properties :=
    if (exists($properties("width"))) then (
      $properties=>map:put("stroke-width", $properties("width"))
    ) else $properties
  return $properties=>map:remove("colour")=>map:remove("width")
}

Function: dump-randomizer
declare function dump-randomizer($key as xs:string, $algorithm as map(xs:string, item()*))


dump-randomizer()
Dump the algorithm map as metadata suitable for SVG embedding.
distribution: Distribution to use, one of "constant", "uniform", "normal",
"skewed", "bernoulli", "flip", "zipf", "markov", "sums", "multimodal"
min: mimumum value (optional)
max: maximum value (optional)
pre-multiplier: multiplier before min/max (optional)
post-multipler: multiplier after min/max (optional)
cast: cast type (optional)
mean: mean of distribution (normal, skewed) (default=0)
std: standard deviation of distribution (normal, skewed) (default=mean)
skew: skew of distribution (skewed) (default=0)
p: probability (bernoulli, flip) (default=50)
sums: cumulative probability sums (zipf, markov)
alpha: alpha parameter (zipf) (needed if no sums) (default=0)
limit: number of sums (zipf) (needed if no sums) (default=1000)
start: index of starting symbol (markov) (default=uniform[1,dim])
dim: size of each dimension of Markov matrix (markov)
matrix: raw Markov matrix (used if sums not provided, not recommended)
sums: cumulative probability sums (zipf, markov)
Single element, sequence of doubles
matrix: raw Markov matrix (used if sums not provided, not recommended)
Single element, sequence of doubles
distributions: component distributions (multimodal)

Params
  • key as xs:string
  • algorithm as map(xs:string,item()*)
declare function this:dump-randomizer($key as xs:string, $algorithm as map(xs:string, item()*))
{
  if (exists($algorithm("describe"))) then (
    let $description := $algorithm("describe")($algorithm)
    return if (empty($description)) then ()
    else (
      <art:algorithm name="{$key}" distribution="{$algorithm('distribution')}">
      {
      $description
      }
      </art:algorithm>
    )
  ) else (
  <art:algorithm name="{$key}" distribution="{$algorithm('distribution')}">
  {
    this:dump-attr("min",$algorithm), this:dump-attr("max",$algorithm),
    this:dump-attr("pre-multiplier",$algorithm), this:dump-attr("post-multiplier",$algorithm), this:dump-attr("post-shift",$algorithm),
    this:dump-attr("cast",$algorithm),
    this:dump-attr("mean",$algorithm), this:dump-attr("std",$algorithm), this:dump-attr("skew",$algorithm),
    this:dump-attr("p",$algorithm),
    this:dump-attr("alpha",$algorithm), this:dump-attr("limit",$algorithm),
    this:dump-attr("dim",$algorithm), this:dump-attr("start",$algorithm),
    this:dump-elt("sums",$algorithm),
    this:dump-elt("keys",$algorithm),
    this:dump-elt("matrix",$algorithm),
    for $distribution at $i in $algorithm("distributions")
    return this:dump-randomizer(string($i), $distribution),
    if (exists($algorithm("selector"))) then (
      this:dump-randomizer("selector", $algorithm("selector"))
    ) else ()
  }
  </art:algorithm>
  )
}

Function: dump-randomizers
declare function dump-randomizers($randomizers as map(xs:string, item()*))

Params
  • randomizers as map(xs:string,item()*)
declare function this:dump-randomizers($randomizers as map(xs:string, item()*))
{
  let $keys := $randomizers=>map:keys()
  let $prefixes :=
    distinct-values(
      for $k in $keys return (
        if (contains($k, ".")) then substring-before($k,".") else ()
      )
    )
  return (
    for $prefix in $prefixes
    order by $prefix ascending
    return (
      element art:component {
        attribute name {$prefix},
        let $pkeys := $keys[starts-with(.,$prefix||".")]
        for $k in $pkeys
        order by $k ascending
        return (
          this:dump-randomizer($k, $randomizers($k))
        )
     }
    )
    ,
    for $k in $keys
    where not(contains($k,"."))
    order by $k ascending
    return (
      this:dump-randomizer($k, $randomizers($k))
    )
  )
}

Function: dump-parameters
declare function dump-parameters($parameters as map(xs:string, item()*))

Params
  • parameters as map(xs:string,item()*)
declare function this:dump-parameters($parameters as map(xs:string, item()*))
{
  this:dump-parameters($parameters, xs:QName("art:parameter"), ())
}

Function: dump-dynamic-parameters
declare function dump-dynamic-parameters($parameters as map(xs:string, item()*), $i as xs:integer?)

Params
  • parameters as map(xs:string,item()*)
  • i as xs:integer?
declare function this:dump-dynamic-parameters($parameters as map(xs:string, item()*), $i as xs:integer?)
{
  this:dump-parameters($parameters, xs:QName("art:dynamic-parameter"), $i)
}

Function: dump-dynamic-parameters
declare function dump-dynamic-parameters($parameters as map(xs:string, item()*))

Params
  • parameters as map(xs:string,item()*)
declare function this:dump-dynamic-parameters($parameters as map(xs:string, item()*))
{
  this:dump-parameters($parameters, xs:QName("art:dynamic-parameter"), ())
}

Function: dump-parameters
declare function dump-parameters($parameters as map(xs:string, item()*), $parameter-qname as xs:QName, $i as xs:integer?) as item()*

Params
  • parameters as map(xs:string,item()*)
  • parameter-qname as xs:QName
  • i as xs:integer?
Returns
  • item()*
declare function this:dump-parameters(
  $parameters as map(xs:string, item()*),
  $parameter-qname as xs:QName,
  $i as xs:integer?
) as item()*
{
  let $keys := $parameters=>map:keys()
  let $prefixes :=
    distinct-values(
      for $k in $keys return (
        if (contains($k, ".")) then substring-before($k,".") else ()
      )
    )
  return (
    for $prefix in $prefixes
    order by $prefix ascending
    return (
      element art:component {
        attribute name {$prefix},
        let $pkeys := $keys[starts-with(.,$prefix||".")]
        for $k in $pkeys
        let $k2 := string-join(($k, $i),".")
        let $v := $parameters($k)
        order by $k ascending
        return (
          if ($v instance of map(xs:string, item()*) and $v=>map:contains("distribution"))
          then (
            this:dump-randomizer($k2, $v)
          ) else (
            element {$parameter-qname} {
              attribute name {$k2},
             for $vi in $v return this:dump-value($vi)
            }
          )
        )
     }
   )
   ,
    for $k in $keys
    let $v := $parameters($k)
    where not(contains($k,".")) and ($k ne "description")
    order by $k ascending
    return (
      if ($v instance of map(xs:string, item()*) and $v=>map:contains("distribution")) then (
        this:dump-randomizer($k, $v)
      ) else (
        element art:parameter {
          attribute name {$k},
          for $vi in $v return this:dump-value($vi)
        }
      )
    )
  )
}

Function: dump-value
declare function dump-value($value as item()?) as xs:string

Params
  • value as item()?
Returns
  • xs:string
declare function this:dump-value($value as item()?) as xs:string
{
  typeswitch ($value)
  case empty-sequence() return ""
  case xs:QName return util:describe-qname($value)
  case map(*) return this:dump-map($value)
  case array(*) return this:dump-array($value)
  case function(*) return util:function-name($value)
  default return geom:quote($value)
}

Function: dump-map
declare function dump-map($map as map(*)) as xs:string

Params
  • map as map(*)
Returns
  • xs:string
declare function this:dump-map($map as map(*)) as xs:string
{
  if ($map("describe") instance of function(*))
  then $map("describe")($map)
  else (
    "{"||
    string-join((
      for $k in $map=>map:keys()
      let $v := $map($k)
      order by string($k) ascending
      return string($k)||": "||
        (if (count($v) > 1) 
         then "("||string-join(for $v1 in $v return this:dump-value($v1),",")||")"
         else this:dump-value($v))
    ),", ")
    ||"}"
  )
}

Function: dump-array
declare function dump-array($array as array(*))

Params
  • array as array(*)
declare function this:dump-array($array as array(*))
{
  "["||
  string-join((
    array:for-each($array, function($v as item()*) as xs:string {this:dump-value($v)})
  ),", ")
  ||"]"
}

Original Source Code

xquery version "3.1";
(:~
 : Module providing basic drawing functions: geo objects => SVG
 :
 : Copyright© Mary Holstege 2020-2023
 : CC-BY (https://creativecommons.org/licenses/by/4.0/)
 : @since December 2021
 : @custom:Status Stable, subject to additions
 :)
module namespace this="http://mathling.com/svg/draw"; 

declare namespace art="http://mathling.com/art";
declare namespace svg="http://www.w3.org/2000/svg";
declare namespace xsl="http://www.w3.org/1999/XSL/Transform";
declare namespace xhtml="http://www.w3.org/1999/xhtml";
declare namespace xlink="http://www.w3.org/1999/xlink";
declare namespace map="http://www.w3.org/2005/xpath-functions/map";
declare namespace array="http://www.w3.org/2005/xpath-functions/array";
declare namespace math="http://www.w3.org/2005/xpath-functions/math";

import module namespace core="http://mathling.com/art/core"
       at "../art/core.xqy";
import module namespace errors="http://mathling.com/core/errors"
       at "../core/errors.xqy";
import module namespace config="http://mathling.com/core/config"
       at "../core/config.xqy";
import module namespace util="http://mathling.com/core/utilities"
       at "../core/utilities.xqy";
import module namespace geom="http://mathling.com/geometric"
       at "../geo/euclidean.xqy";
import module namespace point="http://mathling.com/geometric/point"
       at "../geo/point.xqy";
import module namespace box="http://mathling.com/geometric/rectangle"
       at "../geo/rectangle.xqy";
import module namespace edge="http://mathling.com/geometric/edge"
       at "../geo/edge.xqy";
import module namespace gradient="http://mathling.com/svg/gradients"
       at "../svg/gradients.xqy";
import module namespace effect="http://mathling.com/svg/effects"
       at "../svg/effects.xqy";

(:~
 : rendering-map()
 : Mapping from object kind to SVG rendering function
 :)
declare function this:drawing-map() as map(xs:string,function(*)?)
{
  util:merge-into((
    $geom:DRAWING-MAP,
    map {
      "def": function-lookup(QName("http://mathling.com/type/defref", "draw"), 3),
      "ref": function-lookup(QName("http://mathling.com/type/defref", "draw"), 3),
      "text": function-lookup(QName("http://mathling.com/type/text", "draw"), 3),
      (: Hooks to drawing special functions; callbacks for object drawing :)
      "draw:draw": this:draw#3,
      "draw:svg-style": this:svg-style#1,
      "draw:dump-dynamic-parameters": this:dump-dynamic-parameters#1,
      (: Default no-projection :)
      "draw:project": function ($regions as map(xs:string,item()*)*) as map(xs:string,item()*)* {$regions}
    }
  ))
};

(:~
 : perspective-drawing-map()
 : Drawing map for solid objects.
 :
 : @param $canvas: the canvas we are projecting on to; should have a "depth" key
 :   If it doesn't we'll use the max of height/width
 : @param $p: perspective x parameter
 : @param $q: perspective y parameter
 : @param $r: perspective z parameter
 :)
declare function this:perspective-drawing-map(
  $canvas as map(xs:string,item()*),
  $p as xs:double,
  $q as xs:double,
  $r as xs:double
) as map(xs:string,function(*)?)
{
  map {
    (: Special call back :)
    "draw:project": geom:project(?, $canvas, $p, $q, $r)
  }
};

(:~
 : perspective-drawing-map()
 : Using default perspective parameters (0.5)
 :)
declare function this:perspective-drawing-map(
  $canvas as map(xs:string,item()*)
) as map(xs:string,function(*)?)
{
  this:perspective-drawing-map(
    $canvas,
    0.5, 0.5, 0.5
  )
};

(:~
 : perspective-drawing-map()
 : Using default perspective parameters (0.5), and medium resolution canvas size
 : (1600x1600x1600)
 :)
declare function this:perspective-drawing-map() as map(xs:string,function(*)?)
{
  this:perspective-drawing-map(
    core:canvas("medium")=>map:put("depth", core:width("medium")),
    0.5, 0.5, 0.5
  )
};

(:~
 : draw()
 : Render a set of objects as SVG.
 :
 : @param $items: the objects to render
 : @param $properties: styling properties to apply to the items (default=none)
 : @param $drawing: the map of drawing functions (default=this:drawing-map() merged
 :   with the default perspective map for solid objects)
 :)
declare function this:draw(
  $items as item()*,
  $properties as map(xs:string,item()*)?,
  $drawing as map(xs:string,function(*)?)
) as item()*
{
  let $properties := ($properties, map{})[1]
  for $item in $items return typeswitch ($item)
  case map(*) return (
    let $kind := $item("kind")
    return (
      if (empty($kind)) then (
        if (empty($item=>map:keys())) then ()
        else (
          let $draw := $item("draw")
          return (
            if (exists($draw) and ($draw instance of function(*)))
            then $draw($item, $properties, $drawing)
            else (
              let $uri := $item("uri")
              let $draw :=
                if (exists($uri)) then function-lookup(QName($uri, "draw"), 3) else ()
              return (
                if (exists($draw))
                then $draw($item, $properties, $drawing)
                else errors:error("ART-UNKNOWN", trace($item,"item"))
              )
            )
          )
        )
      ) else (
        let $draw := $drawing($kind)
        return (
          if (empty($draw)) then (
            let $draw := $item("draw")
            return (
              if (exists($draw) and ($draw instance of function(*)))
              then $draw($item, $properties, $drawing)
              else (
                let $uri := ($item("uri"),$config:TYPE-MAP($kind))[1]
                let $draw :=
                  if (exists($uri)) then function-lookup(QName($uri, "draw"), 3) else ()
                return (
                  if (exists($draw))
                  then $draw($item, $properties, $drawing)
                  else errors:error("ART-UNKNOWN", trace($item,"item"))
                )
              )
            )
          ) else (
            $draw($item, $properties, $drawing)
          )
        )
      )
    )
  )
  case element(art:component) return
    <svg:metadata>{$item}</svg:metadata>
  case element(art:dynamic-parameter) return
    <svg:metadata>{$item}</svg:metadata>
  case element(art:parameter) return
    <svg:metadata>{$item}</svg:metadata>
  case element(art:metadata) return
    <svg:metadata>{$item/*}</svg:metadata>
  case element() return (: SVG passthrough :)
    if (namespace-uri-from-QName(node-name($item))="http://mathling.com/art")
    then (
      comment { name($item) },
      this:draw(
        $item/node(),
        util:merge-into(this:as-properties($item/@*), $properties),
        $drawing
      ),
      comment { "/"||name($item) }
    ) else (
      element {node-name($item)} {
        this:merge-attributes(
          util:as-attributes($properties),
          $item/@*
        ), 
        this:draw($item/node(), $properties, $drawing)
      }
    )
  case text() return $item
  case document-node() return this:draw($item/node(), $properties, $drawing)
  default return errors:error("ART-UNKNOWN", trace($item,"item"))
};


declare function this:draw(
  $items as item()*,
  $properties as map(xs:string,item()*)?
) as item()*
{
  this:draw($items, $properties,
    util:merge-into(
      this:drawing-map(),
      let $resolution := ($properties("resolution"),"medium")[1]
      let $p := ($properties("perspective.p"), 0.5)[1]
      let $q := ($properties("perspective.q"), 0.5)[1]
      let $r := ($properties("perspective.r"), 0.5)[1]
      let $canvas := core:canvas($resolution)
      return this:perspective-drawing-map($canvas, $p, $q, $r)
    )
  )
};

declare function this:draw($items as item()*) as item()*
{
  this:draw($items, map{},
    util:merge-into(
      this:drawing-map(),
      this:perspective-drawing-map()
    )
  )
};

declare function this:perspective-draw(
  $items as item()*,
  $properties as map(xs:string,item()*)?,
  $canvas as map(xs:string,item()*)
) as item()*
{
  this:draw($items, $properties, 
    util:merge-into(
      this:drawing-map(),
      let $p := ($properties("perspective.p"), 0.5)[1]
      let $q := ($properties("perspective.q"), 0.5)[1]
      let $r := ($properties("perspective.r"), 0.5)[1]
      return this:perspective-drawing-map($canvas, $p, $q, $r)
    )
  )
};

(:~
 : colour-buckets()
 : Segregate the points in the matrix into colour buckets based on the 
 : colouring function, which should return a colour string or stop number.
 : Service function for draw-point-matrix()
 :
 : @param $matrix: the point array (either core/matrix or core/array)
 : @param $colouring: a function that maps a matrix value to a colour,
 :    the colour can be either a colour name or RGB string or a gradient
 :    stop number, an index into to the colours in $properties("gradient")
 : @param $exclude: return true() if we should skip this value
 :)
declare function this:colour-buckets(
  $matrix as map(*),
  $colouring as function(item()*) as item(),
  $exclude as function(item()*) as xs:boolean
) as map(*)
{
  let $kind := $matrix("kind")
  (: Breaks encapsulation: if we changed keys we'd be in trouble here :)
  let $rows := $matrix("rows")
  let $columns := $matrix("columns")
  let $get := 
    switch ($kind)
    case "matrix" return
      function-lookup(QName("http://mathling.com/core/matrix", "get-if-set"), 3)
    case "array" return
      function-lookup(QName("http://mathling.com/core/array", "get"), 3)
    case "imagearray" return
      function-lookup(QName("http://mathling.com/image/matrix", "get"), 3)
    case "point-matrix" return
      let $get := function-lookup(QName("http://mathling.com/geometric/matrix", "get-if-set"), 3)
      return
        function ($matrix as map(*), $row as xs:integer, $col as xs:integer) as item()* {
          $get($matrix, $col, $row)
        }
    default return errors:error("ART-BADPARMS", ("matrix.kind", $kind))
    
  return (
    map:merge(
      for $row in 1 to $rows
      return map:merge(
        for $col in 1 to $columns
        let $val := $get($matrix, $row, $col)
        return (
          if (empty($val) or $exclude($val)) then ()
          else (
            let $colour := $colouring($val)
            return map {$colour: point:point($col, $row)}
          )
        ),
        map {"duplicates": "combine"}
      )
      ,
      map {"duplicates": "combine"}
    )
    (:
    fold-left(1 to $rows, map{},
      function($buckets as map(*), $row as xs:integer) as map(*) {
        fold-left(1 to $columns, $buckets,
          function($buckets as map(*), $col as xs:integer) as map(*) {
            let $val := $get($matrix, $row, $col)
            return (
              if (empty($val) or $exclude($val)) then $buckets
              else (
                let $colour := $colouring($val)
                return $buckets=>map:put($colour, ($buckets($colour), point:point($col, $row)))
              )
            )
          }
        )
      }
    )
    :)
  )
};

declare function this:colour-buckets(
  $matrix as map(*),
  $colouring as function(item()*) as item()
) as map(*)
{
  this:colour-buckets($matrix, $colouring,
    function($val as item()*) as xs:boolean { false() }
  )
};

(:~
 : draw-point-matrix()
 : Efficient rendering of a point matrix, for when you have a matrix that
 : covers the whole canvas in a relatively small number of colours. This
 : converts each set of points in a single colour bucket into a path with
 : tiny lines at each point.
 :
 : @param $matrix: a point matrix, in either core/matrix, core/array, or 
 :    geometric/matrix format
 : @param $colouring: a function that maps a matrix value to a colour,
 :    the colour can be either a colour name or RGB string or a gradient
 :    stop number, an index into to the colours in $properties("gradient")
 : @param $properties: style properties plus 
 :    "gradient": the gradient to index colour values into (default=none)
 :    "width": how wide/long a line to make (default=1)
 :)
declare function this:draw-point-matrix(
  $matrix as map(*),
  $colouring as function(item()*) as item(),
  $properties as map(xs:string,item()*)
) as element()*
{
  this:draw-colour-buckets(
    this:colour-buckets($matrix, $colouring),
    $properties
  )
};

(:~
 : draw-colour-buckets()
 : Efficient rendering of a colour buckets; workhorse of draw-point-matrix()
 : If you use numeric colour keys, properties must include "colours" with
 : the colour palette
 :
 : @param $buckets: colour buckets, e.g. output from colour-buckets()
 : @param $properties: style properties plus 
 :    "gradient": the gradient to index colour values into (default=none)
 :    "width": how wide/long a line to make (default=1)
 :)
declare function this:draw-colour-buckets(
  $buckets as map(*),
  $properties as map(xs:string,item()*)
) as element()*
{
  let $gradient := $properties("gradient")
  let $colours := gradient:colours($gradient)
  return
  this:draw-colour-buckets(
    $buckets,
    function($key as item()) as node()* {
      let $colour := 
        if ($key instance of xs:numeric) 
        then $colours[$key cast as xs:integer]
        else string($key)
      return util:as-attributes(this:svg-style(map {"colour": $colour}))
    },
    $properties
  )
};

(:~
 : draw-colour-buckets()
 : Efficient rendering of a colour buckets; workhorse of draw-point-matrix()
 :
 : @param $buckets: colour buckets, e.g. output from colour-buckets()
 : @param $keyfn: mapping of key to attributes
 : @param $properties: style properties plus 
 :    "gradient": the gradient to index colour values into (default=none)
 :    "width": how wide/long a line to make (default=1)
 :)
declare function this:draw-colour-buckets(
  $buckets as map(*),
  $keyfn as function(item()) as node()*,
  $properties as map(xs:string,item()*)
) as element()*
{
  let $style-properties := util:exclude($properties,("gradient","width"))
  let $gradient := $properties("gradient")
  let $colours := gradient:colours($gradient)
  let $width := ($properties("width"),1)[1]
  for $key in $buckets=>map:keys()
  let $all-points := $buckets($key)
  let $chunks := 1 + count($all-points) idiv 10000
  for $chunk in 1 to $chunks
  let $points := $all-points[position() > ($chunk - 1)*10000 and position() <= $chunk*10000]
  let $path := 
    string-join(
      for $pt in $points return (
        edge:map-command('goto','absolute')||" "||
        point:px($pt)||" "||point:py($pt)||
        edge:map-command('line','absolute')||" "||
        (point:px($pt) + $width)||" "||(point:py($pt) + 0*$width)
      ),
      " "
    )
  where exists($points)
  return (
    <svg:path d="{$path}">{
      if (exists($style-properties("stroke-width"))) then ()
      else attribute stroke-width {$width}
      ,
      if (exists($style-properties("stroke-linecap"))) then ()
      else attribute stroke-linecap {"round"}
      ,
      util:as-attributes($style-properties),
      $keyfn($key)
    }</svg:path>
  )
};

(:~
 : to-CSS()
 : Render a property bundle as a CSS class style.
 :
 : @param $name: the style name
 : @param $properties: the properties
 :)
declare function this:to-CSS(
  $name as xs:string,
  $properties as map(xs:string,item()*)
)
{
  let $style-properties := this:svg-style($properties)
  return (
"."||$name||" {
"||
    string-join((
      map:for-each($style-properties,
        function ($key as xs:string, $value as item()*) as xs:string {
          $key||": "||string-join($value!string(.)," ")
        }
      )
    ),";
")||
"
}
"
  )
};

(:~
 : drawing()
 : Render a complete SVG. The components here should have already been
 : drawn (i.e. rendered to SVG)
 :
 : @param $canvas: defines size of canvas
 : @param $metadata: metadata elements
 : @param $defs: definitions
 : @param $styles: CSS styles
 : @param $content: body content
 :)
declare function this:drawing(
  $canvas as map(xs:string,item()*),
  $metadata as element()*,
  $defs as element()*,
  $styles as xs:string*,
  $content as item()* 
)
{
  <svg:svg version="1.1"
             viewBox="0 0 {box:width($canvas)} {box:height($canvas)}"
  >
    <svg:metadata>{$metadata}</svg:metadata>
    <svg:defs>{$defs}</svg:defs>
    <svg:style type="text/css">{$styles}</svg:style>
    <svg:g>{$content}</svg:g>
  </svg:svg>
};

declare function this:merge-attributes(
  $old as attribute(*)*,
  $new as attribute(*)*
) as attribute(*)*
{
  let $names := $new!node-name(.)
  return (
    $new, $old[not(node-name(.) = $names)]
  )
};

(:~
 : as-properties()
 : Convert attributes to a series of properties, for rendering.
 : Note: no validity checking here; assumes we have pre-sanitized.
 :)
declare function this:as-properties(
  $attributes as attribute(*)*
) as map(xs:string,item()*)?
{
  fold-left($attributes, map {},
    function ($map as map(xs:string,item()*), $attribute as attribute(*)) as map(xs:string,item()*) {
      $map=>map:put(name($attribute), data($attribute))
    }
  )
};


(:~
 : svg-style()
 : Mapping of art properties to SVG style properties
 :)
declare function this:svg-style(
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $properties :=
    if (exists($properties("stroke"))) then (
      $properties=>map:put("stroke", gradient:ref($properties("stroke")))
    ) else $properties
  let $properties :=
    if (exists($properties("fill"))) then (
      $properties=>map:put("fill", gradient:ref($properties("fill")))
    ) else $properties
  let $properties :=
    if (exists($properties("colour"))) then (
      if (empty($properties("fill"))) then (
        $properties=>map:put("fill", gradient:ref($properties("colour")))
      ) else $properties
    ) else $properties
  let $properties := 
    if (exists($properties("colour"))) then (
      if (empty($properties("stroke"))) then (
        $properties=>map:put("stroke", gradient:ref($properties("colour")))
      ) else $properties
    ) else $properties
  let $properties :=
    if (exists($properties("filter"))) then (
      $properties=>map:put("filter", effect:ref($properties("filter")))
    ) else $properties
  let $properties :=
    if (exists($properties("width"))) then (
      $properties=>map:put("stroke-width", $properties("width"))
    ) else $properties
  return $properties=>map:remove("colour")=>map:remove("width")
};

declare %private function this:dump-attr($k as xs:string, $algorithm as map(xs:string, item()*))
{
  if ($algorithm=>map:contains($k)) then (
    let $v := $algorithm($k)
    return
      attribute {$k} {
        typeswitch ($v)
         case function(*)+ return $v!util:function-name(.)
         case xs:QName+ return $v!util:describe-qname(.)
         default return $v
      }
  ) else ()
};

declare %private function this:dump-elt($k as xs:string, $algorithm as map(xs:string,item()*))
{
  if ($algorithm=>map:contains($k)) then (
    let $v := $algorithm($k)
    return
      element {$k} {
        typeswitch ($v)
         case function(*)+ return $v!util:function-name(.)
         case xs:QName+ return $v!util:describe-qname(.)
         default return $v
      }
  ) else ()
};

(:~
 : dump-randomizer()
 : Dump the algorithm map as metadata suitable for SVG embedding.
 :   distribution: Distribution to use, one of "constant", "uniform", "normal",
 :     "skewed", "bernoulli", "flip", "zipf", "markov", "sums", "multimodal"
 :   min: mimumum value (optional)
 :   max: maximum value (optional)
 :   pre-multiplier: multiplier before min/max (optional)
 :   post-multipler: multiplier after min/max (optional)
 :   cast: cast type (optional)
 :   mean: mean of distribution (normal, skewed) (default=0)
 :   std: standard deviation of distribution (normal, skewed) (default=mean)
 :   skew: skew of distribution (skewed) (default=0)
 :   p: probability (bernoulli, flip) (default=50)
 :   sums: cumulative probability sums (zipf, markov)
 :   alpha: alpha parameter (zipf) (needed if no sums) (default=0)
 :   limit: number of sums (zipf) (needed if no sums) (default=1000)
 :   start: index of starting symbol (markov) (default=uniform[1,dim])
 :   dim: size of each dimension of Markov matrix (markov)
 :   matrix: raw Markov matrix (used if sums not provided, not recommended)
 :   sums: cumulative probability sums (zipf, markov)
 :     Single element, sequence of doubles
 :   matrix: raw Markov matrix (used if sums not provided, not recommended)
 :     Single element, sequence of doubles
 :   distributions: component distributions (multimodal)
 :)
declare function this:dump-randomizer($key as xs:string, $algorithm as map(xs:string, item()*))
{
  if (exists($algorithm("describe"))) then (
    let $description := $algorithm("describe")($algorithm)
    return if (empty($description)) then ()
    else (
      <art:algorithm name="{$key}" distribution="{$algorithm('distribution')}">
      {
      $description
      }
      </art:algorithm>
    )
  ) else (
  <art:algorithm name="{$key}" distribution="{$algorithm('distribution')}">
  {
    this:dump-attr("min",$algorithm), this:dump-attr("max",$algorithm),
    this:dump-attr("pre-multiplier",$algorithm), this:dump-attr("post-multiplier",$algorithm), this:dump-attr("post-shift",$algorithm),
    this:dump-attr("cast",$algorithm),
    this:dump-attr("mean",$algorithm), this:dump-attr("std",$algorithm), this:dump-attr("skew",$algorithm),
    this:dump-attr("p",$algorithm),
    this:dump-attr("alpha",$algorithm), this:dump-attr("limit",$algorithm),
    this:dump-attr("dim",$algorithm), this:dump-attr("start",$algorithm),
    this:dump-elt("sums",$algorithm),
    this:dump-elt("keys",$algorithm),
    this:dump-elt("matrix",$algorithm),
    for $distribution at $i in $algorithm("distributions")
    return this:dump-randomizer(string($i), $distribution),
    if (exists($algorithm("selector"))) then (
      this:dump-randomizer("selector", $algorithm("selector"))
    ) else ()
  }
  </art:algorithm>
  )
};

declare function this:dump-randomizers($randomizers as map(xs:string, item()*))
{
  let $keys := $randomizers=>map:keys()
  let $prefixes :=
    distinct-values(
      for $k in $keys return (
        if (contains($k, ".")) then substring-before($k,".") else ()
      )
    )
  return (
    for $prefix in $prefixes
    order by $prefix ascending
    return (
      element art:component {
        attribute name {$prefix},
        let $pkeys := $keys[starts-with(.,$prefix||".")]
        for $k in $pkeys
        order by $k ascending
        return (
          this:dump-randomizer($k, $randomizers($k))
        )
     }
    )
    ,
    for $k in $keys
    where not(contains($k,"."))
    order by $k ascending
    return (
      this:dump-randomizer($k, $randomizers($k))
    )
  )
};

declare function this:dump-parameters($parameters as map(xs:string, item()*))
{
  this:dump-parameters($parameters, xs:QName("art:parameter"), ())
};

declare function this:dump-dynamic-parameters($parameters as map(xs:string, item()*), $i as xs:integer?)
{
  this:dump-parameters($parameters, xs:QName("art:dynamic-parameter"), $i)
};

declare function this:dump-dynamic-parameters($parameters as map(xs:string, item()*))
{
  this:dump-parameters($parameters, xs:QName("art:dynamic-parameter"), ())
};

declare function this:dump-parameters(
  $parameters as map(xs:string, item()*),
  $parameter-qname as xs:QName,
  $i as xs:integer?
) as item()*
{
  let $keys := $parameters=>map:keys()
  let $prefixes :=
    distinct-values(
      for $k in $keys return (
        if (contains($k, ".")) then substring-before($k,".") else ()
      )
    )
  return (
    for $prefix in $prefixes
    order by $prefix ascending
    return (
      element art:component {
        attribute name {$prefix},
        let $pkeys := $keys[starts-with(.,$prefix||".")]
        for $k in $pkeys
        let $k2 := string-join(($k, $i),".")
        let $v := $parameters($k)
        order by $k ascending
        return (
          if ($v instance of map(xs:string, item()*) and $v=>map:contains("distribution"))
          then (
            this:dump-randomizer($k2, $v)
          ) else (
            element {$parameter-qname} {
              attribute name {$k2},
             for $vi in $v return this:dump-value($vi)
            }
          )
        )
     }
   )
   ,
    for $k in $keys
    let $v := $parameters($k)
    where not(contains($k,".")) and ($k ne "description")
    order by $k ascending
    return (
      if ($v instance of map(xs:string, item()*) and $v=>map:contains("distribution")) then (
        this:dump-randomizer($k, $v)
      ) else (
        element art:parameter {
          attribute name {$k},
          for $vi in $v return this:dump-value($vi)
        }
      )
    )
  )
};

declare function this:dump-value($value as item()?) as xs:string
{
  typeswitch ($value)
  case empty-sequence() return ""
  case xs:QName return util:describe-qname($value)
  case map(*) return this:dump-map($value)
  case array(*) return this:dump-array($value)
  case function(*) return util:function-name($value)
  default return geom:quote($value)
};

declare function this:dump-map($map as map(*)) as xs:string
{
  if ($map("describe") instance of function(*))
  then $map("describe")($map)
  else (
    "{"||
    string-join((
      for $k in $map=>map:keys()
      let $v := $map($k)
      order by string($k) ascending
      return string($k)||": "||
        (if (count($v) > 1) 
         then "("||string-join(for $v1 in $v return this:dump-value($v1),",")||")"
         else this:dump-value($v))
    ),", ")
    ||"}"
  )
};

declare function this:dump-array($array as array(*))
{
  "["||
  string-join((
    array:for-each($array, function($v as item()*) as xs:string {this:dump-value($v)})
  ),", ")
  ||"]"
};