Created
August 18, 2021 15:17
-
-
Save cranst0n/1f14309690a82912396fc4e42ab713d3 to your computer and use it in GitHub Desktop.
Skunk PostGIS codecs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Copyright (c) 2018-2021 by Rob Norris | |
| // This software is licensed under the MIT License (MIT). | |
| // For more information see LICENSE or https://opensource.org/licenses/MIT | |
| package skunk | |
| package codec | |
| import cats.Eq | |
| import cats.syntax.all._ | |
| import skunk.data.Type | |
| final case class Point(x: Double, y: Double) | |
| object Point { | |
| implicit val eq: Eq[Point] = Eq.fromUniversalEquals[Point] | |
| } | |
| final case class Line(a: Double, b: Double, c: Double) | |
| object Line { | |
| implicit val eq: Eq[Line] = Eq.fromUniversalEquals[Line] | |
| } | |
| final case class LineSegment(a: Point, b: Point) | |
| object LineSegment { | |
| implicit val eq: Eq[LineSegment] = Eq.fromUniversalEquals[LineSegment] | |
| } | |
| final case class Box private (upperRight: Point, lowerLeft: Point) | |
| object Box { | |
| // Points are reordered such that the upper right comes first, lower left next. | |
| def apply(x1: Double, y1: Double, x2: Double, y2: Double): Box = | |
| Box(Point(x1.min(x2), y1.min(y2)), Point(x1.max(x2), y1.max(y2))) | |
| implicit val eq: Eq[Box] = Eq.fromUniversalEquals[Box] | |
| } | |
| sealed abstract class Path { | |
| def points: List[Point] | |
| def isOpen: Boolean | |
| def isClosed = !isOpen | |
| } | |
| object Path { | |
| final case class Open(override val points: List[Point]) extends Path { | |
| override val isOpen: Boolean = true | |
| } | |
| final case class Closed(override val points: List[Point]) extends Path { | |
| override val isOpen: Boolean = false | |
| } | |
| implicit val eq: Eq[Path] = new Eq[Path] { | |
| override def eqv(x: Path, y: Path): Boolean = { | |
| (x, y) match { | |
| case (a: Open, b: Open) => a == b | |
| case (a: Closed, b: Closed) => a == b | |
| case _ => false | |
| } | |
| } | |
| } | |
| } | |
| final case class Polygon(points: List[Point]) | |
| object Polygon { | |
| implicit val eq: Eq[Polygon] = Eq.fromUniversalEquals[Polygon] | |
| } | |
| final case class Circle(center: Point, radius: Double) | |
| object Circle { | |
| implicit val eq: Eq[Circle] = Eq.fromUniversalEquals[Circle] | |
| } | |
| ////////////////////////////////////////////////////////////////////////////////////////// | |
| // Codec specs: https://www.postgresql.org/docs/current/datatype-geometric.html#AEN6990 // | |
| ////////////////////////////////////////////////////////////////////////////////////////// | |
| trait PostGISCodecs { | |
| private implicit class RemoveOps(str: String) { | |
| def removeAll(chars: Char*) = | |
| str.filterNot(c => chars.exists(_ == c)) | |
| } | |
| // ( x , y ) | |
| val point: Codec[Point] = Codec.simple[Point]( | |
| point => s"( ${point.x}, ${point.y} )", | |
| str => | |
| str.removeAll('(', ')').split(",") match { | |
| case Array(x, y) => Point(x.toDouble, y.toDouble).asRight[String] | |
| case _ => "[point] split failed".asLeft[Point] | |
| }, | |
| Type.point | |
| ) | |
| // { A, B, C } | |
| val line: Codec[Line] = Codec.simple[Line]( | |
| line => s"{ ${line.a}, ${line.b}, ${line.c} }", | |
| str => | |
| str.removeAll('{', '}').split(",") match { | |
| case Array(a, b, c) => | |
| Line(a.toDouble, b.toDouble, c.toDouble).asRight[String] | |
| case _ => "[line] split failed".asLeft[Line] | |
| }, | |
| Type.line | |
| ) | |
| // [ ( x1 , y1 ) , ( x2 , y2 ) ] | |
| val lseg: Codec[LineSegment] = Codec.simple[LineSegment]( | |
| segment => | |
| s"[ ( ${segment.a.x}, ${segment.a.y} ), ( ${segment.b.x}, ${segment.b.y} ) ]", | |
| str => | |
| str.removeAll('[', '(', ')', ']').split(",") match { | |
| case Array(ax, ay, bx, by) => | |
| LineSegment( | |
| Point(ax.toDouble, ay.toDouble), | |
| Point(bx.toDouble, by.toDouble) | |
| ).asRight[String] | |
| case _ => "[lseg] split failed".asLeft[LineSegment] | |
| }, | |
| Type.lseg | |
| ) | |
| // ( x1 , y1 ) , ( x2 , y2 ) | |
| val box: Codec[Box] = Codec.simple[Box]( | |
| box => | |
| s"( ${box.upperRight.x}, ${box.upperRight.y} ), ( ${box.lowerLeft.x}, ${box.lowerLeft.y} )", | |
| str => | |
| str.removeAll('(', ')').split(",") match { | |
| case Array(ax, ay, bx, by) => | |
| Box( | |
| ax.toDouble, | |
| ay.toDouble, | |
| bx.toDouble, | |
| by.toDouble | |
| ).asRight[String] | |
| case _ => "[box] split failed".asLeft[Box] | |
| }, | |
| Type.box | |
| ) | |
| // Open: [ ( x1 , y1 ) , ... , ( xn , yn ) ] | |
| // Closed: ( ( x1 , y1 ) , ... , ( xn , yn ) ) | |
| val path: Codec[Path] = Codec.simple[Path]( | |
| path => { | |
| val (leading, trailing) = if (path.isOpen) ("[", "]") else ("(", ")") | |
| path.points.map(p => s"${p.x},${p.y}").mkString(leading, ",", trailing) | |
| }, | |
| str => { | |
| str.removeAll('[', '(', ')', ']').split(",") match { | |
| case parts if parts.length % 2 == 0 => { | |
| val ctor: List[Point] => Path = | |
| if (str.contains("[")) Path.Open.apply else Path.Closed.apply | |
| ctor( | |
| parts | |
| .sliding(2, 2) | |
| .map(arr => Point(arr(0).toDouble, arr(1).toDouble)) | |
| .toList | |
| ).asRight[String] | |
| } | |
| case _ => "[path] split failed".asLeft[Path] | |
| } | |
| }, | |
| Type.path | |
| ) | |
| // String representation is same as closed path | |
| // ( ( x1 , y1 ) , ... , ( xn , yn ) ) | |
| val polygon = | |
| path.imap[Polygon](path => Polygon(path.points))(poly => | |
| Path.Closed(poly.points) | |
| ) | |
| // < ( x , y ) , r > | |
| val circle: Codec[Circle] = Codec.simple[Circle]( | |
| circle => | |
| s"< ( ${circle.center.x}, ${circle.center.y} ), ${circle.radius} >", | |
| str => | |
| str.removeAll('<', '(', ')', '>').split(",") match { | |
| case Array(x, y, r) => | |
| Circle(Point(x.toDouble, y.toDouble), r.toDouble).asRight[String] | |
| case _ => "[circle] split failed".asLeft[Circle] | |
| }, | |
| Type.circle | |
| ) | |
| } | |
| object postgis extends PostGISCodecs |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Copyright (c) 2018-2021 by Rob Norris | |
| // This software is licensed under the MIT License (MIT). | |
| // For more information see LICENSE or https://opensource.org/licenses/MIT | |
| package tests | |
| package codec | |
| import skunk.codec.all._ | |
| import skunk.codec._ | |
| /** Test that we can round=trip values via codecs. */ | |
| class PostGISCodecTest extends CodecTest { | |
| val GISBox = skunk.codec.Box | |
| roundtripTest(point)( | |
| Point(Double.MinValue, Double.MinValue), | |
| Point(0.0, 0.0), | |
| Point(Double.MaxValue, Double.MaxValue) | |
| ) | |
| roundtripTest(line)( | |
| Line(Double.MinValue, Double.MinValue, Double.MinValue), | |
| Line(0.1, -2.3, 0.0), | |
| Line(Double.MaxValue, Double.MaxValue, Double.MaxValue) | |
| ) | |
| roundtripTest(lseg)( | |
| LineSegment( | |
| Point(Double.MinValue, Double.MinValue), | |
| Point(Double.MaxValue, Double.MaxValue) | |
| ) | |
| ) | |
| roundtripTest(box)( | |
| GISBox(Double.MinValue, Double.MinValue, Double.MaxValue, Double.MaxValue), | |
| GISBox(Double.MaxValue, Double.MinValue, Double.MinValue, Double.MaxValue), | |
| GISBox(Double.MinValue, Double.MaxValue, Double.MaxValue, Double.MinValue) | |
| ) | |
| roundtripTest(path)( | |
| Path.Open(List.tabulate(10)(ix => Point(ix.toDouble, ix.toDouble))), | |
| Path.Closed(List.tabulate(10)(ix => Point(ix.toDouble, ix.toDouble))) | |
| ) | |
| roundtripTest(polygon)( | |
| Polygon(List.tabulate(10)(ix => Point(ix.toDouble, ix.toDouble))), | |
| Polygon(List.tabulate(10)(ix => Point(ix.toDouble, ix.toDouble))) | |
| ) | |
| roundtripTest(circle)( | |
| Circle(Point(0.0, 0.0), 1.0) | |
| ) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment