Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backport #534 (PostGIS support) to series/0.6.x #1029

Merged
merged 4 commits into from
Jan 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,11 @@ jobs:

- name: Make target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/series/0.6.x')
run: mkdir -p modules/circe/.jvm/target unidocs/target modules/core/native/target modules/core/js/target modules/circe/.js/target modules/core/jvm/target modules/refined/.native/target modules/refined/.js/target modules/refined/.jvm/target modules/circe/.native/target project/target
run: mkdir -p modules/circe/.jvm/target unidocs/target modules/core/native/target modules/core/js/target modules/postgis/.native/target modules/circe/.js/target modules/postgis/.js/target modules/core/jvm/target modules/refined/.native/target modules/refined/.js/target modules/refined/.jvm/target modules/circe/.native/target modules/postgis/.jvm/target project/target

- name: Compress target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/series/0.6.x')
run: tar cf targets.tar modules/circe/.jvm/target unidocs/target modules/core/native/target modules/core/js/target modules/circe/.js/target modules/core/jvm/target modules/refined/.native/target modules/refined/.js/target modules/refined/.jvm/target modules/circe/.native/target project/target
run: tar cf targets.tar modules/circe/.jvm/target unidocs/target modules/core/native/target modules/core/js/target modules/postgis/.native/target modules/circe/.js/target modules/postgis/.js/target modules/core/jvm/target modules/refined/.native/target modules/refined/.js/target modules/refined/.jvm/target modules/circe/.native/target modules/postgis/.jvm/target project/target

- name: Upload target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/series/0.6.x')
Expand Down
22 changes: 19 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ lazy val commonSettings = Seq(

lazy val skunk = tlCrossRootProject
.settings(name := "skunk")
.aggregate(core, tests, circe, refined, example, unidocs)
.aggregate(core, tests, circe, refined, postgis, example, unidocs)
.settings(commonSettings)

lazy val core = crossProject(JVMPlatform, JSPlatform, NativePlatform)
Expand Down Expand Up @@ -161,10 +161,27 @@ lazy val circe = crossProject(JVMPlatform, JSPlatform, NativePlatform)
)
)

lazy val postgis = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.crossType(CrossType.Pure)
.in(file("modules/postgis"))
.dependsOn(core)
.enablePlugins(AutomateHeaderPlugin)
.settings(commonSettings)
.settings(
name := "skunk-postgis",
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-parse" % "1.0.0"
),
mimaPreviousArtifacts := mimaPreviousArtifacts.value.filter { artifact =>
VersionNumber(artifact.revision).matchesSemVer(SemanticSelector(">0.6.3"))
},
mimaFailOnNoPrevious := false
)

lazy val tests = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.crossType(CrossType.Full)
.in(file("modules/tests"))
.dependsOn(core, circe)
.dependsOn(core, circe, postgis)
.enablePlugins(AutomateHeaderPlugin, NoPublishPlugin)
.settings(commonSettings)
.settings(
Expand Down Expand Up @@ -262,4 +279,3 @@ lazy val docs = project
)

// ci

2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: '3'
services:
# main instance for testing
postgres:
image: postgres:11
image: postgis/postgis:11-3.3
volumes:
- ./world/fix_perms.sh:/docker-entrypoint-initdb.d/fix_perms.sh
- ./world/world.sql:/docker-entrypoint-initdb.d/world.sql
Expand Down
18 changes: 18 additions & 0 deletions modules/postgis/src/main/scala-2/ewkb/platform.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// 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.postgis
package ewkb

import scodec.Attempt
import scodec.{Codec => Scodec}
import scodec.bits.BitVector
import scodec.bits.ByteOrdering

trait EWKBCodecPlatform extends EWKBPrimitives {
def ewkbEncode(ewkb: EWKBType, geometry: Geometry, geoEncoder: Scodec[Geometry])(implicit bo: ByteOrdering): Attempt[BitVector] = {
val encoder = byteOrdering :: ewkbType :: geoEncoder
encoder.encode(bo :: ewkb :: geometry :: shapeless.HNil)
}
}
18 changes: 18 additions & 0 deletions modules/postgis/src/main/scala-3/ewkb/platform.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// 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.postgis
package ewkb

import scodec.Attempt
import scodec.{Codec => Scodec}
import scodec.bits.BitVector
import scodec.bits.ByteOrdering

