http://mathling.com/geometric/path  library module

http://mathling.com/geometric/path


Path and polygon (closed path) objects

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

April 2021
Status: Incomplete, subject to refactoring

Imports

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/geometric/box
import module namespace box="http://mathling.com/geometric/box"
       at "../geo/box.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"

Variables

Variable: $RESERVED as xs:string*

Functions

Function: path
declare function path($edges as map(xs:string,item()*)*, $properties as map(xs:string,item()*)) as map(xs:string,item()*)


path()
Construct a path from edges. If the head of the edges parameter is a
point, will treat the whole as a sequence of points.

Params
  • edges as map(xs:string,item()*)*: edges of the path, in order. Edges could be of any kind.
  • properties as map(xs:string,item()*): additional properties (default = none)
Returns
  • map(xs:string,item()*)
declare function this:path(
  $edges as map(xs:string,item()*)*,
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  if (empty($edges)) then (
    util:merge-into($properties,
      map {
        "kind": "path",
        "edges": ()
      }
    )
  ) else if ($edges[1]("kind")="point") then (
    (: Creating path from sequence of points :)
    (: An error, which we try to recover from :)
    this:path(edge:to-edges($edges), $properties)
  ) else (
    util:merge-into($properties,
      map {
        "kind": "path",
        "edges": $edges
      }
    )
  )
}

Function: path
declare function path($edges as map(xs:string,item()*)*) as map(xs:string,item()*)

Params
  • edges as map(xs:string,item()*)*
Returns
  • map(xs:string,item()*)
declare function this:path(
  $edges as map(xs:string,item()*)*
) as map(xs:string,item()*)
{
  if (empty($edges)) then (
    map {
      "kind": "path",
      "edges": ()
    }
  ) else if ($edges[1]("kind")="point") then (
    (: Creating path from sequence of points :)
    (: An error, which we try to recover from :)
    this:path(edge:to-edges($edges))
  ) else (
    map {
      "kind": "path",
      "edges": $edges
    }
  )
}

Function: kind
declare function kind($region as map(xs:string,item()*)) as xs:string

Params
  • region as map(xs:string,item()*)
Returns
  • xs:string
declare function this:kind($region as map(xs:string,item()*)) as xs:string
{
  $region("kind")
}

Function: edges
declare function edges($path as map(xs:string,item()*)) as map(xs:string,item()*)*

Params
  • path as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:edges(
  $path as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  if ($path("kind")=("edge","quad","cubic","arc","ellipse-arc"))
  then $path
  else $path("edges")
}

Function: points
declare function points($region as map(xs:string,item()*)) as map(xs:string,item()*)*

Params
  • region as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:points(
  $region as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  switch (this:kind($region))
  case "path" return (
    for $edge in $region=>this:edges()
    return $edge=>edge:start()=>point:as-dimension(2)
    ,
    ($region=>this:edges())[last()]=>edge:end()=>point:as-dimension(2)
  )
  case "polygon" return (
    for $edge in $region=>this:edges()
    return $edge=>edge:start()=>point:as-dimension(2)
    ,
    ($region=>this:edges())[last()]=>edge:end()=>point:as-dimension(2)
  )
  default return errors:error("GEOM-BADREGION", ($region, "points"))
}

Function: vertices
declare function vertices($region as map(xs:string,item()*)) as map(xs:string,item()*)*

Params
  • region as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:vertices(
  $region as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  switch (this:kind($region))
  case "path" return (
    for $edge in $region=>this:edges()
    return $edge=>edge:start()
    ,
    ($region=>this:edges())[last()]=>edge:end()
  )
  case "polygon" return (
    for $edge in $region=>this:edges()
    return $edge=>edge:start()
    ,
    ($region=>this:edges())[last()]=>edge:end() (: Includes closing vertex=starting vertex :)
  )
  default return errors:error("GEOM-BADREGION", ($region, "vertices"))
}

Function: vertices
declare function vertices($region as map(xs:string,item()*), $close as xs:boolean) as map(xs:string,item()*)*

Params
  • region as map(xs:string,item()*)
  • close as xs:boolean
Returns
  • map(xs:string,item()*)*
declare function this:vertices(
  $region as map(xs:string,item()*),
  $close as xs:boolean
) as map(xs:string,item()*)*
{
  switch (this:kind($region))
  case "path" return (
    for $edge in $region=>this:edges()
    return $edge=>edge:start()
    ,
    ($region=>this:edges())[last()]=>edge:end()
  )
  case "polygon" return (
    for $edge in $region=>this:edges()
    return $edge=>edge:start()
    ,
    if ($close) then ($region=>this:edges())[last()]=>edge:end() else ()
  )
  default return errors:error("GEOM-BADREGION", ($region, "vertices"))
}

Function: as-polygon
declare function as-polygon($region as map(xs:string,item()*)) as map(xs:string,item()*)


as-polygon()
Turn a path into a polygon.

Params
  • region as map(xs:string,item()*): the path
Returns
  • map(xs:string,item()*): unnormalized polygon
declare function this:as-polygon(
  $region as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  switch (this:kind($region))
  case "path" return this:polygon($region=>this:edges(), $region=>this:property-map())
  case "polygon" return $region
  default return errors:error("GEOM-BADREGION", ($region, "as-polygon"))
}

Function: polygon
declare function polygon($edges as map(xs:string,item()*)*) as map(xs:string,item()*)


polygon()
Construct a polygon from edges. If the head of the edges parameter is a
point, will treat the whole as a sequence of points.

Params
  • edges as map(xs:string,item()*)*: edges of the polygon
Returns
  • map(xs:string,item()*): unnormalized polygon
declare function this:polygon(
  $edges as map(xs:string,item()*)*
) as map(xs:string,item()*)
{
  this:polygon($edges, map {})
}

Function: polygon
declare function polygon($edges as map(xs:string,item()*)*, $properties as map(xs:string,item()*)) as map(xs:string,item()*)


polygon()
Construct a polygon from edges. If the head of the edges parameter is a
point, will treat the whole as a sequence of points.

Params
  • edges as map(xs:string,item()*)*: edges of the polygon
  • properties as map(xs:string,item()*): additional properties
Returns
  • map(xs:string,item()*): unnormalized polygon
declare function this:polygon(
  $edges as map(xs:string,item()*)*,
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  if (empty($edges)) then (
    util:merge-into($properties,
      map {
        "kind": "polygon",
        "edges": ()
      }
    )
  ) else if ($edges[1]("kind")="point") then (
    (: Creating polygon from sequence of points :)
    (: An error, which we try to recover from :)
    this:polygon(
      (
        edge:to-edges($edges),
        if (not(point:same($edges[1],$edges[last()])))
        then edge:edge($edges[last()], $edges[1])
        else ()
      )
      ,
      $properties
    )
  ) else (
    util:merge-into($properties,
      map {
        "kind": "polygon",
        "edges": (
          $edges, (
            if (not(point:same(edge:start($edges[1]),edge:end($edges[last()])))) then (
              edge:edge(edge:end($edges[last()]), edge:start($edges[1]))
            ) else ()
          )
        )
      }
    )
  )
}

Function: rectangle
declare function rectangle($min-pt as map(xs:string,item()*), $max-pt as map(xs:string,item()*)) as map(xs:string,item()*)


rectangle()
Construct an axis-aligned rectangle.

Params
  • min-pt as map(xs:string,item()*): minimum point
  • max-pt as map(xs:string,item()*): maximum point
Returns
  • map(xs:string,item()*): normalized axis-aligned rectangle
declare function this:rectangle($min-pt as map(xs:string,item()*), $max-pt as map(xs:string,item()*)) as map(xs:string,item()*)
{
  let $z := avg((point:pz($min-pt),point:pz($max-pt)))
  let $minmaxpt := point:point(point:px($min-pt), point:py($max-pt), $z)
  let $maxminpt := point:point(point:px($max-pt), point:py($min-pt), $z)
  return
  this:polygon((
    edge:edge($min-pt, $maxminpt),
    edge:edge($maxminpt, $max-pt),
    edge:edge($max-pt, $minmaxpt),
    edge:edge($minmaxpt, $min-pt)
  ))
}

Function: triangle
declare function triangle($a as map(xs:string,item()*), $b as map(xs:string,item()*), $c as map(xs:string,item()*)) as map(xs:string,item()*)


triangle()
Construct a triangle given three points.

Params
  • a as map(xs:string,item()*): first point
  • b as map(xs:string,item()*): second point
  • c as map(xs:string,item()*): third point
Returns
  • map(xs:string,item()*): unnormalized triangle
declare function this:triangle(
  $a as map(xs:string,item()*),
  $b as map(xs:string,item()*),
  $c as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  this:polygon((
    edge:edge($a, $b),
    edge:edge($b, $c),
    edge:edge($c, $a)
  ))
}

Function: triangle
declare function triangle($a as map(xs:string,item()*), $b as map(xs:string,item()*), $c as map(xs:string,item()*), $properties as map(xs:string,item()*)) as map(xs:string,item()*)


triangle()
Construct a triangle given three points.

Params
  • a as map(xs:string,item()*): first point
  • b as map(xs:string,item()*): second point
  • c as map(xs:string,item()*): third point
  • properties as map(xs:string,item()*): additional properties
Returns
  • map(xs:string,item()*): unnormalized triangle
declare function this:triangle(
  $a as map(xs:string,item()*),
  $b as map(xs:string,item()*),
  $c as map(xs:string,item()*),
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  this:polygon((
    edge:edge($a, $b),
    edge:edge($b, $c),
    edge:edge($c, $a)
  ), $properties)
}

Function: quadrangle
declare function quadrangle($a as map(xs:string,item()*), $b as map(xs:string,item()*), $c as map(xs:string,item()*), $d as map(xs:string,item()*)) as map(xs:string,item()*)


quadrangle()
Construct a quadrangle given four points.

Params
  • a as map(xs:string,item()*): first point
  • b as map(xs:string,item()*): second point
  • c as map(xs:string,item()*): third point
  • d as map(xs:string,item()*): fourth point
Returns
  • map(xs:string,item()*): unnormalized quadrangle
declare function this:quadrangle(
  $a as map(xs:string,item()*),
  $b as map(xs:string,item()*),
  $c as map(xs:string,item()*),
  $d as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  this:polygon((
    edge:edge($a, $b),
    edge:edge($b, $c),
    edge:edge($c, $d),
    edge:edge($d, $a)
  ))
}

Function: quadrangle
declare function quadrangle($a as map(xs:string,item()*), $b as map(xs:string,item()*), $c as map(xs:string,item()*), $d as map(xs:string,item()*), $properties as map(xs:string,item()*)) as map(xs:string,item()*)


quadrangle()
Construct a quadrangle given four points.

Params
  • a as map(xs:string,item()*): first point
  • b as map(xs:string,item()*): second point
  • c as map(xs:string,item()*): third point
  • d as map(xs:string,item()*): fourth point
  • properties as map(xs:string,item()*): additional properties
Returns
  • map(xs:string,item()*): quadrangle
declare function this:quadrangle(
  $a as map(xs:string,item()*),
  $b as map(xs:string,item()*),
  $c as map(xs:string,item()*),
  $d as map(xs:string,item()*),
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  this:polygon((
    edge:edge($a, $b),
    edge:edge($b, $c),
    edge:edge($c, $d),
    edge:edge($d, $a)
  ), $properties)
}

Function: ngon
declare function ngon($sides as xs:integer, $center as map(xs:string,item()*), $radius as xs:double) as map(xs:string,item()*)


ngon()
Construct a regular n-sided polygon.

Params
  • sides as xs:integer: how many sides to the polygon
  • center as map(xs:string,item()*): center of polygon
  • radius as xs:double: distance from center to vertices
Returns
  • map(xs:string,item()*): normalized n-sided regular polygon
declare function this:ngon(
  $sides as xs:integer,
  $center as map(xs:string,item()*),
  $radius as xs:double
) as map(xs:string,item()*)
{
  this:polygon(
    (
      for $i in 1 to $sides
      let $angle := ($i - 1)*360 div $sides
      return (
        $center=>point:destination($angle, $radius)
      )
    )=>edge:to-edges()
  )
}

Function: star
declare function star($sides as xs:integer, $center as map(xs:string,item()*), $radius as xs:double) as map(xs:string,item()*)


star()
Construct a regular n-sided polygon drawn as a star. If there are an odd number
of sides this is a single polygon; if even it is essentially two polygons,
joined with a skip edge.

Params
  • sides as xs:integer: how many sides to the polygon
  • center as map(xs:string,item()*): center of polygon
  • radius as xs:double: distance from center to vertices
Returns
  • map(xs:string,item()*): normalized polygon
declare function this:star(
  $sides as xs:integer,
  $center as map(xs:string,item()*),
  $radius as xs:double
) as map(xs:string,item()*)
{
  let $points := (
    for $i in 1 to $sides
    let $angle := ($i - 1)*360 div $sides
    return (
      $center=>point:destination($angle, $radius)
    )
  )
  let $skip := (if ($sides mod 2 = 0) then ($sides - 1) idiv 2 else $sides idiv 2)
  return (
    if ($sides mod 2 = 1 or $skip mod 2 = 1) then (
      this:polygon(
        edge:to-edges(
          let $positions := (
            fold-left(1 to $sides, 1,
              function($positions as xs:integer*, $i as xs:integer) as xs:integer* {
                $positions,
                util:modix($positions[last()] + $skip, $sides)
              }
            )
          )
          for $pos in $positions return $points[$pos]
        )
      )
    ) else (
      this:polygon(
        (
          edge:to-edges(
            let $positions := (
              fold-left(1 to $sides idiv 2, 1,
                function($positions as xs:integer*, $i as xs:integer) as xs:integer* {
                  $positions,
                  util:modix($positions[last()] + $skip, $sides)
                }
              )
            )
            for $pos in $positions return $points[$pos]
          ),
          edge:skip($points[1], $points[2]),
          edge:to-edges(
            let $positions := (
              fold-left(1 to $sides idiv 2, 2,
                function($positions as xs:integer*, $i as xs:integer) as xs:integer* {
                  $positions,
                  util:modix($positions[last()] + $skip, $sides)
                }
              )
            )
            for $pos in $positions return $points[$pos]
          ),
          edge:skip($points[2], $points[1])
        )
      )
    )
  )
}

Function: normalize
declare function normalize($polygon as map(xs:string,item()*)) as map(xs:string,item()*)


normalize()
Normalize a polygon so the vertices are ordered counterclockwise.

Params
  • polygon as map(xs:string,item()*): polygon to normalize
Returns
  • map(xs:string,item()*): normalized polygon
declare function this:normalize(
  $polygon as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  if (this:kind($polygon)!="polygon")
  then errors:error("GEOM-BADREGION", ($polygon, "normalize"))
  else (),
  let $points := $polygon=>this:vertices(false())=>point:counterclockwise()
  return (
    this:polygon(edge:to-edges($points), this:property-map($polygon))
  )
}

Function: edge-ts
declare function edge-ts($path as map(xs:string, item()*)) as xs:double*


edge-ts()
Accessor for the edge t values: fractions of the path length that
edge edge represents. Used for optimized path interpolations.

Params
  • path as map(xs:string,item()*): the path
Returns
  • xs:double*
declare function this:edge-ts($path as map(xs:string, item()*)) as xs:double*
{
  if ($path=>map:contains("edge-ts")) then $path("edge-ts")
  else this:compute-edge-ts($path) (: Warning: SLOW! :)
}

