From 2347ef78b9cff45db0323e8a73ce9c028640fa58 Mon Sep 17 00:00:00 2001 From: moradology Date: Thu, 18 Apr 2019 11:55:16 -0400 Subject: [PATCH] Move slick project from core --- .gitignore | 3 +- .travis.yml | 3 +- .travis/build-and-test.sh | 1 + .travis/slickTestDB.sh | 10 + build.sbt | 15 +- project/Dependencies.scala | 29 +- .../slick/PostGisProjectionSupport.scala | 126 ++++++++ .../geotrellis/slick/PostGisSupport.scala | 116 +++++++ slick/src/test/resources/reference.conf | 21 ++ .../test/scala/geotrellis/slick/Data.scala | 69 ++++ .../slick/PostGisProjectionSupportSpec.scala | 110 +++++++ .../scala/geotrellis/slick/PostgisSpec.scala | 300 ++++++++++++++++++ .../scala/geotrellis/slick/TestDatabase.scala | 51 +++ 13 files changed, 837 insertions(+), 17 deletions(-) create mode 100755 .travis/slickTestDB.sh create mode 100644 slick/src/main/scala/geotrellis/slick/PostGisProjectionSupport.scala create mode 100644 slick/src/main/scala/geotrellis/slick/PostGisSupport.scala create mode 100644 slick/src/test/resources/reference.conf create mode 100644 slick/src/test/scala/geotrellis/slick/Data.scala create mode 100644 slick/src/test/scala/geotrellis/slick/PostGisProjectionSupportSpec.scala create mode 100644 slick/src/test/scala/geotrellis/slick/PostgisSpec.scala create mode 100644 slick/src/test/scala/geotrellis/slick/TestDatabase.scala 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 + } +}