trait EWKBCodecPlatform extends EWKBPrimitives {
def ewkbEncode(ewkb: EWKBType, geometry: Geometry, geoEncoder: Scodec[Geometry])(using bo: ByteOrdering): Attempt[BitVector] = {
val encoder = byteOrdering :: ewkbType :: geoEncoder
encoder.encode((bo, ewkb, geometry))
}
}
45 changes: 45 additions & 0 deletions modules/postgis/src/main/scala/codecs.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// 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 postgis
package codecs

import scala.reflect.ClassTag

import cats.syntax.all._
import scodec.bits.ByteVector
import skunk.data.Type

trait PostGISGeometryCodecs {

val geometry: Codec[Geometry] = Codec.simple[Geometry](
geometry => ewkb.codecs.geometry.encode(geometry).require.toHex,
str => {
ByteVector.fromHex(str).fold(s"[postgis] Bad EWKB Hex: $str".asLeft[Geometry]) { byteVector =>
ewkb.codecs.geometry.decodeValue(byteVector.toBitVector).toEither.leftMap(_.message)
}
},
Type("geometry")
)

val point: Codec[Point] = geometryCodec[Point]
val lineString: Codec[LineString] = geometryCodec[LineString]
val polygon: Codec[Polygon] = geometryCodec[Polygon]
val multiPoint: Codec[MultiPoint] = geometryCodec[MultiPoint]
val multiLineString: Codec[MultiLineString] = geometryCodec[MultiLineString]
val multiPolygon: Codec[MultiPolygon] = geometryCodec[MultiPolygon]
val geometryCollection: Codec[GeometryCollection] = geometryCodec[GeometryCollection]

private def geometryCodec[A >: Null <: Geometry](implicit A: ClassTag[A]): Codec[A] = {
geometry.imap[A](geom => A.runtimeClass.cast(geom).asInstanceOf[A])(o =>
o.asInstanceOf[Geometry]
)
}

}

object geometry extends PostGISGeometryCodecs

object all extends PostGISGeometryCodecs
163 changes: 163 additions & 0 deletions modules/postgis/src/main/scala/ewkb/codecs.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// 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.postgis
package ewkb

import cats.data.NonEmptyList
import cats.syntax.all._
import scodec.Attempt
import scodec.SizeBound
import scodec.bits.BitVector
import scodec.bits.ByteOrdering
import scodec.codecs._
import scodec.{Codec => Scodec}

trait EWKBCodecs extends EWKBCodecPlatform {

lazy val geometry: Scodec[Geometry] = Scodec[Geometry](encoder, decoder)

private def decoder: scodec.Decoder[Geometry] = {
byteOrdering.flatMap { implicit byteOrdering =>
ewkbType.flatMap { implicit ewkb =>
ewkb.geometry match {
case EWKBGeometry.Point => point
case EWKBGeometry.LineString => lineString
case EWKBGeometry.Polygon => polygon
case EWKBGeometry.MultiPoint => multiPoint
case EWKBGeometry.MultiLineString => multiLineString
case EWKBGeometry.MultiPolygon => multiPolygon
case EWKBGeometry.GeometryCollection => geometryCollection
}
}
}
}

def encoder: scodec.Encoder[Geometry] = new scodec.Encoder[Geometry] {
override def encode(value: Geometry): Attempt[BitVector] = {
implicit val byteOrder: ByteOrdering = ByteOrdering.LittleEndian

implicit val ewkb = EWKBType.fromGeometry(value)

val geoencoder = value match {
case _: Point => point.upcast[Geometry]
case _: LineString => lineString.upcast[Geometry]
case _: Polygon => polygon.upcast[Geometry]
case _: MultiPoint => multiPoint.upcast[Geometry]
case _: MultiLineString => multiLineString.upcast[Geometry]
case _: MultiPolygon => multiPolygon.upcast[Geometry]
case _: GeometryCollection => geometryCollection.upcast[Geometry]
}

// scala 2/3 platform specific call - scodec1 encodes HLists, scodec2 encodes tuples
ewkbEncode(ewkb, value, geoencoder)
}

override def sizeBound: SizeBound = SizeBound.unknown
}
}

object codecs extends EWKBCodecs