Function: with-edge-ts
declare function with-edge-ts($path as map(xs:string, item()*)) as map(xs:string, item()*)


with-edge-ts()
Return the path with edge t values computed.
Recommended for optimized path interpolations.

Params
  • path as map(xs:string,item()*): the path
Returns
  • map(xs:string,item()*)
declare function this:with-edge-ts($path as map(xs:string, item()*)) as map(xs:string, item()*)
{
  $path=>
    map:put("edge-ts", this:compute-edge-ts($path))
}

Function: edge-intersections
declare function edge-intersections($this as map(xs:string,item()*), $line as map(xs:string,item()*)) as map(xs:string,item()*)*


edge-intersections()
Points of intersection, if any between path and edge

Params
  • this as map(xs:string,item()*)
  • line as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:edge-intersections(
  $this as map(xs:string,item()*),
  $line as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  for $edge in this:edges($this) return edge:intersection($edge, $line)
}

Function: property-map
declare function property-map($region as map(xs:string,item()*)) as map(xs:string,item()*)


property-map()
Return the annotation properties of the region as a map. Check whether this
is actually a path or polygon.

Params
  • region as map(xs:string,item()*): the region
Returns
  • map(xs:string,item()*)
declare function this:property-map(
  $region as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  switch (this:kind($region))
  case "path" return util:exclude($region, $this:RESERVED)
  case "polygon" return util:exclude($region, $this:RESERVED)
  default return map {}
}

Function: properties
declare function properties($region as map(xs:string,item()*)) as xs:string*


properties()
Return the names of the annotation properties of the region.
Check whether this is actually a path or polygon.

Params
  • region as map(xs:string,item()*): the region
Returns
  • xs:string*
declare function this:properties(
  $region as map(xs:string,item()*)
) as xs:string*
{
  switch (this:kind($region))
  case "path" return ($region=>map:keys())[not(. = $this:RESERVED)]
  case "polygon" return ($region=>map:keys())[not(. = $this:RESERVED)]
  default return map {}
}

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


with-properties()
Annotate the region with some new properties and return the new region.
Will not touch any of the core properties. Will override existing properties
with the same keys but leave properties with different keys in place.
Raises an error if this is not actually a path or polygon.

Params
  • regions as map(xs:string,item()*)*
  • properties as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:with-properties(
  $regions as map(xs:string,item()*)*,
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  for $region in $regions return
  switch (this:kind($region))
  case "path" return
    util:merge-into($region, util:exclude($properties,$this:RESERVED))
  case "polygon" return
    util:merge-into($region, util:exclude($properties,$this:RESERVED))
  default return errors:error("GEOM-BADREGION", ($region, "with-properties"))
}

Function: snap
declare function snap($regions as map(xs:string,item()*)*) as map(xs:string,item()*)*


snap()
Snap the coordinates of the points, returning the region with snapped
(i.e. integer) points coordinates.

Params
  • regions as map(xs:string,item()*)*: the regions
Returns
  • map(xs:string,item()*)*
declare function this:snap(
  $regions as map(xs:string,item()*)*
) as map(xs:string,item()*)*
{
  for $region in $regions return switch(this:kind($region))
  case "path" return
    this:path(edge:snap(this:edges($region)), $region)
  case "polygon" return
    this:polygon(edge:snap(this:edges($region)), $region)
  default return $region
}

Function: decimal
declare function decimal($regions as map(xs:string,item()*)*, $digits as xs:integer) as map(xs:string,item()*)*


decimal()
Perform decimal rounding on all the point coordinates (see util:decimal).

Params
  • regions as map(xs:string,item()*)*: the regions to round
  • digits as xs:integer: how many digits after the decimal point to keep
Returns
  • map(xs:string,item()*)*
declare function this:decimal(
  $regions as map(xs:string,item()*)*,
  $digits as xs:integer
) as map(xs:string,item()*)*
{
  for $region in $regions return switch(this:kind($region))
  case "path" return
    this:path(edge:decimal(this:edges($region), $digits), $region)
  case "polygon" return
    this:polygon(edge:decimal(this:edges($region), $digits), $region)
  default return $region
}

Function: quote
declare function quote($paths as map(xs:string,item()*)*) as xs:string


quote()
Return a string value for the path, suitable for debugging.

Params
  • paths as map(xs:string,item()*)*: the path sequence to quote
Returns
  • xs:string
declare function this:quote(
  $paths as map(xs:string,item()*)*
) as xs:string
{
  string-join(
    for $path in $paths return switch(this:kind($path))
    case "path" return
      "("||edge:quote($path=>this:edges())||")"
    case "polygon" return
      "("||edge:quote($path=>this:edges())||")"
    default return errors:quote($path)
    ,
    " "
  )
}

Function: same
declare function same($this as map(xs:string,item()*), $other as map(xs:string,item()*)) as xs:boolean


same()
Equality comparison for paths, ignoring annotation properties.
Return true() if they have equal coordinates.

Params
  • this as map(xs:string,item()*): one path or polygon
  • other as map(xs:string,item()*): the path or polygon to compare it to
Returns
  • xs:boolean
declare function this:same(
  $this as map(xs:string,item()*),
  $other as map(xs:string,item()*)
) as xs:boolean
{
  let $this-kind := this:kind($this)
  let $other-kind := this:kind($other)
  return
  (
    (
      (($this-kind="polygon") and ($other-kind="polygon")) or
      (($this-kind="path") and ($other-kind="path"))
    ) and
    (
      let $this-edges := $this=>this:edges()
      let $other-edges := $other=>this:edges()
      return
        every $i in 1 to count($this-edges)
        satisfies edge:same($this-edges[$i], $other-edges[$i])
    )
  ) or (
    $this-kind=$other-kind and deep-equal($this,$other)
  )
}

Function: mutate
declare function mutate($regions as map(xs:string,item()*)*, $mutate as function(item()) as map(xs:string,item()*)) as map(xs:string,item()*)*


mutate()
Run a function over a sequence of regions to produce a new sequence of
regions. The function maps points to points.

Params
  • regions as map(xs:string,item()*)*: input sequence of regions
  • mutate as function(item())asmap(xs:string,item()*): function that takes a point as an argument and returns a new point
Returns
  • map(xs:string,item()*)*
declare function this:mutate(
  $regions as map(xs:string,item()*)*,
  $mutate as function(item()) as map(xs:string,item()*) (: point to point :)
) as map(xs:string,item()*)*
{
  for $region in $regions return switch(this:kind($region))
  case "path" return
    let $edges := edge:mutate($region=>this:edges(), $mutate)
    return this:path($edges, $region)
  case "polygon" return
    let $edges := edge:mutate($region=>this:edges(), $mutate)
    return this:polygon($edges, $region)
  default return $region
}

Function: reverse
declare function reverse($path as map(xs:string,item()*)) as map(xs:string,item()*)


reverse()
Reverse the direction of the path.

Params
  • path as map(xs:string,item()*): the path to reverse
Returns
  • map(xs:string,item()*)
declare function this:reverse(
  $path as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  switch(this:kind($path))
  case "path" return
    this:path(
      for $edge in fn:reverse(this:edges($path)) return edge:reverse($edge),
      $path
    )
  case "polygon" return
    this:polygon(
      for $edge in fn:reverse(this:edges($path)) return edge:reverse($edge),
      $path
    )
  default return $path
}

Function: start
declare function start($path as map(xs:string,item()*)) as map(xs:string,item()*)


start()
First point of path.

Params
  • path as map(xs:string,item()*): the path
Returns
  • map(xs:string,item()*)
declare function this:start($path as map(xs:string,item()*)) as map(xs:string,item()*)
{
  this:edges($path)[1]=>edge:start()
}

Function: end
declare function end($path as map(xs:string,item()*)) as map(xs:string,item()*)


end()
Last point of path.

Params
  • path as map(xs:string,item()*): the path
Returns
  • map(xs:string,item()*)
declare function this:end($path as map(xs:string,item()*)) as map(xs:string,item()*)
{
  this:edges($path)[last()]=>edge:end()
}

Function: area
declare function area($polygon as map(xs:string,item()*)) as xs:double


area()
Area of polygon. Assumes vertices are ordered counterclockwise.
Will fail for certain twisty polygons.

Params
  • polygon as map(xs:string,item()*)
Returns
  • xs:double
