diff --git a/.gitignore b/.gitignore
index f77be1b9..941f0ab4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,8 @@ project/plugins/target
project/target
target
.ensime
+*.bloop
+*.metals
\#*#
*~
.#*
@@ -45,4 +47,3 @@ nohup.out
derby.log
metastore_db/
*.log
-
diff --git a/.travis.yml b/.travis.yml
index a8de94c3..f1f27b62 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -9,6 +9,7 @@ scala:
- "2.12.8"
before_install:
- docker pull daunnc/openjdk-gdal:2.4.0
+ - docker run -d --restart=always -p 9999:5432 -e POSTGRES_DB=slick_tests quay.io/azavea/postgis:0.1.0
services:
- docker
cache:
@@ -23,4 +24,4 @@ notifications:
- echeipesh@gmail.com
- gr.pomadchin@gmail.com
before_deploy:
-- export VERSION_SUFFIX="-${TRAVIS_COMMIT:0:7}"
\ No newline at end of file
+- export VERSION_SUFFIX="-${TRAVIS_COMMIT:0:7}"
diff --git a/.travis/build-and-test.sh b/.travis/build-and-test.sh
index 2d8c94ec..4a110801 100755
--- a/.travis/build-and-test.sh
+++ b/.travis/build-and-test.sh
@@ -2,4 +2,5 @@
./sbt -J-Xmx2G "++$TRAVIS_SCALA_VERSION" \
"project vlm" test \
+ "project slick" test \
"project gdal" test || { exit 1; }
diff --git a/.travis/slickTestDB.sh b/.travis/slickTestDB.sh
new file mode 100755
index 00000000..cdee04e1
--- /dev/null
+++ b/.travis/slickTestDB.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+docker pull quay.io/azavea/postgis:0.1.0
+
+docker run \
+ -it \
+ --rm \
+ -p 9999:5432 \
+ -e POSTGRES_DB=slick_tests \
+ quay.io/azavea/postgis:0.1.0
diff --git a/build.sbt b/build.sbt
index 1cac3424..2ad446cb 100644
--- a/build.sbt
+++ b/build.sbt
@@ -56,7 +56,7 @@ lazy val commonSettings = Seq(
lazy val root = Project("geotrellis-contrib", file(".")).
aggregate(
- vlm, gdal, summary
+ vlm, gdal, summary, slick
).
settings(commonSettings: _*).
settings(publish / skip := true).
@@ -130,6 +130,19 @@ lazy val gdal = project
""".stripMargin
)
+lazy val slick = project
+ .settings(commonSettings)
+ .settings(
+ organization := "com.azavea.geotrellis",
+ name := "geotrellis-contrib-slick",
+ libraryDependencies ++= Seq(
+ geotrellisVector,
+ slickPG,
+ scalatest % Test
+ )
+ )
+ .settings(crossScalaVersions := Seq(scalaVersion.value))
+
lazy val testkit = project
.settings(commonSettings)
.settings(
diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index 470c6604..cbf4a4c4 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -43,24 +43,25 @@ object Dependencies {
val geotrellisVector = "org.locationtech.geotrellis" %% "geotrellis-vector" % Version.geotrellis
val geotrellisUtil = "org.locationtech.geotrellis" %% "geotrellis-util" % Version.geotrellis
val geotrellisShapefile = "org.locationtech.geotrellis" %% "geotrellis-shapefile" % Version.geotrellis
+ val slickPG = "com.github.tminglei" %% "slick-pg" % "0.16.3"
- val gdal = "org.gdal" % "gdal" % Properties.envOrElse("GDAL_VERSION", "2.4.0")
+ val gdal = "org.gdal" % "gdal" % Properties.envOrElse("GDAL_VERSION", "2.4.0")
- val pureconfig = "com.github.pureconfig" %% "pureconfig" % "0.9.1"
- val logging = "com.typesafe.scala-logging" %% "scala-logging" % "3.9.0"
- val scalatest = "org.scalatest" %% "scalatest" % "3.0.5"
- val scalactic = "org.scalactic" %% "scalactic" % "3.0.5"
- val simulacrum = "com.github.mpilquist" %% "simulacrum" % "0.15.0"
+ val pureconfig = "com.github.pureconfig" %% "pureconfig" % "0.9.1"
+ val logging = "com.typesafe.scala-logging" %% "scala-logging" % "3.9.0"
+ val scalatest = "org.scalatest" %% "scalatest" % "3.0.5"
+ val scalactic = "org.scalactic" %% "scalactic" % "3.0.5"
+ val simulacrum = "com.github.mpilquist" %% "simulacrum" % "0.15.0"
- val catsCore = "org.typelevel" %% "cats-core" % "1.4.0"
- val catsEffect = "org.typelevel" %% "cats-effect" % "1.0.0"
- val fs2Core = "co.fs2" %% "fs2-core" % "1.0.0"
- val fs2Io = "co.fs2" %% "fs2-io" % "1.0.0"
+ val catsCore = "org.typelevel" %% "cats-core" % "1.4.0"
+ val catsEffect = "org.typelevel" %% "cats-effect" % "1.0.0"
+ val fs2Core = "co.fs2" %% "fs2-core" % "1.0.0"
+ val fs2Io = "co.fs2" %% "fs2-io" % "1.0.0"
- val sparkCore = "org.apache.spark" %% "spark-core" % Version.spark
- val sparkSQL = "org.apache.spark" %% "spark-sql" % Version.spark
- val hadoopClient = "org.apache.hadoop" % "hadoop-client" % Version.hadoop
+ val sparkCore = "org.apache.spark" %% "spark-core" % Version.spark
+ val sparkSQL = "org.apache.spark" %% "spark-sql" % Version.spark
+ val hadoopClient = "org.apache.hadoop" % "hadoop-client" % Version.hadoop
- val squants = "org.typelevel" %% "squants" % "1.3.0"
+ val squants = "org.typelevel" %% "squants" % "1.3.0"
}
diff --git a/slick/src/main/scala/geotrellis/slick/PostGisProjectionSupport.scala b/slick/src/main/scala/geotrellis/slick/PostGisProjectionSupport.scala
new file mode 100644
index 00000000..e9207ca1
--- /dev/null
+++ b/slick/src/main/scala/geotrellis/slick/PostGisProjectionSupport.scala
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2016 Azavea
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package geotrellis.contrib.slick
+
+import geotrellis.vector._
+import geotrellis.vector.io.wkt.WKT
+import geotrellis.vector.io.wkb.WKB
+
+import slick.ast.FieldSymbol
+import slick.driver.{JdbcDriver, PostgresDriver}
+import slick.jdbc.{PositionedParameters, PositionedResult, SetParameter}
+import com.github.tminglei.slickpg.geom.PgPostGISExtensions
+
+import java.sql._
+import scala.reflect.ClassTag
+
+/**
+ * This class provides column types and extension methods to work with Geometry columns
+ * associated with an SRID in PostGIS.
+ *
+ * Sample Usage:
+ *
+ * val PostGIS = new PostGisProjectionSupport(PostgresDriver)
+ * import PostGIS._
+ *
+ * class City(tag: Tag) extends Table[(Int,String,Projected[Point])](tag, "cities") {
+ * def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
+ * def name = column[String]("name")
+ * def geom = column[Projected[Point]]("geom")
+ * def * = (id, name, geom)
+ * }
+ *
+ *
+ * based on [[package com.github.tminglei.slickpg.PgPostGISSupport]]
+ */
+trait PostGisProjectionSupport extends PgPostGISExtensions { driver: PostgresDriver =>
+ import PostGisProjectionSupportUtils._
+ import driver.api._
+
+ type GEOMETRY = Projected[Geometry]
+ type POINT = Projected[Point]
+ type LINESTRING = Projected[Line]
+ type POLYGON = Projected[Polygon]
+ type GEOMETRYCOLLECTION = Projected[GeometryCollection]
+
+ trait PostGISProjectionAssistants extends BasePostGISAssistants[GEOMETRY, POINT, LINESTRING, POLYGON, GEOMETRYCOLLECTION]
+trait PostGISProjectionImplicits {
+ implicit val geometryTypeMapper = new ProjectedGeometryJdbcType[GEOMETRY]
+ implicit val pointTypeMapper = new ProjectedGeometryJdbcType[POINT]
+ implicit val lineTypeMapper = new ProjectedGeometryJdbcType[LINESTRING]
+ implicit val polygonTypeMapper = new ProjectedGeometryJdbcType[POLYGON]
+ implicit val geometryCollectionTypeMapper = new ProjectedGeometryJdbcType[GEOMETRYCOLLECTION]
+ implicit val multiPointTypeMapper = new ProjectedGeometryJdbcType[Projected[MultiPoint]]
+ implicit val multiPolygonTypeMapper = new ProjectedGeometryJdbcType[Projected[MultiPolygon]]
+ implicit val multiLineTypeMapper = new ProjectedGeometryJdbcType[Projected[MultiLine]]
+
+ implicit def geometryColumnExtensionMethods[G1 <: GEOMETRY](c: Rep[G1]) =
+ new GeometryColumnExtensionMethods[GEOMETRY, POINT, LINESTRING, POLYGON, GEOMETRYCOLLECTION, G1, G1](c)
+
+ implicit def geometryOptionColumnExtensionMethods[G1 <: GEOMETRY](c: Rep[Option[G1]]) =
+ new GeometryColumnExtensionMethods[GEOMETRY, POINT, LINESTRING, POLYGON, GEOMETRYCOLLECTION, G1, Option[G1]](c)
+}
+
+ class ProjectedGeometryJdbcType[T <: Projected[Geometry] :ClassTag] extends DriverJdbcType[T] {
+
+ override def sqlTypeName(sym: Option[FieldSymbol]): String = "geometry"
+
+ override def hasLiteralForm: Boolean = false
+
+ override def valueToSQLLiteral(v: T) = toLiteral(v)
+
+ def zero: T = null.asInstanceOf[T]
+
+ def sqlType: Int = java.sql.Types.OTHER
+
+ def setValue(v: T, p: PreparedStatement, idx: Int) = p.setBytes(idx, WKB.write(v.geom, v.srid))
+
+ def updateValue(v: T, r: ResultSet, idx: Int) = r.updateBytes(idx, WKB.write(v.geom, v.srid))
+
+ def getValue(r: ResultSet, idx: Int): T = {
+ val s = r.getString(idx)
+ (if(r.wasNull) None else Some(s))
+ .map(fromLiteral[T](_))
+ .getOrElse(zero)
+ }
+ }
+}
+
+object PostGisProjectionSupportUtils {
+ lazy val WITH_SRID = """^SRID=([\d]+);(.*)""".r
+
+ def toLiteral(pg: Projected[Geometry]): String = s"SRID=${pg.srid};${WKT.write(pg.geom)}"
+
+ def fromLiteral[T <: Projected[_]](value: String): T =
+ value match {
+ case WITH_SRID(srid, wkt) =>
+ val geom = readWktOrWkb(wkt)
+ Projected(geom, srid.toInt).asInstanceOf[T]
+ case _ =>
+ val geom = readWktOrWkb(value)
+ Projected(geom, geom.jtsGeom.getSRID).asInstanceOf[T]
+ }
+
+ def readWktOrWkb(s: String): Geometry = {
+ if (s.startsWith("\\x"))
+ WKB.read(s.drop(2))
+ else if (s.startsWith("00") || s.startsWith("01"))
+ WKB.read(s)
+ else
+ WKT.read(s)
+ }
+}
diff --git a/slick/src/main/scala/geotrellis/slick/PostGisSupport.scala b/slick/src/main/scala/geotrellis/slick/PostGisSupport.scala
new file mode 100644
index 00000000..42c99f4e
--- /dev/null
+++ b/slick/src/main/scala/geotrellis/slick/PostGisSupport.scala
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2016 Azavea
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package geotrellis.contrib.slick
+
+import geotrellis.vector._
+import geotrellis.vector.io.wkt.WKT
+import geotrellis.vector.io.wkb.WKB
+
+import slick.ast.FieldSymbol
+import slick.driver.{JdbcDriver, PostgresDriver}
+import slick.jdbc.{PositionedParameters, PositionedResult, SetParameter}
+import com.github.tminglei.slickpg.geom.PgPostGISExtensions
+
+import scala.reflect.ClassTag
+import java.sql.{PreparedStatement, ResultSet}
+
+/**
+ * This class provides column types and extension methods to work with Geometry columns in PostGIS.
+ *
+ * Sample Usage:
+ *
+ * val PostGIS = new PostGisSupport(PostgresDriver)
+ * import PostGIS._
+ *
+ * class City(tag: Tag) extends Table[(Int,String,Point)](tag, "cities") {
+ * def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
+ * def name = column[String]("name")
+ * def geom = column[Point]("geom")
+ * def * = (id, name, geom)
+ * }
+ *
+ *
+ * based on [[package com.github.tminglei.slickpg.PgPostGISSupport]]
+ */
+trait PostGisSupport extends PgPostGISExtensions { driver: PostgresDriver =>
+ import PostGisSupportUtils._
+ import driver.api._
+
+ type GEOMETRY = Geometry
+ type POINT = Point
+ type LINESTRING = Line
+ type POLYGON = Polygon
+ type GEOMETRYCOLLECTION = GeometryCollection
+
+ trait PostGISAssistants extends BasePostGISAssistants[GEOMETRY, POINT, LINESTRING, POLYGON, GEOMETRYCOLLECTION]
+ trait PostGISImplicits {
+ implicit val geometryTypeMapper = new GeometryJdbcType[GEOMETRY]
+ implicit val pointTypeMapper = new GeometryJdbcType[POINT]
+ implicit val lineTypeMapper = new GeometryJdbcType[LINESTRING]
+ implicit val polygonTypeMapper = new GeometryJdbcType[POLYGON]
+ implicit val geometryCollectionTypeMapper = new GeometryJdbcType[GEOMETRYCOLLECTION]
+ implicit val multiPointTypeMapper = new GeometryJdbcType[MultiPoint]
+ implicit val multiPolygonTypeMapper = new GeometryJdbcType[MultiPolygon]
+ implicit val multiLineTypeMapper = new GeometryJdbcType[MultiLine]
+
+ implicit def geometryColumnExtensionMethods[G1 <: Geometry](c: Rep[G1]) =
+ new GeometryColumnExtensionMethods[GEOMETRY, POINT, LINESTRING, POLYGON, GEOMETRYCOLLECTION, G1, G1](c)
+
+ implicit def geometryOptionColumnExtensionMethods[G1 <: Geometry](c: Rep[Option[G1]]) =
+ new GeometryColumnExtensionMethods[GEOMETRY, POINT, LINESTRING, POLYGON, GEOMETRYCOLLECTION, G1, Option[G1]](c)
+ }
+
+ class GeometryJdbcType[T <: Geometry](implicit override val classTag: ClassTag[T]) extends DriverJdbcType[T]{
+
+ override def sqlTypeName(sym: Option[FieldSymbol]): String = "geometry"
+
+ override def hasLiteralForm: Boolean = false
+
+ override def valueToSQLLiteral(v: T) = toLiteral(v)
+
+ def zero: T = null.asInstanceOf[T]
+
+ def sqlType: Int = java.sql.Types.OTHER
+
+ def setValue(v: T, p: PreparedStatement, idx: Int) = p.setBytes(idx, WKB.write(v))
+
+ def updateValue(v: T, r: ResultSet, idx: Int) = r.updateBytes(idx, WKB.write(v))
+
+ def getValue(r: ResultSet, idx: Int): T = {
+ val s = r.getString(idx)
+ (if(r.wasNull) None else Some(s))
+ .map(fromLiteral[T](_))
+ .getOrElse(zero)
+ }
+ }
+}
+
+object PostGisSupportUtils {
+ import PostGisProjectionSupportUtils._
+
+ def toLiteral(geom: Geometry): String = WKT.write(geom)
+
+ def fromLiteral[T <: Geometry](value: String): T = {
+ val wkt =
+ value match {
+ case WITH_SRID(srid, wkt) => wkt
+ case _ => value
+ }
+
+ readWktOrWkb(wkt).asInstanceOf[T]
+ }
+}
diff --git a/slick/src/test/resources/reference.conf b/slick/src/test/resources/reference.conf
new file mode 100644
index 00000000..605428d9
--- /dev/null
+++ b/slick/src/test/resources/reference.conf
@@ -0,0 +1,21 @@
+# Copyright 2016 Azavea
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+db {
+ user = "postgres"
+ password = "postgres"
+ database = "slick_tests"
+ host = "localhost:9999"
+}
+
diff --git a/slick/src/test/scala/geotrellis/slick/Data.scala b/slick/src/test/scala/geotrellis/slick/Data.scala
new file mode 100644
index 00000000..f80b0359
--- /dev/null
+++ b/slick/src/test/scala/geotrellis/slick/Data.scala
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2016 Azavea
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package geotrellis.contrib.slick
+
+import org.locationtech.jts.{geom => jts}
+import geotrellis.vector._
+import geotrellis.vector.io.wkt._
+
+object util {
+
+ def data: Array[(String, Point)] =
+"""[ABE] 40.65 75.43 Allentown,PA
+[AOO] 40.30 78.32 Altoona,PA
+[BVI] 40.75 80.33 Beaver Falls,PA
+[BSI] 40.27 79.09 Blairsville,PA
+[BFD] 41.80 78.63 Bradford,PA
+[DUJ] 41.18 78.90 Dubois,PA
+[ERI] 42.08 80.18 Erie,PA
+[FKL] 41.38 79.87 Franklin,PA
+[CXY] 40.22 76.85 Harrisburg,PA
+[HAR] 40.37 77.42 Harrisburg,PA
+[JST] 40.32 78.83 Johnstown,PA
+[LNS] 40.13 76.30 Lancaster,PA
+[LBE] 40.28 79.40 Latrobe,PA
+[MDT] 40.20 76.77 Middletown,PA
+[MUI] 40.43 76.57 Muir,PA
+[PNE] 40.08 75.02 Nth Philadel,PA
+[PHL] 39.88 75.25 Philadelphia,PA
+[PSB] 41.47 78.13 Philipsburg,PA
+[AGC] 40.35 79.93 Pittsburgh,PA
+[PIT] 40.50 80.22 Pittsburgh,PA
+[RDG] 40.38 75.97 Reading,PA
+[43M] 39.73 77.43 Site R,PA
+[UNV] 40.85 77.83 State Colleg,PA
+[AVP] 41.33 75.73 Wilkes-Barre,PA
+[IPT] 41.25 76.92 Williamsport,PA
+[NXX] 40.20 75.15 Willow Grove,PA
+""".split("\n")
+ .map(str => (str.substring(7,12), str.substring(15,20), str.substring(22)))
+ .map(_ match {
+ case (lat,lng,city) =>
+ (city, Point(lng.toDouble, lat.toDouble))
+ })
+
+ def bboxBuffer(x: Double, y: Double, d: Double) =
+ Polygon(Line(
+ (x - d, y - d),
+ (x - d, y + d),
+ (x + d, y + d),
+ (x + d, y - d),
+ (x - d, y - d)
+ ))
+
+ def pt(x: Double, y: Double) = Point(x, y)
+}
diff --git a/slick/src/test/scala/geotrellis/slick/PostGisProjectionSupportSpec.scala b/slick/src/test/scala/geotrellis/slick/PostGisProjectionSupportSpec.scala
new file mode 100644
index 00000000..b015e5c9
--- /dev/null
+++ b/slick/src/test/scala/geotrellis/slick/PostGisProjectionSupportSpec.scala
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2016 Azavea
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package geotrellis.contrib.slick
+
+import geotrellis.vector._
+
+import org.scalatest.concurrent.ScalaFutures
+import org.scalatest.time.{Seconds, Span}
+import org.scalatest._
+import slick.driver.PostgresDriver
+import util._
+
+class PostGisProjectionSupportSpec extends FlatSpec with Matchers with TestDatabase with ScalaFutures {
+ implicit override val patienceConfig = PatienceConfig(timeout = Span(5, Seconds))
+
+ object driver extends PostgresDriver with PostGisProjectionSupport {
+ override val api = new API with PostGISProjectionAssistants with PostGISProjectionImplicits
+ }
+ import driver.api._
+
+ class City(tag: Tag) extends Table[(Int,String,Projected[Point])](tag, "cities") {
+ def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
+ def name = column[String]("name")
+ def geom = column[Projected[Point]]("geom")
+
+ def * = (id, name, geom)
+ }
+ val CityTable = TableQuery[City]
+
+ "ProjectedGeometry" should "not make Slick barf" in {
+ try { db.run(CityTable.schema.drop).futureValue } catch { case e: Throwable => }
+ db.run(CityTable.schema.create).futureValue
+
+ db.run(CityTable.map(c => (c.name, c.geom)) += ("Megacity 1", Projected(Point(1,1), 43211))).futureValue
+
+ db.run(CityTable.schema.drop).futureValue
+ }
+
+ class LineRow(tag: Tag) extends Table[(Int,Projected[Line])](tag, "lines") {
+ def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
+ def geom = column[Projected[Line]]("geom")
+
+ def * = (id, geom)
+ }
+
+ it should "support PostGIS function mapping" in {
+ val LineTable = TableQuery[LineRow]
+ try { db.run(LineTable.schema.drop).futureValue } catch { case e: Throwable => }
+ db.run(LineTable.schema.create).futureValue
+
+ db.run(LineTable.map(_.geom) += Projected(Line(Point(1,1), Point(1,3)), 3131)).futureValue
+
+ val q = for {
+ line <- LineTable
+ } yield line.geom.length
+
+ db.run(q.result).futureValue.toList.head should equal (2.0)
+ }
+
+ it should "support PostGIS multi points" in {
+ class MPRow(tag: Tag) extends Table[(Int,Projected[MultiPoint])](tag, "points") {
+ def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
+ def geom = column[Projected[MultiPoint]]("geom")
+ def * = (id, geom)
+ }
+ val MPTable = TableQuery[MPRow]
+
+ try { db.run(MPTable.schema.drop).futureValue } catch { case e: Throwable => }
+ db.run(MPTable.schema.create).futureValue
+
+ db.run(MPTable.map(_.geom) += Projected(MultiPoint(Point(1,1), Point(2,2)), 3131)).futureValue
+
+ val q = for {
+ mp <- MPTable
+ } yield {mp.geom.centroid}
+
+ db.run(q.result).futureValue.toList.head should equal ( Projected(Point(1.5, 1.5), 3131) )
+ }
+
+ it should "handle hex strings starting with \\x" in {
+ val wkb ="\\x002000000300000f110000000100000005c170b8793ccc8e80415ca9f4683a18dcc170b8793ccc8e8041631bf8457c1091c16ca9f4683a18dc41631bf8457c1091c16ca9f4683a18dc415ca9f4683a18dcc170b8793ccc8e80415ca9f4683a18dc"
+ val interpreted = PostGisProjectionSupportUtils.readWktOrWkb(wkb)
+ val poly = Polygon(Point(-17532819.799940586, 7514065.628545966), Point(-17532819.799940586, 10018754.171394618), Point(-15028131.257091932, 10018754.171394618), Point(-15028131.257091932, 7514065.628545966), Point(-17532819.799940586, 7514065.628545966))
+
+ interpreted should be (poly)
+ }
+
+ it should "handle hex strings starting with 00" in {
+ val wkb ="002000000300000f110000000100000005c170b8793ccc8e80415ca9f4683a18dcc170b8793ccc8e8041631bf8457c1091c16ca9f4683a18dc41631bf8457c1091c16ca9f4683a18dc415ca9f4683a18dcc170b8793ccc8e80415ca9f4683a18dc"
+ val interpreted = PostGisProjectionSupportUtils.readWktOrWkb(wkb)
+ val poly = Polygon(Point(-17532819.799940586, 7514065.628545966), Point(-17532819.799940586, 10018754.171394618), Point(-15028131.257091932, 10018754.171394618), Point(-15028131.257091932, 7514065.628545966), Point(-17532819.799940586, 7514065.628545966))
+
+ interpreted should be (poly)
+ }
+
+}
diff --git a/slick/src/test/scala/geotrellis/slick/PostgisSpec.scala b/slick/src/test/scala/geotrellis/slick/PostgisSpec.scala
new file mode 100644
index 00000000..4e34b4f8
--- /dev/null
+++ b/slick/src/test/scala/geotrellis/slick/PostgisSpec.scala
@@ -0,0 +1,300 @@
+/*
+ * Copyright 2016 Azavea
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package geotrellis.contrib.slick
+
+import geotrellis.vector._
+
+import org.scalatest._
+import org.scalatest.concurrent.ScalaFutures
+import org.scalatest.time.{Seconds, Span}
+import slick.driver.PostgresDriver
+import util._
+
+import java.util.Locale
+
+
+class PostgisSpec extends FlatSpec with Matchers with TestDatabase with ScalaFutures {
+
+ implicit override val patienceConfig = PatienceConfig(timeout = Span(5, Seconds))
+
+ object driver extends PostgresDriver with PostGisSupport {
+ override val api = new API with PostGISAssistants with PostGISImplicits
+ }
+ import driver.api._
+ //import support of Subclasses of Geometry
+
+ class SimpleCity(tag: Tag) extends Table[(Int,String)](tag, "simple_cities") {
+ def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
+ def name = column[String]("name")
+
+ def * = (id, name)
+ }
+ val SimpleCityTable = TableQuery[SimpleCity]
+
+
+ class City(tag: Tag) extends Table[(Int,String,Point)](tag, "cities") {
+ def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
+ def name = column[String]("name")
+ def geom = column[Point]("geom")
+
+ def * = (id, name, geom)
+ }
+ val CityTable = TableQuery[City]
+
+ def createSchema() =
+ try {
+ db.run(CityTable.schema.create).futureValue
+ } catch {
+ case _: Throwable =>
+ println("A script for setting up the PSQL environment necessary to run these tests can be found at scripts/slickTestDB.sh - requires a working docker setup")
+ }
+
+ def dropSchema() = db.run(CityTable.schema.drop).futureValue
+ "Environment" should "be sane" in {
+
+ try { db.run(SimpleCityTable.schema.drop).futureValue } catch { case e: Throwable => }
+
+ val cities = Seq("washington", "london", "paris")
+
+ db.run(SimpleCityTable.schema.create).futureValue
+ db.run(SimpleCityTable.map(c => c.name) ++= cities).futureValue
+
+ val q = for { c <- SimpleCityTable } yield c.name
+ db.run(q.result).futureValue.toList should equal (cities)
+
+ val q2 = for { c <- SimpleCityTable if c.id > 1 } yield c
+ db.run(q2.delete).futureValue
+ db.run( { for { c <- SimpleCityTable } yield c }.result).futureValue.toList.length should equal (1)
+
+ val q3 = for { c <- SimpleCityTable } yield c
+ db.run(q3.delete).futureValue
+ db.run({ for { c <- SimpleCityTable } yield c }.result).futureValue.toList.length should equal (0)
+
+ db.run(SimpleCityTable.schema.drop).futureValue
+ }
+
+ "Postgis driver" should "be able to insert geoms" in {
+ try { db.run(CityTable.schema.drop).futureValue } catch { case e: Throwable => }
+
+ createSchema()
+ db.run(CityTable.map(c => (c.name, c.geom)) ++= data.map { d => (d._1, d._2) }).futureValue
+
+ val q = for { c <- CityTable } yield (c.name, c.geom)
+
+ db.run(q.result).futureValue.toList should equal (data.toList)
+
+ dropSchema()
+ }
+
+ it should "be able to delete all" in {
+
+ // Make sure things are clean
+ // we probably shouldn't need this
+ try { db.run(CityTable.schema.drop).futureValue } catch { case e: Throwable => }
+
+ createSchema()
+ db.run(CityTable.map(c => (c.name, c.geom)) ++= data.map { d => (d._1, d._2) }).futureValue
+
+ val q1 = for { c <- CityTable } yield c
+ db.run(q1.result).futureValue.toList.length should equal (data.length)
+
+ val q2 = for { c <- CityTable } yield c
+ db.run(q2.delete).futureValue
+
+ val q3 = for { c <- CityTable } yield c
+ db.run(q3.result).futureValue.toList.length should equal (0)
+
+ dropSchema()
+ }
+
+ it should "be able to delete with geom where clause" in {
+ // Make sure things are clean
+ // we probably shouldn't need this
+ try { db.run(CityTable.schema.drop).futureValue } catch { case e: Throwable => }
+
+ createSchema()
+ db.run(CityTable.map(c => (c.name, c.geom)) ++= data.map { d => (d._1, d._2) }).futureValue
+
+ // 40.30, 78.32 -> Altoona,PA
+ val bbox = bboxBuffer(78.32, 40.30, 0.01)
+
+ val q = for {c <- CityTable if c.geom @&& bbox} yield c
+ db.run(q.delete).futureValue
+
+ val q2 = for { c <- CityTable } yield c.name
+
+ db.run(q2.result).futureValue.toList should equal (data.map(_._1).filter(_ != "Altoona,PA").toList)
+
+ db.run(CityTable.forceInsert(4000, "ATown",pt(-55.1,23.3))).futureValue
+
+ val q3 = for { c <- CityTable if c.id =!= 4000 } yield c
+ db.run(q3.delete).futureValue
+
+ val q4 = for { c <- CityTable } yield c.name
+ db.run(q4.result).futureValue.toList should equal (List("ATown"))
+
+ dropSchema()
+ }
+
+ it should "be able to query with geo fcns" in {
+ // Make sure things are clean
+ // we probably shouldn't need this
+ try { db.run(CityTable.schema.drop).futureValue } catch { case e: Throwable => }
+
+ createSchema()
+ db.run(CityTable.map(c => (c.name, c.geom)) ++= data.map { d => (d._1, d._2) }).futureValue
+
+ // 40.30, 78.32 -> Altoona,PA
+ val bbox = bboxBuffer(78.32, 40.30, 0.01)
+
+ // Operator
+ val q = for {
+ c <- CityTable if c.geom @&& bbox // && -> overlaps
+ } yield c.name
+
+
+ db.run(q.result).futureValue.toList should equal (List("Altoona,PA"))
+
+ // Function
+ val dist = 0.5f
+ val q2 = for {
+ c1 <- CityTable
+ c2 <- CityTable if c1.geom.distance(c2.geom) < dist && c1.name =!= c2.name
+ } yield (c1.name, c2.name, c1.geom.distance(c2.geom))
+
+ val q2format = db.run(q2.result).futureValue.toList map {
+ case (n1,n2,d) => (n1,n2, "%1.4f".formatLocal(Locale.ENGLISH, d))
+ }
+
+ val jts = for {
+ j1 <- data
+ j2 <- data if j1._2.distance(j2._2) < dist && j1._1 != j2._1
+ } yield (j1._1, j2._1, "%1.4f".formatLocal(Locale.ENGLISH, j1._2.distance(j2._2)))
+
+ q2format should equal (jts.toList)
+
+ // Output function
+ val q3 = for {
+ c <- CityTable if c.name === "Reading,PA"
+ } yield c.geom.asGeoJSON()
+
+ println(db.run(q3.result).futureValue.head) // todo checki if this is correct
+ db.run(q3.result).futureValue.head should equal ("""{"type":"Point","coordinates":[75.97,40.38]}""") // it should be first
+
+ dropSchema()
+ }
+
+ class OptCityRow(tag: Tag) extends Table[(Int,String,Option[Point])](tag, "cities") {
+ def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
+ def name = column[String]("name")
+ def geom = column[Option[Point]]("geom")
+
+ def * = (id, name, geom)
+ }
+ val OptCity = TableQuery[OptCityRow]
+
+ it should "be able to handle optional fields" in {
+ try { db.run(OptCity.schema.drop).futureValue } catch { case e: Throwable => }
+
+ db.run(OptCity.schema.create).futureValue
+
+ val cities = Seq(
+ ("washington",Some(pt(-77.02,38.53))),
+ ("london", None),
+ ("paris", Some(pt(2.3470,48.8742)))
+ )
+
+ db.run(OptCity.map(c => (c.name, c.geom)) ++= cities).futureValue
+
+ val q1 = for {
+ c <- OptCity if !(c.geom isDefined)
+ } yield (c.name, c.geom)
+ db.run(q1.result).futureValue.toList should equal (List(("london", None)))
+
+ val q2 = for {
+ c <- OptCity if c.geom isDefined
+ } yield c.name
+
+ db.run(q2.result).futureValue.toList should equal (List("washington", "paris"))
+
+ db.run(OptCity.schema.drop).futureValue
+ }
+
+ it should "be able to query with geo fcns on null fields" in {
+ // Make sure things are clean
+ // we probably shouldn't need this
+ try { db.run(OptCity.schema.drop).futureValue } catch { case e: Throwable => }
+
+ val data2 = data.map { case (s, g) => s -> Some(g)}
+
+ db.run(OptCity.schema.create).futureValue
+ db.run(OptCity.map(c => (c.name, c.geom)) ++= data2).futureValue
+
+ // 40.30, 78.32 -> Altoona,PA
+ val bbox = bboxBuffer(78.32, 40.30, 0.01)
+
+ val q = for {
+ c <- OptCity if c.geom @&& bbox // && -> overlaps
+ } yield c.name
+
+ db.run(q.result).futureValue should equal (List("Altoona,PA"))
+
+ db.run(OptCity.schema.drop).futureValue
+ }
+
+ it should "be able to handle generic geom fields" in {
+ // if this compiles we're golden
+ class Foo(tag: Tag) extends Table[(Int,String,Option[Geometry])](tag, "foo") {
+
+ def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
+ def name = column[String]("name")
+ def geom = column[Option[Geometry]]("geom")
+
+ def * = (id, name, geom)
+ }
+
+ class Bar(tag: Tag) extends Table[(Int,String,Geometry)](tag, "bar") {
+ def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
+ def name = column[String]("name")
+ def geom = column[Geometry]("geom")
+
+ def * = (id, name, geom)
+ }
+ }
+
+ class LineRow(tag: Tag) extends Table[(Int,Line)](tag, "lines") {
+ def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
+ def geom = column[Line]("geom")
+
+ def * = (id, geom)
+ }
+ val LineTable = TableQuery[LineRow]
+
+ it should "wrap PostGIS functions on Geometry Fields" in {
+ try { db.run(LineTable.schema.drop).futureValue } catch { case e: Throwable => }
+ db.run(LineTable.schema.create).futureValue
+
+ db.run(LineTable.map(_.geom) += Line(Point(1,1), Point(1,2))).futureValue
+
+ val q = for {
+ line <- LineTable
+ } yield line.geom.length
+
+ println(q.result.statements)
+ println(db.run(q.result).futureValue.toList)
+ }
+}
diff --git a/slick/src/test/scala/geotrellis/slick/TestDatabase.scala b/slick/src/test/scala/geotrellis/slick/TestDatabase.scala
new file mode 100644
index 00000000..45ba9463
--- /dev/null
+++ b/slick/src/test/scala/geotrellis/slick/TestDatabase.scala
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2016 Azavea
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package geotrellis.contrib.slick
+
+import com.typesafe.config.{ConfigFactory,Config}
+import org.scalatest._
+
+import slick.driver.PostgresDriver.api._
+
+object TestDatabase {
+ def newInstance = {
+ val config = ConfigFactory.load
+ val pguser = config.getString("db.user")
+ val pgpass = config.getString("db.password")
+ val pgdb = config.getString("db.database")
+ val pghost = config.getString("db.host")
+
+ val s = s"jdbc:postgresql://$pghost/$pgdb"
+ println(s"Connecting to $s")
+
+ Database.forURL(
+ "jdbc:postgresql://" + pghost + "/" + pgdb,
+ driver="org.postgresql.Driver",
+ user=pguser,
+ password=pgpass
+ )
+ }
+}
+
+trait TestDatabase extends BeforeAndAfterAll { self: Suite =>
+ protected var db: Database = null
+
+ override def beforeAll() = {
+ val config = ConfigFactory.load
+ db = TestDatabase.newInstance
+ }
+}