Skip to content

Commit

Permalink
Merge pull request #534 from cranst0n/postgis
Browse files Browse the repository at this point in the history
Add PostGIS support.
  • Loading branch information
mpilquist authored Jan 10, 2024
2 parents 04456c7 + f6204fa commit 6696bcc
Show file tree
Hide file tree
Showing 14 changed files with 1,279 additions and 8 deletions.
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
20 changes: 16 additions & 4 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 @@ -160,10 +160,23 @@ 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"
)
)

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

// ci

// ci
4 changes: 2 additions & 2 deletions 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 Expand Up @@ -65,4 +65,4 @@ services:
- 4317:4317
- 4318:4318
environment:
COLLECTOR_OTLP_ENABLED: "true"
COLLECTOR_OTLP_ENABLED: "true"
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

0 comments on commit 6696bcc

Please sign in to comment.