declare function this:area($polygon as map(xs:string,item()*)) as xs:double
{
  switch(this:kind($polygon))
  case "polygon" return (
    let $points := this:vertices($polygon)
    let $n := count($points) - 1
    return (
      switch ($n)
      case 1 return 0
      case 2 return 0
      case 3 return (
        (: Heron's formula :)
        let $a := point:distance($points[1], $points[2])
        let $b := point:distance($points[2], $points[3])
        let $c := point:distance($points[3], $points[1])
        let $s := sum(($a, $b, $c)) div 2
        return math:sqrt($s * ($s - $a) * ($s - $b) * ($s - $c))
      )
      default return (
        abs( (: Sign flips for canvas geometry :)
          sum(
            for $i in 1 to $n return (
              point:px($points[$i])*point:py($points[$i+1]) -
              point:px($points[$i+1])*point:py($points[$i])
            )
          )
        ) div 2E0
      )
    )
  )
  default return errors:error("GEOM-BADREGION", ($polygon, "area"))
}

Function: length
declare function length($path as map(xs:string,item()*)) as xs:double

Params
  • path as map(xs:string,item()*)
Returns
  • xs:double
declare function this:length($path as map(xs:string,item()*)) as xs:double
{
  sum(
    for $edge in this:edges($path) return edge:length($edge)
  )
}

Function: polygon-center
declare function polygon-center($polygon as map(xs:string,item()*)) as map(xs:string,item()*)

Params
  • polygon as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:polygon-center(
  $polygon as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $n := count(this:edges($polygon))
  return switch ($n)
  case 0 return $point:ORIGIN (: GIGO :)
  case 1 return this:edges($polygon)[1]=>edge:edge-point(0.5)
  case 2 return this:edges($polygon)[1]=>edge:edge-point(0.5)
  case 3 return
    let $edges := this:edges($polygon)
    return (
      (
        edge:intersection(
          edge:edge(edge:start($edges[1]), $edges[2]=>edge:edge-point(0.5)),
          edge:edge(edge:start($edges[2]), $edges[3]=>edge:edge-point(0.5))
        ),
        $edges[1]=>edge:edge-point(0.5)  (: In case of colinearity :)
      )=>head()
    )
  case 4 return
    let $points := this:points($polygon)
    return (
      (
        edge:intersection(
          edge:edge($points[1], $points[3]),
          edge:edge($points[2], $points[4])
        ),
        this:edges($polygon)[1]=>edge:edge-point(0.5)  (: In case of colinearity :)
      )=>head()
    )
  default return (
    (: This formula is actually only good for non-intersecting
     : straight-edge 2D polygons, so it is going to be a dubious approximation
     : for other cases.
     :)
    let $points := this:points($polygon)
    (: Note: 1 more point than edges start=end :)
    let $area :=
      sum(
        for $i in 1 to $n return (
          point:px($points[$i])*point:py($points[$i+1]) -
          point:px($points[$i+1])*point:py($points[$i])
        )
      ) div 2
    return (
      point:point(
        sum(
          for $i in 1 to $n return (
            (point:px($points[$i]) + point:px($points[$i+1]))*
            (
              point:px($points[$i])*point:py($points[$i+1]) -
              point:px($points[$i+1])*point:py($points[$i])
            )
          )
        ) div (6*$area),
        sum(
          for $i in 1 to $n return (
            (point:py($points[$i]) + point:py($points[$i+1]))*
            (
              point:px($points[$i])*point:py($points[$i+1]) -
              point:px($points[$i+1])*point:py($points[$i])
            )
          )
        ) div (6*$area)
      )
    )
  )
}

Function: path-point
declare function path-point($path as map(xs:string,item()*), $t as xs:double) as map(xs:string, item()*)


path-point()
Find a point at fraction $t between start and finish

First find the edge that encompasses that fraction of the path
The find the edge t and use it to get the point
Suppose edge covers [0.3, 0.5] of path and $t = 0.4 => $edge-t = 0.5
($t - $start-t) div ($end-t - $start-t)

If you are going to compute a bunch of these, augment the path using
path:with-edge-ts()

Params
  • path as map(xs:string,item()*): the path
  • t as xs:double: fraction of path [0,1]
Returns
  • map(xs:string,item()*)
declare function this:path-point($path as map(xs:string,item()*), $t as xs:double) as map(xs:string, item()*)
{
  let $edges := this:edges($path)
  let $edge-ts := $path=>this:edge-ts()
  let $edge-ix := util:rangeindex($edge-ts, $t)
  let $start-t := if ($edge-ix = 1) then 0 else $edge-ts[$edge-ix - 1]
  let $end-t := $edge-ts[$edge-ix]
  let $edge-t := (
    (: start-t = end-t if one of the edges has zero length, which really
     : should't happen. If it does, just take the start of the edge
     :)
    if ($start-t = $end-t)
    then 0
    else ($t - $start-t) div ($end-t - $start-t)
  )
  return (
    edge:edge-point($edges[$edge-ix], $edge-t)
  )
}

Function: osculating-circle
declare function osculating-circle($path as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*)


osculating-circle()
Compute the osculating circle at the given point on the path.
Returns a zero radius circle at the path point for infinite or NaN
curvatures.

Params
  • path as map(xs:string,item()*)
  • t as xs:double
Returns
  • map(xs:string,item()*)
declare function this:osculating-circle(
  $path as map(xs:string,item()*),
  $t as xs:double
) as map(xs:string,item()*)
{
  let $edges := this:edges($path)
  let $edge-ts := $path=>this:edge-ts()
  let $edge-ix := util:rangeindex($edge-ts, $t)
  let $start-t := if ($edge-ix = 1) then 0 else $edge-ts[$edge-ix - 1]
  let $end-t := $edge-ts[$edge-ix]
  let $edge-t := (
    (: start-t = end-t if one of the edges has zero length, which really
     : should't happen. If it does, just take the start of the edge
     :)
    if ($start-t = $end-t)
    then 0
    else ($t - $start-t) div ($end-t - $start-t)
  )
  return (
    edge:osculating-circle($edges[$edge-ix], $edge-t)
  )
}

Function: slice
declare function slice($path as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*)*


slice()
Create two paths at the given fraction of the source path. If we
need to slice an edge, it will be sliced per slice-edge()

Params
  • path as map(xs:string,item()*): the path to cut
  • t as xs:double: fraction of edge [0,1]
Returns
  • map(xs:string,item()*)*
declare function this:slice(
  $path as map(xs:string,item()*),
  $t as xs:double
) as map(xs:string,item()*)*
{
  if (empty(this:edges($path))) then ()
  else if (count(this:edges($path)) = 1)
  then (
    for $edge in edge:slice(this:edges($path), $t)
    return this:path($edge)
  ) else (
    let $edges := this:edges($path)
    let $edge-ts := $path=>this:edge-ts()
    let $edge-ix := util:rangeindex($edge-ts, $t)
    let $start-t := (if ($edge-ix = 1) then 0 else $edge-ts[$edge-ix - 1])
    let $end-t := $edge-ts[$edge-ix]
    let $edge-t :=
      (: start-t = end-t if one of the edges has zero length, which really
       : should't happen. If it does, just take the start of the edge
       :)
      if ($start-t = $end-t)
      then 0
      else ($t - $start-t) div ($end-t - $start-t)
    let $slices := edge:slice($edges[$edge-ix], $edge-t)
    let $edges1 := (
      $edges[position() < $edge-ix],
      if ($edge-t=0) then ()
      else if ($edge-t=1) then $slices
      else $slices[1]
    )
    let $edges2 := (
      if ($edge-t=0) then $slices
      else if ($edge-t=1) then ()
      else $slices[2],
      $edges[position() > $edge-ix]
    )
    return (
      if (exists($edges1)) then this:path($edges1) else (),
      if (exists($edges2)) then this:path($edges2) else ()
    )
  )
}

Function: slice
declare function slice($path as map(xs:string,item()*), $start-t as xs:double, $end-t as xs:double) as map(xs:string,item()*)*


slice()
Slice the path between two cut points. Will end up with three paths in
general: [start, p(start-t)], [p(start-t), p(end-t)], [p(end-t), end]
start-t <= end-t

If start-t=end-t this is same as slice(path, start-t)
If start-t or end-t is at an end will get one less edge

Params
  • path as map(xs:string,item()*): the path to cut
  • start-t as xs:double: fraction of path [0,1]
  • end-t as xs:double: fraction of path [0,1]
Returns
  • map(xs:string,item()*)*
declare function this:slice(
  $path as map(xs:string,item()*),
  $start-t as xs:double,
  $end-t as xs:double
) as map(xs:string,item()*)*
{
  if ($start-t > $end-t) then errors:error("GEOM-BADT", ($start-t, $end-t)) else (),
  if (empty(this:edges($path))) then ()
  else if (count(this:edges($path))=1) then (
    for $edge in edge:slice(this:edges($path), $start-t, $end-t)
    return this:path($edge)
  ) else (
    if ($start-t = 1) then $path (: start-t=end-t=1 :)
    else if ($start-t = $end-t) then this:slice($path, $start-t)
    else if ($start-t = 0) then this:slice($path, $end-t)
    else (
      (:
       : end-t needs to be adjusted for the second slice:
       : Effective end-t should be fraction of length(slice) not of length(path)
       : e * l - l1 = x * l2; l1 = s * l, l2 = l - l1 = l - s * l
       : x = (el - sl) / (s - sl) = (e - s) / (1 - s)
       :)
      let $slice := this:slice($path, $start-t)
      let $eff-end-t := ($end-t - $start-t) div (1 - $start-t)
      return ($slice[1], this:slice($slice[2]=>this:with-edge-ts(), $eff-end-t))
    )
  )
}

Function: path-t
declare function path-t($path as map(xs:string,item()*), $point as map(xs:string,item()*)) as xs:double


path-t()
Determine the path t-value of the given point.

Params
  • path as map(xs:string,item()*): the path
  • point as map(xs:string,item()*): the point on the path
Returns
  • xs:double
declare function this:path-t(
  $path as map(xs:string,item()*),
  $point as map(xs:string,item()*)
) as xs:double
{
  let $path := $path=>this:with-edge-ts()
  let $edges := this:edges($path)
  let $edge-ts := $path=>this:edge-ts()
  let $edge-ix :=
    (
      for $edge at $i in $edges
      let $intersects := $edge=>edge:edge-intersects($point)
      return (
        if ($intersects) then $i else ()
      )
    )=>head()
  let $edge-t := $edges[$edge-ix]=>edge:edge-t($point)
  let $start-t := if ($edge-ix = 1) then 0.0 else $edge-ts[$edge-ix - 1]
  let $end-t := $edge-ts[$edge-ix]
  return (
    $start-t + $edge-t * ($end-t - $start-t)
  )
}

Function: tangent
declare function tangent($region as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*)


tangent()
Return the tangent vector to the given point on the path (2D)

Params
  • region as map(xs:string,item()*)
  • t as xs:double
Returns
  • map(xs:string,item()*)
declare function this:tangent(
  $region as map(xs:string,item()*),
  $t as xs:double
) as map(xs:string,item()*)
{
  let $edges := this:edges($region)
  let $edge-ts := $region=>this:edge-ts()
  let $edge-ix := util:rangeindex($edge-ts, $t)
  let $start-t := if ($edge-ix = 1) then 0 else $edge-ts[$edge-ix - 1]
  let $end-t := $edge-ts[$edge-ix]
  let $edge-t :=
    (: start-t = end-t if one of the edges has zero length, which really
     : should't happen. If it does, just take the start of the edge
     :)
    if ($start-t = $end-t)
    then 0
    else ($t - $start-t) div ($end-t - $start-t)
  return (
    edge:tangent($edges[$edge-ix], $edge-t)
  )
}

Function: bounding-box
declare function bounding-box($regions as map(xs:string,item()*)*) as map(xs:string,item()*)


bounding-box()
Minimum box surrounding the set of regions. Some approximation for non-linear
edges.

Params
  • regions as map(xs:string,item()*)*
Returns
  • map(xs:string,item()*)
declare function this:bounding-box($regions as map(xs:string,item()*)*) as map(xs:string,item()*)
{
  let $boxes := (
    for $region in $regions return
    if (this:kind($region) = ("path", "polygon")) then (
      let $pts :=
        if (every $edge in this:edges($region) satisfies this:kind($edge)="edge")
        then (
          this:points($region)
        ) else (
          let $boxes := this:edges($region)!edge:bounding-box(.)
          return $boxes!box:points(.)
        )
      return (
        box:box(
          min($pts!point:px(.)), min($pts!point:py(.)),
          max($pts!point:px(.)), max($pts!point:py(.))
        )
      )
    ) else (
      errors:error("GEOM-BADREGION", ($region, "bounding-box"))
    )
  )
  return (
    if (empty($boxes)) then box:box(0,0,0,0)
    else if (empty(tail($boxes))) then $boxes
    else (
      let $pts := $boxes!box:points(.)
      return
        box:box(
          min($pts!point:px(.)), min($pts!point:py(.)),
          max($pts!point:px(.)), max($pts!point:py(.))
        )
    )
  )
}

Function: polygon-contains-point
declare function polygon-contains-point($polygon as map(xs:string,item()*), $point as map(xs:string,item()*), $tolerance as xs:double) as xs:boolean

Params
  • polygon as map(xs:string,item()*)
  • point as map(xs:string,item()*)
  • tolerance as xs:double
Returns
  • xs:boolean
declare function this:polygon-contains-point(
  $polygon as map(xs:string,item()*),
  $point as map(xs:string,item()*),
  $tolerance as xs:double
) as xs:boolean
{
  (: Special cases: point is a vertex or lies on a axis edge
   : count point as in. Non-axis edges below with winding numbers.
   :)
  (some $edge in this:edges($polygon) satisfies (
    let $v0 := edge:start($edge)
    let $v1 := edge:end($edge)
    return
      point:same($point, $v0, $tolerance) or
      point:same($point, $v1, $tolerance) or
      (
        point:px($v0)=point:px($v1) and
        point:px($v0)=point:px($point) and
        util:twixt(point:py($point), point:py($v0), point:py($v1))
      ) or
      (
        point:py($v0)=point:py($v1) and
        point:py($v0)=point:py($point) and
        util:twixt(point:px($point), point:px($v0), point:px($v1))
      )
  ))
  or
  fold-left(
    this:edges($polygon),
    0,
    function($wn as xs:integer, $edge as map(xs:string,item()*)) as xs:integer {
      let $v0 := edge:start($edge)
      let $v1 := edge:end($edge)
      let $orientation := point:sorientation($v0, $v1, $point)
      return (
        if (point:py($v0) <= point:py($point)) then (
          (: point is above start of edge :)
          if (point:py($v1) > point:py($point)) then (
            (: point is below end of edge (i.e. between start and end) :)
            (: increment winding number if it is to left :)
            if ($orientation > 0)
            then $wn + 1
            else $wn
          ) else (
            $wn
          )
        ) else (
          (: point is below start of edge :)
          if (point:py($v1) <= point:py($point)) then (
            (: point is above end of edge (i.e. between start and end) :)
            (: decrement winding number if it is to right :)
            if ($orientation <= 0)
            then $wn - 1
            else $wn
          ) else (
            $wn
          )
        )
      )
    }
  ) != 0
}

Function: polygon-contains-point
declare function polygon-contains-point($polygon as map(xs:string,item()*), $point as map(xs:string,item()*)) as xs:boolean

Params
  • polygon as map(xs:string,item()*)
  • point as map(xs:string,item()*)
Returns
  • xs:boolean
declare function this:polygon-contains-point(
  $polygon as map(xs:string,item()*),
  $point as map(xs:string,item()*)
) as xs:boolean
{
  this:polygon-contains-point($polygon, $point, $config:tolerance)
}

Function: simplify
declare function simplify($path-or-polygon as map(xs:string,item()*), $ε as xs:double) as map(xs:string,item()*)


simplify()
Douglas-Peucker simplification. Given a path returns a path, given a
polygon returns a polygon.
https://karthaus.nl/rdp/ via Wikipedia

Params
  • path-or-polygon as map(xs:string,item()*): either a path, or a polygon
  • ε as xs:double: error
Returns
  • map(xs:string,item()*)
declare function this:simplify(
  $path-or-polygon as map(xs:string,item()*),
  $ε as xs:double
) as map(xs:string,item()*)
{
  let $pts := this:vertices($path-or-polygon)
  let $n := count($pts)
  return (
    if ($n <= 3) then $path-or-polygon
    else switch (this:kind($path-or-polygon))
    case "polygon" return this:polygon(edge:to-edges(this:simplify-points($pts, $ε)))
    case "path" return this:path(edge:to-edges(this:simplify-points($pts, $ε)))
    default return $path-or-polygon
  )
}

Function: simplify-points
declare function simplify-points($pts as map(xs:string,item()*)*, $ε as xs:double) as map(xs:string,item()*)*


simplify-points()
Workhorse of simplify(): recursive Douglas-Peucker

Params
  • pts as map(xs:string,item()*)*
  • ε as xs:double
Returns
  • map(xs:string,item()*)*
declare function this:simplify-points(
  $pts as map(xs:string,item()*)*,
  $ε as xs:double
) as map(xs:string,item()*)*
{
  let $line := edge:edge($pts[1], $pts[last()])
  let $find-max :=
    fold-left(2 to count($pts) - 1, array{0, 1},
      function($data as array(xs:numeric), $i as xs:integer) as array(xs:numeric) {
        let $d := $line=>edge:point-distance($pts[$i])
        return (
          if ($d > $data(1)) then array {$d, $i} else $data
        )
      }
    )
  let $dmax := $find-max(1)
  let $index := $find-max(2)
  return (
    if ($dmax > $ε) then (
      this:simplify-points($pts[position() <= $index], $ε)[position() < last()],
      this:simplify-points($pts[position() > $index], $ε)
    ) else (
      $pts[1], $pts[last()]
    )
  )
}

Function: chaikin-points
declare function chaikin-points($points as map(xs:string,item()*)*, $closed as xs:boolean, $t as xs:double, $iterations as xs:integer) as map(xs:string,item()*)*


chaikin-points()
Workhorse of Chaikin's corner-cutting smoothing algorithm
Don't use high iterations: you'll run out of heap and it doesn't really change things
much. Closed sequences will cut the corner from last to first. Open sequences
will leave the first and last points alone.

Params
  • points as map(xs:string,item()*)*: sequence of input points
  • closed as xs:boolean: closed or open?
  • t as xs:double: cutting ratio [0, 0.5]
  • iterations as xs:integer: number of iterations
Returns
  • map(xs:string,item()*)*
declare function this:chaikin-points(
  $points as map(xs:string,item()*)*,
  $closed as xs:boolean,
  $t as xs:double,
  $iterations as xs:integer
) as map(xs:string,item()*)*
{
  if (not(util:twixt($t, 0.0, 0.5))) then errors:error("ML-BADARGS", ("t", $t)) else
  if ($closed) then (
    (: Closed loop: be sure to cut corner between last and first as well :)
    fold-left(1 to $iterations, $points,
      function($points as map(xs:string,item()*)*, $i as xs:integer) as map(xs:string,item()*)* {
        if (empty($points[2])) then $points else
        let $n := count($points)
        for $i in 1 to $n return (
          (:
           : e.g. for t=0.25 (default)
           : p[i]*0.75 + p[i+1]*0.25
           : p[i]*0.25 + p[i+1]*0.75
           :)
          $points[$i]=>point:times(1 - $t)=>point:add($points[util:modix($i + 1, $n)]=>point:times($t)),
          $points[$i]=>point:times($t)=>point:add($points[util:modix($i + 1, $n)]=>point:times(1 - $t))
        )
      }
    )
  ) else (
    (: Open: leave start and end points alone :)
    fold-left(1 to $iterations, $points,
      function($points as map(xs:string,item()*)*, $i as xs:integer) as map(xs:string,item()*)* {
        if (empty($points[2])) then $points else
        let $n := count($points) return (
          $points[1],
          $points[1]=>point:times($t)=>point:add($points[2]=>point:times(1 - $t))
          ,
          for $i in 2 to $n - 2 return (
            (:
             : e.g. for t=0.25 (default)
             : p[i]*0.75 + p[i+1]*0.25
             : p[i]*0.25 + p[i+1]*0.75
             :)
            $points[$i]=>point:times(1 - $t)=>point:add($points[$i + 1]=>point:times($t)),
            $points[$i]=>point:times($t)=>point:add($points[$i + 1]=>point:times(1 - $t))
          ),
          $points[$n - 1]=>point:times(1 - $t)=>point:add($points[$n]=>point:times($t)),
          $points[$n]
        )
      }
    )
  )
}

Function: chaikin
declare function chaikin($region as map(xs:string,item()*), $t as xs:double, $iterations as xs:integer) as map(xs:string,item()*)


chaikin()
Chaikin's corner-cutting algorithm.

Params
  • region as map(xs:string,item()*): path or polygon
  • t as xs:double: cutting ratio [0, 0.5] (default=0.25)
  • iterations as xs:integer: number of iterations (default=1)
Returns
  • map(xs:string,item()*)
declare function this:chaikin(
  $region as map(xs:string,item()*),
  $t as xs:double,
  $iterations as xs:integer
) as map(xs:string,item()*)
{
  if (this:kind($region) = "polygon") then (
    this:polygon(edge:to-edges(
      this:chaikin-points(this:vertices($region), true(), $t, $iterations)
    ))
  ) else (
    this:path(edge:to-edges(
      this:chaikin-points(this:vertices($region), false(), $t, $iterations)
    ))
  )
}

Function: chaikin
declare function chaikin($region as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*)

Params
  • region as map(xs:string,item()*)
  • t as xs:double
Returns
  • map(xs:string,item()*)
declare function this:chaikin(
  $region as map(xs:string,item()*),
  $t as xs:double
) as map(xs:string,item()*)
{
  this:chaikin($region, $t, 1)
}

Function: chaikin
declare function chaikin($region as map(xs:string,item()*)) as map(xs:string,item()*)

Params
  • region as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:chaikin(
  $region as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  this:chaikin($region, 0.25, 1)
}

Function: streamline
declare function streamline($region as map(xs:string,item()*), $ε as xs:double) as map(xs:string,item()*)


streamline()
Streamline a path to remove edges less than a certain length. Can give
a very crude approximation where you have a lot of small edges in sequence.

Params
  • region as map(xs:string,item()*): path or polygon
  • ε as xs:double: minimum edge length (default=0.6)
Returns
  • map(xs:string,item()*): path or polygon with short edges elided
declare function this:streamline(
  $region as map(xs:string,item()*),
  $ε as xs:double
) as map(xs:string,item()*)
{
  let $edges := this:edges($region)
  let $good-edges := $edges[edge:length(.) >= $ε]
  let $n-edges := count($edges)
  let $n-good := count($good-edges)
  return (
    (: util:log("ep="||$ε||" ce="||$n-edges||" ge="||$n-good), :)
    if ($ε <= 0) then $region
    else if ($n-edges <= 3) then $region
    else if ($n-good < 3) then this:streamline($region, $ε div 2)
    else if ($n-edges=$n-good) then $region
    else (
      let $new-edges := (
        for $i in 1 to $n-good - 1 return (
          if (point:distance(edge:end($good-edges[$i]), edge:start($good-edges[$i + 1])) > 0) then (
            edge:edge(edge:start($good-edges[$i]), edge:start($good-edges[$i + 1]))
          ) else (
            $good-edges[$i]
          )
        ),
        $good-edges[last()]
      )
      return (
        if (this:kind($region)="path") then (
          this:path($new-edges, this:property-map($region))
        ) else if (this:kind($region)="polygon") then (
          this:polygon($new-edges, this:property-map($region))
        ) else (
          errors:error("GEOM-BADREGION", ($region, "streamline-path"))
        )
      )
    )
  )
}

Function: streamline
declare function streamline($region as map(xs:string,item()*)) as map(xs:string,item()*)

Params
  • region as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:streamline(
  $region as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  this:streamline($region, 0.6)
}

Function: consolidate
declare function consolidate($paths as map(xs:string,item()*)*) as map(xs:string,item()*)*


consolidate()
Consolidate a collection of paths and polygons into a single polygon with
some goto edges. Makes for more efficient rendering. Consolidatation is
by the "colour" and "width" properties. Other properties will be lost.

Params
  • paths as map(xs:string,item()*)*: paths or polygons
Returns
  • map(xs:string,item()*)*: consolidated polygons
declare function this:consolidate(
  $paths as map(xs:string,item()*)*
) as map(xs:string,item()*)*
{
  let $colours := distinct-values($paths!(.("colour")))
  let $widths := distinct-values($paths!(.("width")))
  return (
    (: Consolidate those with both width and colour :)
    for $colour in $colours
    for $width in $widths
    let $relevant := $paths[.("colour")=$colour and .("width")=$widths]
    where exists($relevant)
    return (
      this:polygon((
        this:edges(head($relevant)),
        for $t at $i in tail($relevant) return (
          edge:skip(this:end($relevant[$i]), this:start($t)),
          this:edges($t)
        ),
        edge:skip(this:end($relevant[last()]), this:start($relevant[1]))
      ))=>this:with-properties(map {"colour": $colour, "width": $width})
    ),
    (: Consolidate by colour for those with no width :)
    for $colour in $colours
    let $relevant := $paths[.("colour")=$colour and empty(.("width"))]
    where exists($relevant)
    return (
      this:polygon((
        this:edges(head($relevant)),
        for $t at $i in tail($relevant) return (
          edge:skip(this:end($relevant[$i]), this:start($t)),
          this:edges($t)
        ),
        edge:skip(this:end($relevant[last()]), this:start($relevant[1]))
      ))=>this:with-properties(map {"colour": $colour})
    ),
    (: Consolidate by width for those with no colour :)
    for $width in $widths
    let $relevant := $paths[.("width")=$width and empty(.("colour"))]
    where exists($relevant)
    return (
      this:polygon((
        this:edges(head($relevant)),
        for $t at $i in tail($relevant) return (
          edge:skip(this:end($relevant[$i]), this:start($t)),
          this:edges($t)
        ),
        edge:skip(this:end($relevant[last()]), this:start($relevant[1]))
      ))=>this:with-properties(map {"width": $width})
    ),
    (: Consolidate those with no width or colour :)
    let $relevant := $paths[empty(.("width")) and empty(.("colour"))]
    where exists($relevant)
    return (
      this:polygon((
        this:edges(head($relevant)),
        for $t at $i in tail($relevant) return (
          edge:skip(this:end($relevant[$i]), this:start($t)),
          this:edges($t)
        ),
        edge:skip(this:end($relevant[last()]), this:start($relevant[1]))
      ))
    )
  )
}

Function: consolidate-by-colour
declare function consolidate-by-colour($paths as map(xs:string,item()*)*) as map(xs:string,item()*)*


consolidate-by-colour()
Consolidate a collection of paths and polygons into a single polygon with
some goto edges. Makes for more efficient rendering. Consolidatation is
by the "colour" property. Other properties will be lost.

Params
  • paths as map(xs:string,item()*)*: paths or polygons
Returns
  • map(xs:string,item()*)*: consolidated polygons
declare function this:consolidate-by-colour(
  $paths as map(xs:string,item()*)*
) as map(xs:string,item()*)*
{
  let $colours := distinct-values($paths!(.("colour")))
  return (
    (: Consolidate those with colour :)
    for $colour in $colours
    let $relevant := $paths[.("colour")=$colour]
    return (
      if (exists($relevant)) then (
        this:polygon((
          this:edges(head($relevant)),
          for $t at $i in tail($relevant) return (
            edge:skip(this:end($relevant[$i]), this:start($t)),
            this:edges($t)
          ),
          edge:skip(this:end($relevant[last()]), this:start($relevant[1]))
        ))=>this:with-properties(map {"colour": $colour})
      ) else ()
    ),
    (: Consolidate those with no colour :)
    let $relevant := $paths[empty(.("colour"))]
    return (
      if (exists($relevant)) then (
        this:polygon((
          this:edges(head($relevant)),
          for $t at $i in tail($relevant) return (
            edge:skip(this:end($relevant[$i]), this:start($t)),
            this:edges($t)
          ),
          edge:skip(this:end($relevant[last()]), this:start($relevant[1]))
        ))
      ) else ()
    )
  )
}

Function: consolidate-by-width
declare function consolidate-by-width($paths as map(xs:string,item()*)*) as map(xs:string,item()*)*


consolidate-by-width()
Consolidate a collection of paths and polygons into a single polygon with
some goto edges. Makes for more efficient rendering. Consolidatation is
by the "width" property. Other properties will be lost.

Params
  • paths as map(xs:string,item()*)*: paths or polygons
Returns
  • map(xs:string,item()*)*: consolidated polygons
declare function this:consolidate-by-width(
  $paths as map(xs:string,item()*)*
) as map(xs:string,item()*)*
{
  let $widths := distinct-values($paths!(.("width")))
  return (
    (: Consolidate those with width :)
    for $width in $widths
    let $relevant := $paths[.("width")=$width]
    where exists($relevant)
    return (
      this:polygon((
        this:edges(head($relevant)),
        for $t at $i in tail($relevant) return (
          edge:skip(this:end($relevant[$i]), this:start($t)),
          this:edges($t)
        ),
        edge:skip(this:end($relevant[last()]), this:start($relevant[1]))
      ))=>this:with-properties(map {"width": $width})
    ),
    (: Consolidate those with no width :)
    let $relevant := $paths[empty(.("width"))]
    where exists($relevant)
    return (
      this:polygon((
        this:edges(head($relevant)),
        for $t at $i in tail($relevant) return (
          edge:skip(this:end($relevant[$i]), this:start($t)),
          this:edges($t)
        ),
        edge:skip(this:end($relevant[last()]), this:start($relevant[1]))
      ))
    )
  )
}

Function: consolidate-points
declare function consolidate-points($points as map(xs:string,item()*)*, $width as xs:double) as map(xs:string,item()*)


consolidate-points()
Consolidate a collection of points into a single path with
some goto edges. Makes for more efficient rendering.

Params
  • points as map(xs:string,item()*)*: points
  • width as xs:double
Returns
  • map(xs:string,item()*): consolidated path
declare function this:consolidate-points(
  $points as map(xs:string,item()*)*,
  $width as xs:double
) as map(xs:string,item()*)
{
  if (empty($points)) then this:path(()) else
  let $points := point:distinct($points) return (
    this:path((
      edge:edge($points[1], point:point(point:px($points[1])+$width div 2, point:py($points[1]))),
      for $i in 2 to count($points) return (
        edge:skip($points[$i - 1], $points[$i]),
        edge:edge($points[$i], point:point(point:px($points[$i])+$width div 2, point:py($points[$i])))
      )
    ))=>this:with-properties(map {"width": $width, "stroke-linecap":"round"})
  )
}

Function: consolidate-points-by-colour
declare function consolidate-points-by-colour($points as map(xs:string,item()*)*, $width as xs:double) as map(xs:string,item()*)*


consolidate-points-by-colour()
Consolidate a collection of points into a single path for each colour
using goto edges. Makes for more efficient rendering.

Params
  • points as map(xs:string,item()*)*: points
  • width as xs:double
Returns
  • map(xs:string,item()*)*: consolidated path
declare function this:consolidate-points-by-colour(
  $points as map(xs:string,item()*)*,
  $width as xs:double
) as map(xs:string,item()*)*
{
  let $colours := distinct-values($points!.("colour"))
  return (
    (: Consolidate those with colours :)
    for $colour in $colours
    let $relevant := point:distinct($points[.("colour")=$colour])
    where exists($relevant)
    return (
      this:path((
        edge:edge($relevant[1], point:point(point:px($relevant[1])+$width div 2, point:py($relevant[1]))),
        for $i in 2 to count($relevant) return (
          edge:skip($relevant[$i - 1], $relevant[$i]),
          edge:edge($relevant[$i], point:point(point:px($relevant[$i])+$width div 2, point:py($relevant[$i])))
        )
      ))=>this:with-properties(map {"width": $width, "stroke-linecap":"round", "colour": $colour})
    ),
    (: consolidate those with no colour :)
    let $relevant := $points[empty(.("colour"))]
    where exists($relevant)
    return (
      this:path((
        edge:edge($relevant[1], point:point(point:px($relevant[1])+$width div 2, point:py($relevant[1]))),
        for $i in 2 to count($relevant) return (
          edge:skip($relevant[$i - 1], $relevant[$i]),
          edge:edge($relevant[$i], point:point(point:px($relevant[$i])+$width div 2, point:py($relevant[$i])))
        )
      ))=>this:with-properties(map {"width": $width, "stroke-linecap":"round"})
    )
  )
}

Function: consolidate-points-by-colour-and-opacity
declare function consolidate-points-by-colour-and-opacity($points as map(xs:string,item()*)*, $width as xs:double) as map(xs:string,item()*)*


consolidate-points-by-colour-and-opacity()
Consolidate a collection of points into a single path for each combination
of colour and opacity using goto edges. Makes for more efficient rendering.

Params
  • points as map(xs:string,item()*)*: points
  • width as xs:double
Returns
  • map(xs:string,item()*)*: consolidated path
declare function this:consolidate-points-by-colour-and-opacity(
  $points as map(xs:string,item()*)*,
  $width as xs:double
) as map(xs:string,item()*)*
{
  let $colours := distinct-values($points!.("colour"))
  let $opacities := distinct-values($points!.("opacity"))
  return (
    (: Consolidate those with colours and opacities :)
    for $colour in $colours
    for $opacity in $opacities
    let $relevant := point:distinct($points[.("colour")=$colour and .("opacity")=$opacity])
    where exists($relevant)
    return (
      this:path((
        edge:edge($relevant[1], point:point(point:px($relevant[1])+$width div 2, point:py($relevant[1]))),
        for $i in 2 to count($relevant) return (
          edge:skip($relevant[$i - 1], $relevant[$i]),
          edge:edge($relevant[$i], point:point(point:px($relevant[$i])+$width div 2, point:py($relevant[$i])))
        )
      ))=>this:with-properties(
        map {
          "width": $width,
          "stroke-linecap":"round",
          "colour": $colour,
          "stroke-opacity": $opacity
        })
    ),
    (: consolidate those with no colour :)
    for $opacity in $opacities
    let $relevant := $points[empty(.("colour")) and .("opacity")=$opacity]
    where exists($relevant)
    return (
      this:path((
        edge:edge($relevant[1], point:point(point:px($relevant[1])+$width div 2, point:py($relevant[1]))),
        for $i in 2 to count($relevant) return (
          edge:skip($relevant[$i - 1], $relevant[$i]),
          edge:edge($relevant[$i], point:point(point:px($relevant[$i])+$width div 2, point:py($relevant[$i])))
        )
      ))=>this:with-properties(
        map {
          "width": $width,
          "stroke-linecap":"round",
          "stroke-opacity": $opacity
        }
      )
    ),
    (: consolidate those with no opacity :)
    for $colour in $colours
    let $relevant := $points[.("colour")=$colour and empty(.("opacity"))]
    where exists($relevant)
    return (
      this:path((
        edge:edge($relevant[1], point:point(point:px($relevant[1])+$width div 2, point:py($relevant[1]))),
        for $i in 2 to count($relevant) return (
          edge:skip($relevant[$i - 1], $relevant[$i]),
          edge:edge($relevant[$i], point:point(point:px($relevant[$i])+$width div 2, point:py($relevant[$i])))
        )
      ))=>this:with-properties(
        map {
          "width": $width,
          "stroke-linecap":"round",
          "colour": $colour
        }
      )
    ),
    (: consolidate those with no colour or opacity :)
    let $relevant := $points[empty(.("colour")) and empty(.("opacity"))]
    where exists($relevant)
    return (
      this:path((
        edge:edge($relevant[1], point:point(point:px($relevant[1])+$width div 2, point:py($relevant[1]))),
        for $i in 2 to count($relevant) return (
          edge:skip($relevant[$i - 1], $relevant[$i]),
          edge:edge($relevant[$i], point:point(point:px($relevant[$i])+$width div 2, point:py($relevant[$i])))
        )
      ))=>this:with-properties(
        map {
          "width": $width,
          "stroke-linecap":"round"
        }
      )
    )
  )
}

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

Params
  • item as map(xs:string,item()*)
  • properties as map(xs:string,item()*)
  • drawing as map(xs:string,function(*)?)
Returns
  • item()*
declare function this:draw(
  $item as map(xs:string,item()*),
  $properties as map(xs:string,item()*),
  $drawing as map(xs:string,function(*)?)
) as item()*
{
  switch(this:kind($item))
  case "path" return $this:draw-path-impl($item, $properties, $drawing)
  case "polygon" return $this:draw-polygon-impl($item, $properties, $drawing)
  default return ()
}

Original Source Code

xquery version "3.1";
(:~
 : Path and polygon (closed path) objects
 :
 : Copyright© Mary Holstege 2020-2024
 : CC-BY (https://creativecommons.org/licenses/by/4.0/)
 : @since April 2021
 : @custom:Status Incomplete, subject to refactoring
 :)
module namespace this="http://mathling.com/geometric/path";

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 point="http://mathling.com/geometric/point"
       at "../geo/point.xqy";
import module namespace edge="http://mathling.com/geometric/edge"
       at "../geo/edge.xqy";
import module namespace box="http://mathling.com/geometric/box"
       at "../geo/box.xqy";

declare namespace art="http://mathling.com/art";
declare namespace svg="http://www.w3.org/2000/svg";
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";

declare variable $this:RESERVED as xs:string* := ("kind", "edges", "edge-ts");

(:~
 : path()
 : Construct a path from edges. If the head of the edges parameter is a
 : point, will treat the whole as a sequence of points.
 :
 : @param $edges: edges of the path, in order. Edges could be of any kind.
 : @param $properties: additional properties (default = none)
 :)
declare function this:path(
  $edges as map(xs:string,item()*)*,
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  if (empty($edges)) then (
    util:merge-into($properties,
      map {
        "kind": "path",
        "edges": ()
      }
    )
  ) else if ($edges[1]("kind")="point") then (
    (: Creating path from sequence of points :)
    (: An error, which we try to recover from :)
    this:path(edge:to-edges($edges), $properties)
  ) else (
    util:merge-into($properties,
      map {
        "kind": "path",
        "edges": $edges
      }
    )
  )
};

declare function this:path(
  $edges as map(xs:string,item()*)*
) as map(xs:string,item()*)
{
  if (empty($edges)) then (
    map {
      "kind": "path",
      "edges": ()
    }
  ) else if ($edges[1]("kind")="point") then (
    (: Creating path from sequence of points :)
    (: An error, which we try to recover from :)
    this:path(edge:to-edges($edges))
  ) else (
    map {
      "kind": "path",
      "edges": $edges
    }
  )
};

declare function this:kind($region as map(xs:string,item()*)) as xs:string
{
  $region("kind")
};

declare function this:edges(
  $path as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  if ($path("kind")=("edge","quad","cubic","arc","ellipse-arc"))
  then $path
  else $path("edges")
};

declare function this:points(
  $region as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  switch (this:kind($region))
  case "path" return (
    for $edge in $region=>this:edges()
    return $edge=>edge:start()=>point:as-dimension(2)
    ,
    ($region=>this:edges())[last()]=>edge:end()=>point:as-dimension(2)
  )
  case "polygon" return (
    for $edge in $region=>this:edges()
    return $edge=>edge:start()=>point:as-dimension(2)
    ,
    ($region=>this:edges())[last()]=>edge:end()=>point:as-dimension(2)
  )
  default return errors:error("GEOM-BADREGION", ($region, "points"))
};

declare function this:vertices(
  $region as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  switch (this:kind($region))
  case "path" return (
    for $edge in $region=>this:edges()
    return $edge=>edge:start()
    ,
    ($region=>this:edges())[last()]=>edge:end()
  )
  case "polygon" return (
    for $edge in $region=>this:edges()
    return $edge=>edge:start()
    ,
    ($region=>this:edges())[last()]=>edge:end() (: Includes closing vertex=starting vertex :)
  )
  default return errors:error("GEOM-BADREGION", ($region, "vertices"))
};

declare function this:vertices(
  $region as map(xs:string,item()*),
  $close as xs:boolean
) as map(xs:string,item()*)*
{
  switch (this:kind($region))
  case "path" return (
    for $edge in $region=>this:edges()
    return $edge=>edge:start()
    ,
    ($region=>this:edges())[last()]=>edge:end()
  )
  case "polygon" return (
    for $edge in $region=>this:edges()
    return $edge=>edge:start()
    ,
    if ($close) then ($region=>this:edges())[last()]=>edge:end() else ()
  )
  default return errors:error("GEOM-BADREGION", ($region, "vertices"))
};

(:~
 : as-polygon()
 : Turn a path into a polygon.
 :
 : @param $region: the path
 : @return unnormalized polygon
 :)
declare function this:as-polygon(
  $region as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  switch (this:kind($region))
  case "path" return this:polygon($region=>this:edges(), $region=>this:property-map())
  case "polygon" return $region
  default return errors:error("GEOM-BADREGION", ($region, "as-polygon"))
};

(:~
 : polygon()
 : Construct a polygon from edges. If the head of the edges parameter is a
 : point, will treat the whole as a sequence of points.
 :
 : @param $edges: edges of the polygon
 : @return unnormalized polygon
 :)
declare function this:polygon(
  $edges as map(xs:string,item()*)*
) as map(xs:string,item()*)
{
  this:polygon($edges, map {})
};

(:~
 : polygon()
 : Construct a polygon from edges. If the head of the edges parameter is a
 : point, will treat the whole as a sequence of points.
 :
 : @param $edges: edges of the polygon
 : @param $properties: additional properties
 : @return unnormalized polygon
 :)
declare function this:polygon(
  $edges as map(xs:string,item()*)*,
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  if (empty($edges)) then (
    util:merge-into($properties,
      map {
        "kind": "polygon",
        "edges": ()
      }
    )
  ) else if ($edges[1]("kind")="point") then (
    (: Creating polygon from sequence of points :)
    (: An error, which we try to recover from :)
    this:polygon(
      (
        edge:to-edges($edges),
        if (not(point:same($edges[1],$edges[last()])))
        then edge:edge($edges[last()], $edges[1])
        else ()
      )
      ,
      $properties
    )
  ) else (
    util:merge-into($properties,
      map {
        "kind": "polygon",
        "edges": (
          $edges, (
            if (not(point:same(edge:start($edges[1]),edge:end($edges[last()])))) then (
              edge:edge(edge:end($edges[last()]), edge:start($edges[1]))
            ) else ()
          )
        )
      }
    )
  )
};

(:~
 : rectangle()
 : Construct an axis-aligned rectangle.
 : @param $min-pt: minimum point
 : @param $max-pt: maximum point
 : @return normalized axis-aligned rectangle
 :)
declare function this:rectangle($min-pt as map(xs:string,item()*), $max-pt as map(xs:string,item()*)) as map(xs:string,item()*)
{
  let $z := avg((point:pz($min-pt),point:pz($max-pt)))
  let $minmaxpt := point:point(point:px($min-pt), point:py($max-pt), $z)
  let $maxminpt := point:point(point:px($max-pt), point:py($min-pt), $z)
  return
  this:polygon((
    edge:edge($min-pt, $maxminpt),
    edge:edge($maxminpt, $max-pt),
    edge:edge($max-pt, $minmaxpt),
    edge:edge($minmaxpt, $min-pt)
  ))
};

(:~
 : triangle()
 : Construct a triangle given three points.
 : @param $a: first point
 : @param $b: second point
 : @param $c: third point
 : @return unnormalized triangle
 :)
declare function this:triangle(
  $a as map(xs:string,item()*),
  $b as map(xs:string,item()*),
  $c as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  this:polygon((
    edge:edge($a, $b),
    edge:edge($b, $c),
    edge:edge($c, $a)
  ))
};

(:~
 : triangle()
 : Construct a triangle given three points.
 : @param $a: first point
 : @param $b: second point
 : @param $c: third point
 : @param $properties: additional properties
 : @return unnormalized triangle
 :)
declare function this:triangle(
  $a as map(xs:string,item()*),
  $b as map(xs:string,item()*),
  $c as map(xs:string,item()*),
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  this:polygon((
    edge:edge($a, $b),
    edge:edge($b, $c),
    edge:edge($c, $a)
  ), $properties)
};

(:~
 : quadrangle()
 : Construct a quadrangle given four points.
 :
 : @param $a: first point
 : @param $b: second point
 : @param $c: third point
 : @param $d: fourth point
 : @return unnormalized quadrangle
 :)
declare function this:quadrangle(
  $a as map(xs:string,item()*),
  $b as map(xs:string,item()*),
  $c as map(xs:string,item()*),
  $d as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  this:polygon((
    edge:edge($a, $b),
    edge:edge($b, $c),
    edge:edge($c, $d),
    edge:edge($d, $a)
  ))
};

(:~
 : quadrangle()
 : Construct a quadrangle given four points.
 : @param $a: first point
 : @param $b: second point
 : @param $c: third point
 : @param $d: fourth point
 : @param $properties: additional properties
 : @return quadrangle
 :)
declare function this:quadrangle(
  $a as map(xs:string,item()*),
  $b as map(xs:string,item()*),
  $c as map(xs:string,item()*),
  $d as map(xs:string,item()*),
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  this:polygon((
    edge:edge($a, $b),
    edge:edge($b, $c),
    edge:edge($c, $d),
    edge:edge($d, $a)
  ), $properties)
};

(:~
 : ngon()
 : Construct a regular n-sided polygon.
 :
 : @param $sides: how many sides to the polygon
 : @param $center: center of polygon
 : @param $radius: distance from center to vertices
 : @return normalized n-sided regular polygon
 :)
declare function this:ngon(
  $sides as xs:integer,
  $center as map(xs:string,item()*),
  $radius as xs:double
) as map(xs:string,item()*)
{
  this:polygon(
    (
      for $i in 1 to $sides
      let $angle := ($i - 1)*360 div $sides
      return (
        $center=>point:destination($angle, $radius)
      )
    )=>edge:to-edges()
  )
};

(:~
 : star()
 : Construct a regular n-sided polygon drawn as a star. If there are an odd number
 : of sides this is a single polygon; if even it is essentially two polygons,
 : joined with a skip edge.
 :
 : @param $sides: how many sides to the polygon
 : @param $center: center of polygon
 : @param $radius: distance from center to vertices
 : @return normalized polygon
 :)
declare function this:star(
  $sides as xs:integer,
  $center as map(xs:string,item()*),
  $radius as xs:double
) as map(xs:string,item()*)
{
  let $points := (
    for $i in 1 to $sides
    let $angle := ($i - 1)*360 div $sides
    return (
      $center=>point:destination($angle, $radius)
    )
  )
  let $skip := (if ($sides mod 2 = 0) then ($sides - 1) idiv 2 else $sides idiv 2)
  return (
    if ($sides mod 2 = 1 or $skip mod 2 = 1) then (
      this:polygon(
        edge:to-edges(
          let $positions := (
            fold-left(1 to $sides, 1,
              function($positions as xs:integer*, $i as xs:integer) as xs:integer* {
                $positions,
                util:modix($positions[last()] + $skip, $sides)
              }
            )
          )
          for $pos in $positions return $points[$pos]
        )
      )
    ) else (
      this:polygon(
        (
          edge:to-edges(
            let $positions := (
              fold-left(1 to $sides idiv 2, 1,
                function($positions as xs:integer*, $i as xs:integer) as xs:integer* {
                  $positions,
                  util:modix($positions[last()] + $skip, $sides)
                }
              )
            )
            for $pos in $positions return $points[$pos]
          ),
          edge:skip($points[1], $points[2]),
          edge:to-edges(
            let $positions := (
              fold-left(1 to $sides idiv 2, 2,
                function($positions as xs:integer*, $i as xs:integer) as xs:integer* {
                  $positions,
                  util:modix($positions[last()] + $skip, $sides)
                }
              )
            )
            for $pos in $positions return $points[$pos]
          ),
          edge:skip($points[2], $points[1])
        )
      )
    )
  )
};

(:~
 : normalize()
 : Normalize a polygon so the vertices are ordered counterclockwise.
 :
 : @param $polygon: polygon to normalize
 : @return normalized polygon
 :)
declare function this:normalize(
  $polygon as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  if (this:kind($polygon)!="polygon")
  then errors:error("GEOM-BADREGION", ($polygon, "normalize"))
  else (),
  let $points := $polygon=>this:vertices(false())=>point:counterclockwise()
  return (
    this:polygon(edge:to-edges($points), this:property-map($polygon))
  )
};

declare %private function this:compute-edge-ts($path as map(xs:string, item()*)) as xs:double*
{
  let $edges := this:edges($path)
  let $edge-lengths := $edges!edge:length(.)
  let $partial-lengths :=
    fold-left(
      tail($edge-lengths),
      head($edge-lengths),
      function($lengths as xs:double*, $edge-length as xs:double) as xs:double* {
        $lengths,
        $lengths[last()] + $edge-length
      }
    )
  let $path-length := $partial-lengths[last()]
  return $partial-lengths!(. div $path-length)
};

(:~
 : edge-ts()
 : Accessor for the edge t values: fractions of the path length that
 : edge edge represents. Used for optimized path interpolations.
 :
 : @param $path: the path
 :)
declare function this:edge-ts($path as map(xs:string, item()*)) as xs:double*
{
  if ($path=>map:contains("edge-ts")) then $path("edge-ts")
  else this:compute-edge-ts($path) (: Warning: SLOW! :)
};

(:~
 : with-edge-ts()
 : Return the path with edge t values computed.
 : Recommended for optimized path interpolations.
 :
 : @param $path: the path
 :)
declare function this:with-edge-ts($path as map(xs:string, item()*)) as map(xs:string, item()*)
{
  $path=>
    map:put("edge-ts", this:compute-edge-ts($path))
};

(:~
 : edge-intersections()
 : Points of intersection, if any between path and edge
 :)
declare function this:edge-intersections(
  $this as map(xs:string,item()*),
  $line as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  for $edge in this:edges($this) return edge:intersection($edge, $line)
};

(:======================================================================
 : Property management
 :======================================================================:)

(:~
 : property-map()
 : Return the annotation properties of the region as a map. Check whether this
 : is actually a path or polygon.
 :
 : @param $region: the region
 :)
declare function this:property-map(
  $region as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  switch (this:kind($region))
  case "path" return util:exclude($region, $this:RESERVED)
  case "polygon" return util:exclude($region, $this:RESERVED)
  default return map {}
};

(:~
 : properties()
 : Return the names of the annotation properties of the region.
 : Check whether this is actually a path or polygon.
 :
 : @param $region: the region
 :)
declare function this:properties(
  $region as map(xs:string,item()*)
) as xs:string*
{
  switch (this:kind($region))
  case "path" return ($region=>map:keys())[not(. = $this:RESERVED)]
  case "polygon" return ($region=>map:keys())[not(. = $this:RESERVED)]
  default return map {}
};

(:~
 : with-properties()
 : Annotate the region with some new properties and return the new region.
 : Will not touch any of the core properties. Will override existing properties
 : with the same keys but leave properties with different keys in place.
 : Raises an error if this is not actually a path or polygon.
 :
 : @param $region: the region
 :)
declare function this:with-properties(
  $regions as map(xs:string,item()*)*,
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  for $region in $regions return
  switch (this:kind($region))
  case "path" return
    util:merge-into($region, util:exclude($properties,$this:RESERVED))
  case "polygon" return
    util:merge-into($region, util:exclude($properties,$this:RESERVED))
  default return errors:error("GEOM-BADREGION", ($region, "with-properties"))
};

(:======================================================================
 : Operations
 :======================================================================:)

(:~
 : snap()
 : Snap the coordinates of the points, returning the region with snapped
 : (i.e. integer) points coordinates.
 :
 : @param $regions: the regions
 :)
declare function this:snap(
  $regions as map(xs:string,item()*)*
) as map(xs:string,item()*)*
{
  for $region in $regions return switch(this:kind($region))
  case "path" return
    this:path(edge:snap(this:edges($region)), $region)
  case "polygon" return
    this:polygon(edge:snap(this:edges($region)), $region)
  default return $region
};

(:~
 : decimal()
 : Perform decimal rounding on all the point coordinates (see util:decimal).
 :
 : @param $regions: the regions to round
 : @param $digits: how many digits after the decimal point to keep
 :)
declare function this:decimal(
  $regions as map(xs:string,item()*)*,
  $digits as xs:integer
) as map(xs:string,item()*)*
{
  for $region in $regions return switch(this:kind($region))
  case "path" return
    this:path(edge:decimal(this:edges($region), $digits), $region)
  case "polygon" return
    this:polygon(edge:decimal(this:edges($region), $digits), $region)
  default return $region
};

(:~
 : quote()
 : Return a string value for the path, suitable for debugging.
 :
 : @param $paths: the path sequence to quote
 :)
declare function this:quote(
  $paths as map(xs:string,item()*)*
) as xs:string
{
  string-join(
    for $path in $paths return switch(this:kind($path))
    case "path" return
      "("||edge:quote($path=>this:edges())||")"
    case "polygon" return
      "("||edge:quote($path=>this:edges())||")"
    default return errors:quote($path)
    ,
    " "
  )
};


(:~
 : same()
 : Equality comparison for paths, ignoring annotation properties.
 : Return true() if they have equal coordinates.
 :
 : @param $this: one path or polygon
 : @param $other: the path or polygon to compare it to
 :)
declare function this:same(
  $this as map(xs:string,item()*),
  $other as map(xs:string,item()*)
) as xs:boolean
{
  let $this-kind := this:kind($this)
  let $other-kind := this:kind($other)
  return
  (
    (
      (($this-kind="polygon") and ($other-kind="polygon")) or
      (($this-kind="path") and ($other-kind="path"))
    ) and
    (
      let $this-edges := $this=>this:edges()
      let $other-edges := $other=>this:edges()
      return
        every $i in 1 to count($this-edges)
        satisfies edge:same($this-edges[$i], $other-edges[$i])
    )
  ) or (
    $this-kind=$other-kind and deep-equal($this,$other)
  )
};

(:~
 : mutate()
 : Run a function over a sequence of regions to produce a new sequence of
 : regions. The function maps points to points.
 :
 : @param $regions: input sequence of regions
 : @param $mutate: function that takes a point as an argument and returns a new point
 :)
declare function this:mutate(
  $regions as map(xs:string,item()*)*,
  $mutate as function(item()) as map(xs:string,item()*) (: point to point :)
) as map(xs:string,item()*)*
{
  for $region in $regions return switch(this:kind($region))
  case "path" return
    let $edges := edge:mutate($region=>this:edges(), $mutate)
    return this:path($edges, $region)
  case "polygon" return
    let $edges := edge:mutate($region=>this:edges(), $mutate)
    return this:polygon($edges, $region)
  default return $region
};

(:~
 : reverse()
 : Reverse the direction of the path.
 :
 : @param $path: the path to reverse
 :)
declare function this:reverse(
  $path as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  switch(this:kind($path))
  case "path" return
    this:path(
      for $edge in fn:reverse(this:edges($path)) return edge:reverse($edge),
      $path
    )
  case "polygon" return
    this:polygon(
      for $edge in fn:reverse(this:edges($path)) return edge:reverse($edge),
      $path
    )
  default return $path
};

(:~
 : start()
 : First point of path.
 :
 : @param $path: the path
 :)
declare function this:start($path as map(xs:string,item()*)) as map(xs:string,item()*)
{
  this:edges($path)[1]=>edge:start()
};

(:~
 : end()
 : Last point of path.
 :
 : @param $path: the path
 :)
declare function this:end($path as map(xs:string,item()*)) as map(xs:string,item()*)
{
  this:edges($path)[last()]=>edge:end()
};

(:~
 : area()
 : Area of polygon. Assumes vertices are ordered counterclockwise.
 : Will fail for certain twisty polygons.
 :)
declare function this:area($polygon as map(xs:string,item()*)) as xs:double
{
  switch(this:kind($polygon))
  case "polygon" return (
    let $points := this:vertices($polygon)
    let $n := count($points) - 1
    return (
      switch ($n)
      case 1 return 0
      case 2 return 0
      case 3 return (
        (: Heron's formula :)
        let $a := point:distance($points[1], $points[2])
        let $b := point:distance($points[2], $points[3])
        let $c := point:distance($points[3], $points[1])
        let $s := sum(($a, $b, $c)) div 2
        return math:sqrt($s * ($s - $a) * ($s - $b) * ($s - $c))
      )
      default return (
        abs( (: Sign flips for canvas geometry :)
          sum(
            for $i in 1 to $n return (
              point:px($points[$i])*point:py($points[$i+1]) -
              point:px($points[$i+1])*point:py($points[$i])
            )
          )
        ) div 2E0
      )
    )
  )
  default return errors:error("GEOM-BADREGION", ($polygon, "area"))
};

declare function this:length($path as map(xs:string,item()*)) as xs:double
{
  sum(
    for $edge in this:edges($path) return edge:length($edge)
  )
};

declare function this:polygon-center(
  $polygon as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $n := count(this:edges($polygon))
  return switch ($n)
  case 0 return $point:ORIGIN (: GIGO :)
  case 1 return this:edges($polygon)[1]=>edge:edge-point(0.5)
  case 2 return this:edges($polygon)[1]=>edge:edge-point(0.5)
  case 3 return
    let $edges := this:edges($polygon)
    return (
      (
        edge:intersection(
          edge:edge(edge:start($edges[1]), $edges[2]=>edge:edge-point(0.5)),
          edge:edge(edge:start($edges[2]), $edges[3]=>edge:edge-point(0.5))
        ),
        $edges[1]=>edge:edge-point(0.5)  (: In case of colinearity :)
      )=>head()
    )
  case 4 return
    let $points := this:points($polygon)
    return (
      (
        edge:intersection(
          edge:edge($points[1], $points[3]),
          edge:edge($points[2], $points[4])
        ),
        this:edges($polygon)[1]=>edge:edge-point(0.5)  (: In case of colinearity :)
      )=>head()
    )
  default return (
    (: This formula is actually only good for non-intersecting
     : straight-edge 2D polygons, so it is going to be a dubious approximation
     : for other cases.
     :)
    let $points := this:points($polygon)
    (: Note: 1 more point than edges start=end :)
    let $area :=
      sum(
        for $i in 1 to $n return (
          point:px($points[$i])*point:py($points[$i+1]) -
          point:px($points[$i+1])*point:py($points[$i])
        )
      ) div 2
    return (
      point:point(
        sum(
          for $i in 1 to $n return (
            (point:px($points[$i]) + point:px($points[$i+1]))*
            (
              point:px($points[$i])*point:py($points[$i+1]) -
              point:px($points[$i+1])*point:py($points[$i])
            )
          )
        ) div (6*$area),
        sum(
          for $i in 1 to $n return (
            (point:py($points[$i]) + point:py($points[$i+1]))*
            (
              point:px($points[$i])*point:py($points[$i+1]) -
              point:px($points[$i+1])*point:py($points[$i])
            )
          )
        ) div (6*$area)
      )
    )
  )
};


(:~
 : path-point()
 : Find a point at fraction $t between start and finish
 :
 : First find the edge that encompasses that fraction of the path
 : The find the edge t and use it to get the point
 : Suppose edge covers [0.3, 0.5] of path and $t = 0.4 => $edge-t = 0.5
 : ($t - $start-t) div ($end-t - $start-t)
 :
 : If you are going to compute a bunch of these, augment the path using
 : path:with-edge-ts()
 :
 : @param $path: the path
 : @param $t: fraction of path [0,1]
 :)
declare function this:path-point($path as map(xs:string,item()*), $t as xs:double) as map(xs:string, item()*)
{
  let $edges := this:edges($path)
  let $edge-ts := $path=>this:edge-ts()
  let $edge-ix := util:rangeindex($edge-ts, $t)
  let $start-t := if ($edge-ix = 1) then 0 else $edge-ts[$edge-ix - 1]
  let $end-t := $edge-ts[$edge-ix]
  let $edge-t := (
    (: start-t = end-t if one of the edges has zero length, which really
     : should't happen. If it does, just take the start of the edge
     :)
    if ($start-t = $end-t)
    then 0
    else ($t - $start-t) div ($end-t - $start-t)
  )
  return (
    edge:edge-point($edges[$edge-ix], $edge-t)
  )
};

(:~
 : osculating-circle()
 : Compute the osculating circle at the given point on the path.
 : Returns a zero radius circle at the path point for infinite or NaN
 : curvatures.
 :)
declare function this:osculating-circle(
  $path as map(xs:string,item()*),
  $t as xs:double
) as map(xs:string,item()*)
{
  let $edges := this:edges($path)
  let $edge-ts := $path=>this:edge-ts()
  let $edge-ix := util:rangeindex($edge-ts, $t)
  let $start-t := if ($edge-ix = 1) then 0 else $edge-ts[$edge-ix - 1]
  let $end-t := $edge-ts[$edge-ix]
  let $edge-t := (
    (: start-t = end-t if one of the edges has zero length, which really
     : should't happen. If it does, just take the start of the edge
     :)
    if ($start-t = $end-t)
    then 0
    else ($t - $start-t) div ($end-t - $start-t)
  )
  return (
    edge:osculating-circle($edges[$edge-ix], $edge-t)
  )
};

(:~
 : slice()
 : Create two paths at the given fraction of the source path. If we
 : need to slice an edge, it will be sliced per slice-edge()
 :
 : @param $path: the path to cut
 : @param $t: fraction of edge [0,1]
 :)
declare function this:slice(
  $path as map(xs:string,item()*),
  $t as xs:double
) as map(xs:string,item()*)*
{
  if (empty(this:edges($path))) then ()
  else if (count(this:edges($path)) = 1)
  then (
    for $edge in edge:slice(this:edges($path), $t)
    return this:path($edge)
  ) else (
    let $edges := this:edges($path)
    let $edge-ts := $path=>this:edge-ts()
    let $edge-ix := util:rangeindex($edge-ts, $t)
    let $start-t := (if ($edge-ix = 1) then 0 else $edge-ts[$edge-ix - 1])
    let $end-t := $edge-ts[$edge-ix]
    let $edge-t :=
      (: start-t = end-t if one of the edges has zero length, which really
       : should't happen. If it does, just take the start of the edge
       :)
      if ($start-t = $end-t)
      then 0
      else ($t - $start-t) div ($end-t - $start-t)
    let $slices := edge:slice($edges[$edge-ix], $edge-t)
    let $edges1 := (
      $edges[position() < $edge-ix],
      if ($edge-t=0) then ()
      else if ($edge-t=1) then $slices
      else $slices[1]
    )
    let $edges2 := (
      if ($edge-t=0) then $slices
      else if ($edge-t=1) then ()
      else $slices[2],
      $edges[position() > $edge-ix]
    )
    return (
      if (exists($edges1)) then this:path($edges1) else (),
      if (exists($edges2)) then this:path($edges2) else ()
    )
  )
};

(:~
 : slice()
 : Slice the path between two cut points. Will end up with three paths in
 : general: [start, p(start-t)], [p(start-t), p(end-t)], [p(end-t), end]
 : start-t <= end-t
 :
 : If start-t=end-t this is same as slice(path, start-t)
 : If start-t or end-t is at an end will get one less edge
 :
 : @param $path: the path to cut
 : @param $start-t: fraction of path [0,1]
 : @param $end-t: fraction of path [0,1]
 :)
declare function this:slice(
  $path as map(xs:string,item()*),
  $start-t as xs:double,
  $end-t as xs:double
) as map(xs:string,item()*)*
{
  if ($start-t > $end-t) then errors:error("GEOM-BADT", ($start-t, $end-t)) else (),
  if (empty(this:edges($path))) then ()
  else if (count(this:edges($path))=1) then (
    for $edge in edge:slice(this:edges($path), $start-t, $end-t)
    return this:path($edge)
  ) else (
    if ($start-t = 1) then $path (: start-t=end-t=1 :)
    else if ($start-t = $end-t) then this:slice($path, $start-t)
    else if ($start-t = 0) then this:slice($path, $end-t)
    else (
      (:
       : end-t needs to be adjusted for the second slice:
       : Effective end-t should be fraction of length(slice) not of length(path)
       : e * l - l1 = x * l2; l1 = s * l, l2 = l - l1 = l - s * l
       : x = (el - sl) / (s - sl) = (e - s) / (1 - s)
       :)
      let $slice := this:slice($path, $start-t)
      let $eff-end-t := ($end-t - $start-t) div (1 - $start-t)
      return ($slice[1], this:slice($slice[2]=>this:with-edge-ts(), $eff-end-t))
    )
  )
};


(:~
 : path-t()
 : Determine the path t-value of the given point.
 :
 : @param $path: the path
 : @param $point: the point on the path
 :)
declare function this:path-t(
  $path as map(xs:string,item()*),
  $point as map(xs:string,item()*)
) as xs:double
{
  let $path := $path=>this:with-edge-ts()
  let $edges := this:edges($path)
  let $edge-ts := $path=>this:edge-ts()
  let $edge-ix :=
    (
      for $edge at $i in $edges
      let $intersects := $edge=>edge:edge-intersects($point)
      return (
        if ($intersects) then $i else ()
      )
    )=>head()
  let $edge-t := $edges[$edge-ix]=>edge:edge-t($point)
  let $start-t := if ($edge-ix = 1) then 0.0 else $edge-ts[$edge-ix - 1]
  let $end-t := $edge-ts[$edge-ix]
  return (
    $start-t + $edge-t * ($end-t - $start-t)
  )
};

(:~
 : tangent()
 : Return the tangent vector to the given point on the path (2D)
 :)
declare function this:tangent(
  $region as map(xs:string,item()*),
  $t as xs:double
) as map(xs:string,item()*)
{
  let $edges := this:edges($region)
  let $edge-ts := $region=>this:edge-ts()
  let $edge-ix := util:rangeindex($edge-ts, $t)
  let $start-t := if ($edge-ix = 1) then 0 else $edge-ts[$edge-ix - 1]
  let $end-t := $edge-ts[$edge-ix]
  let $edge-t :=
    (: start-t = end-t if one of the edges has zero length, which really
     : should't happen. If it does, just take the start of the edge
     :)
    if ($start-t = $end-t)
    then 0
    else ($t - $start-t) div ($end-t - $start-t)
  return (
    edge:tangent($edges[$edge-ix], $edge-t)
  )
};

(:~
 : bounding-box()
 : Minimum box surrounding the set of regions. Some approximation for non-linear
 : edges.
 :)
declare function this:bounding-box($regions as map(xs:string,item()*)*) as map(xs:string,item()*)
{
  let $boxes := (
    for $region in $regions return
    if (this:kind($region) = ("path", "polygon")) then (
      let $pts :=
        if (every $edge in this:edges($region) satisfies this:kind($edge)="edge")
        then (
          this:points($region)
        ) else (
          let $boxes := this:edges($region)!edge:bounding-box(.)
          return $boxes!box:points(.)
        )
      return (
        box:box(
          min($pts!point:px(.)), min($pts!point:py(.)),
          max($pts!point:px(.)), max($pts!point:py(.))
        )
      )
    ) else (
      errors:error("GEOM-BADREGION", ($region, "bounding-box"))
    )
  )
  return (
    if (empty($boxes)) then box:box(0,0,0,0)
    else if (empty(tail($boxes))) then $boxes
    else (
      let $pts := $boxes!box:points(.)
      return
        box:box(
          min($pts!point:px(.)), min($pts!point:py(.)),
          max($pts!point:px(.)), max($pts!point:py(.))
        )
    )
  )
};

(: TODO: more tolerance :)
(: TODO: 3D :)
declare function this:polygon-contains-point(
  $polygon as map(xs:string,item()*),
  $point as map(xs:string,item()*),
  $tolerance as xs:double
) as xs:boolean
{
  (: Special cases: point is a vertex or lies on a axis edge
   : count point as in. Non-axis edges below with winding numbers.
   :)
  (some $edge in this:edges($polygon) satisfies (
    let $v0 := edge:start($edge)
    let $v1 := edge:end($edge)
    return
      point:same($point, $v0, $tolerance) or
      point:same($point, $v1, $tolerance) or
      (
        point:px($v0)=point:px($v1) and
        point:px($v0)=point:px($point) and
        util:twixt(point:py($point), point:py($v0), point:py($v1))
      ) or
      (
        point:py($v0)=point:py($v1) and
        point:py($v0)=point:py($point) and
        util:twixt(point:px($point), point:px($v0), point:px($v1))
      )
  ))
  or
  fold-left(
    this:edges($polygon),
    0,
    function($wn as xs:integer, $edge as map(xs:string,item()*)) as xs:integer {
      let $v0 := edge:start($edge)
      let $v1 := edge:end($edge)
      let $orientation := point:sorientation($v0, $v1, $point)
      return (
        if (point:py($v0) <= point:py($point)) then (
          (: point is above start of edge :)
          if (point:py($v1) > point:py($point)) then (
            (: point is below end of edge (i.e. between start and end) :)
            (: increment winding number if it is to left :)
            if ($orientation > 0)
            then $wn + 1
            else $wn
          ) else (
            $wn
          )
        ) else (
          (: point is below start of edge :)
          if (point:py($v1) <= point:py($point)) then (
            (: point is above end of edge (i.e. between start and end) :)
            (: decrement winding number if it is to right :)
            if ($orientation <= 0)
            then $wn - 1
            else $wn
          ) else (
            $wn
          )
        )
      )
    }
  ) != 0
};

declare function this:polygon-contains-point(
  $polygon as map(xs:string,item()*),
  $point as map(xs:string,item()*)
) as xs:boolean
{
  this:polygon-contains-point($polygon, $point, $config:tolerance)
};

(:~
 : simplify()
 : Douglas-Peucker simplification. Given a path returns a path, given a
 : polygon returns a polygon.
 : https://karthaus.nl/rdp/ via Wikipedia
 :
 : @param $path-or-polygon: either a path, or a polygon
 : @param $ε: error
 :)
declare function this:simplify(
  $path-or-polygon as map(xs:string,item()*),
  $ε as xs:double
) as map(xs:string,item()*)
{
  let $pts := this:vertices($path-or-polygon)
  let $n := count($pts)
  return (
    if ($n <= 3) then $path-or-polygon
    else switch (this:kind($path-or-polygon))
    case "polygon" return this:polygon(edge:to-edges(this:simplify-points($pts, $ε)))
    case "path" return this:path(edge:to-edges(this:simplify-points($pts, $ε)))
    default return $path-or-polygon
  )
};

(:~
 : simplify-points()
 : Workhorse of simplify(): recursive Douglas-Peucker
 :)
declare function this:simplify-points(
  $pts as map(xs:string,item()*)*,
  $ε as xs:double
) as map(xs:string,item()*)*
{
  let $line := edge:edge($pts[1], $pts[last()])
  let $find-max :=
    fold-left(2 to count($pts) - 1, array{0, 1},
      function($data as array(xs:numeric), $i as xs:integer) as array(xs:numeric) {
        let $d := $line=>edge:point-distance($pts[$i])
        return (
          if ($d > $data(1)) then array {$d, $i} else $data
        )
      }
    )
  let $dmax := $find-max(1)
  let $index := $find-max(2)
  return (
    if ($dmax > $ε) then (
      this:simplify-points($pts[position() <= $index], $ε)[position() < last()],
      this:simplify-points($pts[position() > $index], $ε)
    ) else (
      $pts[1], $pts[last()]
    )
  )
};

(:~
 : chaikin-points()
 : Workhorse of Chaikin's corner-cutting smoothing algorithm
 : Don't use high iterations: you'll run out of heap and it doesn't really change things
 : much. Closed sequences will cut the corner from last to first. Open sequences
 : will leave the first and last points alone.
 :
 : @param $points: sequence of input points
 : @param $closed: closed or open?
 : @param $t: cutting ratio [0, 0.5]
 : @param $iterations: number of iterations
 :)
declare function this:chaikin-points(
  $points as map(xs:string,item()*)*,
  $closed as xs:boolean,
  $t as xs:double,
  $iterations as xs:integer
) as map(xs:string,item()*)*
{
  if (not(util:twixt($t, 0.0, 0.5))) then errors:error("ML-BADARGS", ("t", $t)) else
  if ($closed) then (
    (: Closed loop: be sure to cut corner between last and first as well :)
    fold-left(1 to $iterations, $points,
      function($points as map(xs:string,item()*)*, $i as xs:integer) as map(xs:string,item()*)* {
        if (empty($points[2])) then $points else
        let $n := count($points)
        for $i in 1 to $n return (
          (:
           : e.g. for t=0.25 (default)
           : p[i]*0.75 + p[i+1]*0.25
           : p[i]*0.25 + p[i+1]*0.75
           :)
          $points[$i]=>point:times(1 - $t)=>point:add($points[util:modix($i + 1, $n)]=>point:times($t)),
          $points[$i]=>point:times($t)=>point:add($points[util:modix($i + 1, $n)]=>point:times(1 - $t))
        )
      }
    )
  ) else (
    (: Open: leave start and end points alone :)
    fold-left(1 to $iterations, $points,
      function($points as map(xs:string,item()*)*, $i as xs:integer) as map(xs:string,item()*)* {
        if (empty($points[2])) then $points else
        let $n := count($points) return (
          $points[1],
          $points[1]=>point:times($t)=>point:add($points[2]=>point:times(1 - $t))
          ,
          for $i in 2 to $n - 2 return (
            (:
             : e.g. for t=0.25 (default)
             : p[i]*0.75 + p[i+1]*0.25
             : p[i]*0.25 + p[i+1]*0.75
             :)
            $points[$i]=>point:times(1 - $t)=>point:add($points[$i + 1]=>point:times($t)),
            $points[$i]=>point:times($t)=>point:add($points[$i + 1]=>point:times(1 - $t))
          ),
          $points[$n - 1]=>point:times(1 - $t)=>point:add($points[$n]=>point:times($t)),
          $points[$n]
        )
      }
    )
  )
};

(:~
 : chaikin()
 : Chaikin's corner-cutting algorithm.
 :
 : @param $region: path or polygon
 : @param $t: cutting ratio [0, 0.5] (default=0.25)
 : @param $iterations: number of iterations (default=1)
 :)
declare function this:chaikin(
  $region as map(xs:string,item()*),
  $t as xs:double,
  $iterations as xs:integer
) as map(xs:string,item()*)
{
  if (this:kind($region) = "polygon") then (
    this:polygon(edge:to-edges(
      this:chaikin-points(this:vertices($region), true(), $t, $iterations)
    ))
  ) else (
    this:path(edge:to-edges(
      this:chaikin-points(this:vertices($region), false(), $t, $iterations)
    ))
  )
};

declare function this:chaikin(
  $region as map(xs:string,item()*),
  $t as xs:double
) as map(xs:string,item()*)
{
  this:chaikin($region, $t, 1)
};

declare function this:chaikin(
  $region as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  this:chaikin($region, 0.25, 1)
};

(:~
 : streamline()
 : Streamline a path to remove edges less than a certain length. Can give
 : a very crude approximation where you have a lot of small edges in sequence.
 : @param $region: path or polygon
 : @param $ε: minimum edge length (default=0.6)
 : @return path or polygon with short edges elided
 :)
declare function this:streamline(
  $region as map(xs:string,item()*),
  $ε as xs:double
) as map(xs:string,item()*)
{
  let $edges := this:edges($region)
  let $good-edges := $edges[edge:length(.) >= $ε]
  let $n-edges := count($edges)
  let $n-good := count($good-edges)
  return (
    (: util:log("ep="||$ε||" ce="||$n-edges||" ge="||$n-good), :)
    if ($ε <= 0) then $region
    else if ($n-edges <= 3) then $region
    else if ($n-good < 3) then this:streamline($region, $ε div 2)
    else if ($n-edges=$n-good) then $region
    else (
      let $new-edges := (
        for $i in 1 to $n-good - 1 return (
          if (point:distance(edge:end($good-edges[$i]), edge:start($good-edges[$i + 1])) > 0) then (
            edge:edge(edge:start($good-edges[$i]), edge:start($good-edges[$i + 1]))
          ) else (
            $good-edges[$i]
          )
        ),
        $good-edges[last()]
      )
      return (
        if (this:kind($region)="path") then (
          this:path($new-edges, this:property-map($region))
        ) else if (this:kind($region)="polygon") then (
          this:polygon($new-edges, this:property-map($region))
        ) else (
          errors:error("GEOM-BADREGION", ($region, "streamline-path"))
        )
      )
    )
  )
};

declare function this:streamline(
  $region as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  this:streamline($region, 0.6)
};

(:~
 : consolidate()
 : Consolidate a collection of paths and polygons into a single polygon with
 : some goto edges. Makes for more efficient rendering. Consolidatation is
 : by the "colour" and "width" properties. Other properties will be lost.
 :
 : @param $paths: paths or polygons
 : @return consolidated polygons
 :)
declare function this:consolidate(
  $paths as map(xs:string,item()*)*
) as map(xs:string,item()*)*
{
  let $colours := distinct-values($paths!(.("colour")))
  let $widths := distinct-values($paths!(.("width")))
  return (
    (: Consolidate those with both width and colour :)
    for $colour in $colours
    for $width in $widths
    let $relevant := $paths[.("colour")=$colour and .("width")=$widths]
    where exists($relevant)
    return (
      this:polygon((
        this:edges(head($relevant)),
        for $t at $i in tail($relevant) return (
          edge:skip(this:end($relevant[$i]), this:start($t)),
          this:edges($t)
        ),
        edge:skip(this:end($relevant[last()]), this:start($relevant[1]))
      ))=>this:with-properties(map {"colour": $colour, "width": $width})
    ),
    (: Consolidate by colour for those with no width :)
    for $colour in $colours
    let $relevant := $paths[.("colour")=$colour and empty(.("width"))]
    where exists($relevant)
    return (
      this:polygon((
        this:edges(head($relevant)),
        for $t at $i in tail($relevant) return (
          edge:skip(this:end($relevant[$i]), this:start($t)),
          this:edges($t)
        ),
        edge:skip(this:end($relevant[last()]), this:start($relevant[1]))
      ))=>this:with-properties(map {"colour": $colour})
    ),
    (: Consolidate by width for those with no colour :)
    for $width in $widths
    let $relevant := $paths[.("width")=$width and empty(.("colour"))]
    where exists($relevant)
    return (
      this:polygon((
        this:edges(head($relevant)),
        for $t at $i in tail($relevant) return (
          edge:skip(this:end($relevant[$i]), this:start($t)),
          this:edges($t)
        ),
        edge:skip(this:end($relevant[last()]), this:start($relevant[1]))
      ))=>this:with-properties(map {"width": $width})
    ),
    (: Consolidate those with no width or colour :)
    let $relevant := $paths[empty(.("width")) and empty(.("colour"))]
    where exists($relevant)
    return (
      this:polygon((
        this:edges(head($relevant)),
        for $t at $i in tail($relevant) return (
          edge:skip(this:end($relevant[$i]), this:start($t)),
          this:edges($t)
        ),
        edge:skip(this:end($relevant[last()]), this:start($relevant[1]))
      ))
    )
  )
};

(:~
 : consolidate-by-colour()
 : Consolidate a collection of paths and polygons into a single polygon with
 : some goto edges. Makes for more efficient rendering. Consolidatation is
 : by the "colour" property. Other properties will be lost.
 :
 : @param $paths: paths or polygons
 : @return consolidated polygons
 :)
declare function this:consolidate-by-colour(
  $paths as map(xs:string,item()*)*
) as map(xs:string,item()*)*
{
  let $colours := distinct-values($paths!(.("colour")))
  return (
    (: Consolidate those with colour :)
    for $colour in $colours
    let $relevant := $paths[.("colour")=$colour]
    return (
      if (exists($relevant)) then (
        this:polygon((
          this:edges(head($relevant)),
          for $t at $i in tail($relevant) return (
            edge:skip(this:end($relevant[$i]), this:start($t)),
            this:edges($t)
          ),
          edge:skip(this:end($relevant[last()]), this:start($relevant[1]))
        ))=>this:with-properties(map {"colour": $colour})
      ) else ()
    ),
    (: Consolidate those with no colour :)
    let $relevant := $paths[empty(.("colour"))]
    return (
      if (exists($relevant)) then (
        this:polygon((
          this:edges(head($relevant)),
          for $t at $i in tail($relevant) return (
            edge:skip(this:end($relevant[$i]), this:start($t)),
            this:edges($t)
          ),
          edge:skip(this:end($relevant[last()]), this:start($relevant[1]))
        ))
      ) else ()
    )
  )
};

(:~
 : consolidate-by-width()
 : Consolidate a collection of paths and polygons into a single polygon with
 : some goto edges. Makes for more efficient rendering. Consolidatation is
 : by the "width" property. Other properties will be lost.
 :
 : @param $paths: paths or polygons
 : @return consolidated polygons
 :)
declare function this:consolidate-by-width(
  $paths as map(xs:string,item()*)*
) as map(xs:string,item()*)*
{
  let $widths := distinct-values($paths!(.("width")))
  return (
    (: Consolidate those with width :)
    for $width in $widths
    let $relevant := $paths[.("width")=$width]
    where exists($relevant)
    return (
      this:polygon((
        this:edges(head($relevant)),
        for $t at $i in tail($relevant) return (
          edge:skip(this:end($relevant[$i]), this:start($t)),
          this:edges($t)
        ),
        edge:skip(this:end($relevant[last()]), this:start($relevant[1]))
      ))=>this:with-properties(map {"width": $width})
    ),
    (: Consolidate those with no width :)
    let $relevant := $paths[empty(.("width"))]
    where exists($relevant)
    return (
      this:polygon((
        this:edges(head($relevant)),
        for $t at $i in tail($relevant) return (
          edge:skip(this:end($relevant[$i]), this:start($t)),
          this:edges($t)
        ),
        edge:skip(this:end($relevant[last()]), this:start($relevant[1]))
      ))
    )
  )
};

(:~
 : consolidate-points()
 : Consolidate a collection of points into a single path with
 : some goto edges. Makes for more efficient rendering.
 :
 : @param $points: points
 : @return consolidated path
 :)
declare function this:consolidate-points(
  $points as map(xs:string,item()*)*,
  $width as xs:double
) as map(xs:string,item()*)
{
  if (empty($points)) then this:path(()) else
  let $points := point:distinct($points) return (
    this:path((
      edge:edge($points[1], point:point(point:px($points[1])+$width div 2, point:py($points[1]))),
      for $i in 2 to count($points) return (
        edge:skip($points[$i - 1], $points[$i]),
        edge:edge($points[$i], point:point(point:px($points[$i])+$width div 2, point:py($points[$i])))
      )
    ))=>this:with-properties(map {"width": $width, "stroke-linecap":"round"})
  )
};

(:~
 : consolidate-points-by-colour()
 : Consolidate a collection of points into a single path for each colour
 : using goto edges. Makes for more efficient rendering.
 :
 : @param $points: points
 : @return consolidated path
 :)
declare function this:consolidate-points-by-colour(
  $points as map(xs:string,item()*)*,
  $width as xs:double
) as map(xs:string,item()*)*
{
  let $colours := distinct-values($points!.("colour"))
  return (
    (: Consolidate those with colours :)
    for $colour in $colours
    let $relevant := point:distinct($points[.("colour")=$colour])
    where exists($relevant)
    return (
      this:path((
        edge:edge($relevant[1], point:point(point:px($relevant[1])+$width div 2, point:py($relevant[1]))),
        for $i in 2 to count($relevant) return (
          edge:skip($relevant[$i - 1], $relevant[$i]),
          edge:edge($relevant[$i], point:point(point:px($relevant[$i])+$width div 2, point:py($relevant[$i])))
        )
      ))=>this:with-properties(map {"width": $width, "stroke-linecap":"round", "colour": $colour})
    ),
    (: consolidate those with no colour :)
    let $relevant := $points[empty(.("colour"))]
    where exists($relevant)
    return (
      this:path((
        edge:edge($relevant[1], point:point(point:px($relevant[1])+$width div 2, point:py($relevant[1]))),
        for $i in 2 to count($relevant) return (
          edge:skip($relevant[$i - 1], $relevant[$i]),
          edge:edge($relevant[$i], point:point(point:px($relevant[$i])+$width div 2, point:py($relevant[$i])))
        )
      ))=>this:with-properties(map {"width": $width, "stroke-linecap":"round"})
    )
  )
};

(:~
 : consolidate-points-by-colour-and-opacity()
 : Consolidate a collection of points into a single path for each combination
 : of colour and opacity using goto edges. Makes for more efficient rendering.
 :
 : @param $points: points
 : @return consolidated path
 :)
declare function this:consolidate-points-by-colour-and-opacity(
  $points as map(xs:string,item()*)*,
  $width as xs:double
) as map(xs:string,item()*)*
{
  let $colours := distinct-values($points!.("colour"))
  let $opacities := distinct-values($points!.("opacity"))
  return (
    (: Consolidate those with colours and opacities :)
    for $colour in $colours
    for $opacity in $opacities
    let $relevant := point:distinct($points[.("colour")=$colour and .("opacity")=$opacity])
    where exists($relevant)
    return (
      this:path((
        edge:edge($relevant[1], point:point(point:px($relevant[1])+$width div 2, point:py($relevant[1]))),
        for $i in 2 to count($relevant) return (
          edge:skip($relevant[$i - 1], $relevant[$i]),
          edge:edge($relevant[$i], point:point(point:px($relevant[$i])+$width div 2, point:py($relevant[$i])))
        )
      ))=>this:with-properties(
        map {
          "width": $width,
          "stroke-linecap":"round",
          "colour": $colour,
          "stroke-opacity": $opacity
        })
    ),
    (: consolidate those with no colour :)
    for $opacity in $opacities
    let $relevant := $points[empty(.("colour")) and .("opacity")=$opacity]
    where exists($relevant)
    return (
      this:path((
        edge:edge($relevant[1], point:point(point:px($relevant[1])+$width div 2, point:py($relevant[1]))),
        for $i in 2 to count($relevant) return (
          edge:skip($relevant[$i - 1], $relevant[$i]),
          edge:edge($relevant[$i], point:point(point:px($relevant[$i])+$width div 2, point:py($relevant[$i])))
        )
      ))=>this:with-properties(
        map {
          "width": $width,
          "stroke-linecap":"round",
          "stroke-opacity": $opacity
        }
      )
    ),
    (: consolidate those with no opacity :)
    for $colour in $colours
    let $relevant := $points[.("colour")=$colour and empty(.("opacity"))]
    where exists($relevant)
    return (
      this:path((
        edge:edge($relevant[1], point:point(point:px($relevant[1])+$width div 2, point:py($relevant[1]))),
        for $i in 2 to count($relevant) return (
          edge:skip($relevant[$i - 1], $relevant[$i]),
          edge:edge($relevant[$i], point:point(point:px($relevant[$i])+$width div 2, point:py($relevant[$i])))
        )
      ))=>this:with-properties(
        map {
          "width": $width,
          "stroke-linecap":"round",
          "colour": $colour
        }
      )
    ),
    (: consolidate those with no colour or opacity :)
    let $relevant := $points[empty(.("colour")) and empty(.("opacity"))]
    where exists($relevant)
    return (
      this:path((
        edge:edge($relevant[1], point:point(point:px($relevant[1])+$width div 2, point:py($relevant[1]))),
        for $i in 2 to count($relevant) return (
          edge:skip($relevant[$i - 1], $relevant[$i]),
          edge:edge($relevant[$i], point:point(point:px($relevant[$i])+$width div 2, point:py($relevant[$i])))
        )
      ))=>this:with-properties(
        map {
          "width": $width,
          "stroke-linecap":"round"
        }
      )
    )
  )
};

declare %private variable $this:draw-path-impl as function(*) :=
  if ($config:DRAWING-METHOD="art") then (
    if ($config:DRAW-SNAPPED) then (
      function($item as map(xs:string,item()*), $properties as map(xs:string,item()*), $drawing as map(xs:string,item()*)) as item()* {
        if (empty(this:edges($item))) then () else
        let $style-properties :=
          util:merge-into(($properties, util:exclude(this:property-map($item), "type")))
        let $edges := this:snap($item)=>this:edges()
        let $start := $edges[1]=>edge:start()
        let $type := ($item=>map:get("type"),"")[1]
        return (
          if ($type="stroke") then (
            <art:stroke-path>{
              $drawing("draw:as-attributes")($style-properties),
              if ($config:DENSE-EDGES) then (
                let $path :=
                  edge:map-command('goto','absolute')||" "||
                  point:px($start)||" "||
                  point:py($start)||" "||
                  string-join(
                    for $edge in $edges return edge:translate-edge($edge),
                    " "
                  )
                return attribute d {$path}
              ) else (
                $drawing("draw:draw")($edges, map {}, $drawing) (: Path gets style properties :)
              )
            }</art:stroke-path>
          ) else (
            <art:path x="{point:px($start)}" y="{point:py($start)}">{
              if ($start=>point:dimension()>2)
              then attribute z {point:pz($start)}
              else (),
              $drawing("draw:as-attributes")($style-properties),
              if ($config:DENSE-EDGES) then (
                let $path :=
                  edge:map-command('goto','absolute')||" "||
                  point:x($start)||" "||
                  point:y($start)||" "||
                  string-join(
                    for $edge in $edges return edge:translate-edge($edge),
                    " "
                  )
                return attribute d {$path}
              ) else (
                $drawing("draw:draw")($edges, map {}, $drawing) (: Path gets style properties :)
              )
            }</art:path>
          )
        )
      }
    ) else (
      function($item as map(xs:string,item()*), $properties as map(xs:string,item()*), $drawing as map(xs:string,item()*)) as item()* {
        if (empty(this:edges($item))) then () else
        let $style-properties :=
          util:merge-into(($properties, util:exclude(this:property-map($item), "type")))
        let $edges := $item=>this:edges()
        let $start := $edges[1]=>edge:start()
        let $type := ($item=>map:get("type"),"")[1]
        return (
          if ($type="stroke") then (
            <art:stroke-path>{
              $drawing("draw:as-attributes")($style-properties),
              if ($config:DENSE-EDGES) then (
                let $path :=
                  edge:map-command('goto','absolute')||" "||
                  point:px($start)||" "||
                  point:py($start)||" "||
                  string-join(
                    for $edge in $edges return edge:translate-edge($edge),
                    " "
                  )
                return attribute d {$path}
              ) else (
                $drawing("draw:draw")($edges, map {}, $drawing) (: Path gets style properties :)
              )
            }</art:stroke-path>
          ) else (
            <art:path x="{point:px($start)}" y="{point:py($start)}">{
              if ($start=>point:dimension()>2)
              then attribute z {point:pz($start)}
              else (),
              $drawing("draw:as-attributes")($style-properties),
              if ($config:DENSE-EDGES) then (
                let $path :=
                  edge:map-command('goto','absolute')||" "||
                  point:px($start)||" "||
                  point:py($start)||" "||
                  string-join(
                    for $edge in $edges return edge:translate-edge($edge),
                    " "
                  )
                return attribute d {$path}
              ) else (
                $drawing("draw:draw")($edges, map {}, $drawing) (: Path gets style properties :)
              )
            }</art:path>
          )
        )
      }
    )
  ) else (
    if ($config:DRAW-SNAPPED) then (
      function($item as map(xs:string,item()*), $properties as map(xs:string,item()*), $drawing as map(xs:string,item()*)) as item()* {
        if (empty(this:edges($item))) then () else
        let $style-properties :=
          util:merge-into(($drawing("draw:svg-style")($properties), $drawing("draw:svg-style")(this:property-map($item))))
        let $start := this:start($item)
        let $path :=
          edge:map-command('goto','absolute')||" "||
          point:px($start)||" "||
          point:py($start)||" "||
          string-join(
            for $edge in this:edges($item) return edge:translate-edge($edge),
            " "
          )
        return (
          <svg:path d="{$path}">{
            $drawing("draw:as-attributes")($style-properties)
          }</svg:path>
        )
      }
    ) else (
      function($item as map(xs:string,item()*), $properties as map(xs:string,item()*), $drawing as map(xs:string,item()*)) as item()* {
        if (empty(this:edges($item))) then () else
        let $style-properties :=
          util:merge-into(($drawing("draw:svg-style")($properties), $drawing("draw:svg-style")(this:property-map($item))))
        let $start := this:start($item)
        let $path :=
          edge:map-command('goto','absolute')||" "||
          point:x($start)||" "||
          point:y($start)||" "||
          string-join(
            for $edge in this:edges(this:snap($item)) return edge:translate-edge($edge),
            " "
          )
        return (
          <svg:path d="{$path}">{
            $drawing("draw:as-attributes")($style-properties)
          }</svg:path>
        )
      }
    )
  )
;

declare %private variable $this:draw-polygon-impl as function(*) :=
  if ($config:DRAWING-METHOD="art") then (
    if ($config:DRAW-SNAPPED) then (
      function($item as map(xs:string,item()*), $properties as map(xs:string,item()*), $drawing as map(xs:string,item()*)) as item()* {
        if (empty(this:edges($item))) then () else
        let $style-properties :=
          util:merge-into(($properties, util:exclude(this:property-map($item), "type")))
        let $edges := this:snap($item)=>this:edges()
        let $start := $edges[1]=>edge:start()
        let $type := ($item=>map:get("type"),"")[1]
        return (
          if ($type="stroke") then (
            <art:stroke-path>{
              $drawing("draw:as-attributes")($style-properties),
              if ($config:DENSE-EDGES) then (
                let $path :=
                  edge:map-command('goto','absolute')||" "||
                  point:x($start)||" "||
                  point:y($start)||" "||
                  string-join(
                    for $edge in $edges return edge:translate-edge($edge),
                    " "
                  )
                return attribute d {$path}
              ) else (
                $drawing("draw:draw")($edges, map {}, $drawing) (: Polygon gets style properties :)
              )
            }</art:stroke-path>
          ) else (
            <art:polygon>{
              $drawing("draw:as-attributes")($style-properties),
              if ($config:DENSE-EDGES) then (
                let $path :=
                  edge:map-command('goto','absolute')||" "||
                  point:x($start)||" "||
                  point:y($start)||" "||
                  string-join(
                    for $edge in $edges return edge:translate-edge($edge),
                    " "
                  )
                return attribute d {$path}
              ) else (
                $drawing("draw:draw")($edges, map {}, $drawing) (: Polygon gets style properties :)
              )
            }</art:polygon>
          )
        )
      }
    ) else (
      function($item as map(xs:string,item()*), $properties as map(xs:string,item()*), $drawing as map(xs:string,item()*)) as item()* {
        if (empty(this:edges($item))) then () else
        let $style-properties :=
          util:merge-into(($properties, util:exclude(this:property-map($item), "type")))
        let $edges := $item=>this:edges()
        let $start := $edges[1]=>edge:start()
        let $type := ($item=>map:get("type"),"")[1]
        return (
          if ($type="stroke") then (
            <art:stroke-path>{
              $drawing("draw:as-attributes")($style-properties),
              if ($config:DENSE-EDGES) then (
                let $path :=
                  edge:map-command('goto','absolute')||" "||
                  point:px($start)||" "||
                  point:py($start)||" "||
                  string-join(
                    for $edge in $edges return edge:translate-edge($edge),
                    " "
                  )
                return attribute d {$path}
              ) else (
                $drawing("draw:draw")($edges, map {}, $drawing) (: Polygon gets style properties :)
              )
            }</art:stroke-path>
          ) else (
            <art:polygon>{
              $drawing("draw:as-attributes")($style-properties),
              if ($config:DENSE-EDGES) then (
                let $path :=
                  edge:map-command('goto','absolute')||" "||
                  point:px($start)||" "||
                  point:py($start)||" "||
                  string-join(
                    for $edge in $edges return edge:translate-edge($edge),
                    " "
                  )
                return attribute d {$path}
              ) else (
                $drawing("draw:draw")($edges, map {}, $drawing) (: Polygon gets style properties :)
              )
            }</art:polygon>
          )
        )
      }
    )
  ) else (
    if ($config:DRAW-SNAPPED) then (
      function($item as map(xs:string,item()*), $properties as map(xs:string,item()*), $drawing as map(xs:string,item()*)) as item()* {
        if (empty(this:edges($item))) then () else
        let $style-properties :=
          util:merge-into(($drawing("draw:svg-style")($properties), $drawing("draw:svg-style")(this:property-map($item))))
        let $start := this:start($item)
        let $path :=
          edge:map-command('goto','absolute')||" "||
          point:x($start)||" "||
          point:y($start)||" "||
          string-join(
            for $edge in this:edges(this:snap($item)) return edge:translate-edge($edge),
            " "
          )||"Z"
        return (
          <svg:path d="{$path}">{
            $drawing("draw:as-attributes")($style-properties)
          }</svg:path>
        )
      }
    ) else (
      function($item as map(xs:string,item()*), $properties as map(xs:string,item()*), $drawing as map(xs:string,item()*)) as item()* {
        if (empty(this:edges($item))) then () else
        let $style-properties :=
          util:merge-into(($drawing("draw:svg-style")($properties), $drawing("draw:svg-style")(this:property-map($item))))
        let $start := this:start($item)
        let $path :=
          edge:map-command('goto','absolute')||" "||
          point:px($start)||" "||
          point:py($start)||" "||
          string-join(
            for $edge in this:edges($item) return edge:translate-edge($edge),
            " "
          )||"Z"
        return (
          <svg:path d="{$path}">{
            $drawing("draw:as-attributes")($style-properties)
          }</svg:path>
        )
      }
    )
  )
;

declare function this:draw(
  $item as map(xs:string,item()*),
  $properties as map(xs:string,item()*),
  $drawing as map(xs:string,function(*)?)
) as item()*
{
  switch(this:kind($item))
  case "path" return $this:draw-path-impl($item, $properties, $drawing)
  case "polygon" return $this:draw-polygon-impl($item, $properties, $drawing)
  default return ()
};