trait EWKBPrimitives {

def geometry: Scodec[Geometry]

def byteOrdering: Scodec[ByteOrdering] = byte.xmap(
b => b match {
case 0 => ByteOrdering.BigEndian
case 1 => ByteOrdering.LittleEndian
},
o => o match {
case ByteOrdering.BigEndian => 0
case ByteOrdering.LittleEndian => 1
}
)

def ewkbType(implicit byteOrdering: ByteOrdering): Scodec[EWKBType] =
guint32.xmap(EWKBType.fromRaw, EWKBType.toRaw)

def srid(implicit byteOrderering: ByteOrdering, ewkb: EWKBType): Scodec[Option[SRID]] = {
optional(provide(ewkb.srid == EWKBSRID.Present), gint32.xmap(SRID.apply, _.value))
}

def coordinate(implicit byteOrdering: ByteOrdering, ewkb: EWKBType): Scodec[Coordinate] = {
ewkb.coordinate match {
case Dimension.TwoD => gdouble :: gdouble :: provide(none[Double]) :: provide(none[Double])
case Dimension.Z => gdouble :: gdouble :: gdoubleOpt :: provide(none[Double])
case Dimension.M => gdouble :: gdouble :: provide(none[Double]) :: gdoubleOpt
case Dimension.ZM => gdouble :: gdouble :: gdoubleOpt :: gdoubleOpt
}
}.as[Coordinate]

def point(implicit byteOrdering: ByteOrdering, ewkb: EWKBType): Scodec[Point] = {
ewkb.coordinate match {
case Dimension.TwoD => srid :: coordinate
case Dimension.Z => srid :: coordinate
case Dimension.M => srid :: coordinate
case Dimension.ZM => srid :: coordinate
}
}.as[Point]

def lineString(implicit byteOrdering: ByteOrdering, ewkb: EWKBType): Scodec[LineString] = {
srid :: provide(ewkb.coordinate) :: listOfN(gint32, coordinate)
}.as[LineString]

private case class PolygonRepr(srid: Option[SRID], dim: Dimension, rings: List[LinearRing])
def polygon(implicit byteOrdering: ByteOrdering, ewkb: EWKBType): Scodec[Polygon] = {
(srid :: provide(ewkb.coordinate) :: listOfN(gint32, linearRing)).as[PolygonRepr].xmap[Polygon](
repr => Polygon(repr.srid, repr.dim, repr.rings.headOption, repr.rings.drop(1)),
p => PolygonRepr(p.srid, p.dimension, p.shell.toList ::: p.holes)
)
}

def linearRing(implicit byteOrdering: ByteOrdering, ewkb: EWKBType): Scodec[LinearRing] = {
listOfN(gint32, coordinate).xmap[NonEmptyList[Coordinate]](NonEmptyList.fromListUnsafe, _.toList)
}.as[LinearRing]

def multiPoint(implicit byteOrdering: ByteOrdering, ewkb: EWKBType): Scodec[MultiPoint] = {
srid :: provide(ewkb.coordinate) :: listOfN(gint32, geometry.downcast[Point]).xmap[List[Point]](
pts => pts.map(_.copy(srid = None)),
identity
)
}.as[MultiPoint]

def multiLineString(implicit byteOrdering: ByteOrdering, ewkb: EWKBType): Scodec[MultiLineString] = {
srid.flatPrepend { srid =>
provide(ewkb.coordinate) :: listOfN(gint32, geometry.downcast[LineString]).xmap[List[LineString]](
lss => lss.map(_.copy(srid = srid)),
identity
)
}
}.as[MultiLineString]

def multiPolygon(implicit byteOrdering: ByteOrdering, ewkb: EWKBType): Scodec[MultiPolygon] = {
srid.flatPrepend { srid =>
provide(ewkb.coordinate) :: listOfN(gint32, geometry.downcast[Polygon]).xmap[List[Polygon]](
ps => ps.map(_.copy(srid = srid)),
identity
)
}
}.as[MultiPolygon]

def geometryCollection(implicit byteOrdering: ByteOrdering, ewkb: EWKBType): Scodec[GeometryCollection] = {
srid :: provide(ewkb.coordinate) :: listOfN(gint32, geometry)
}.as[GeometryCollection]

def gint32(implicit byteOrdering: ByteOrdering) =
gcodec(int32, int32L)
def guint32(implicit byteOrdering: ByteOrdering) =
gcodec(uint32, uint32L)
def gdouble(implicit byteOrdering: ByteOrdering) =
gcodec(double, doubleL)
def gdoubleOpt(implicit byteOrdering: ByteOrdering): Scodec[Option[Double]] =
gdouble.widenOpt(x => Some(x), x => x)

def gcodec[A](big: Scodec[A], little: Scodec[A])(implicit byteOrdering: ByteOrdering) =
byteOrdering match {
case ByteOrdering.BigEndian => big
case ByteOrdering.LittleEndian => little
}

}
Loading