Skip to content

Commit bb7fb77

Browse files
committed
postgis support in a separate module
1 parent 6af0a4c commit bb7fb77

File tree

16 files changed

+820
-6
lines changed

16 files changed

+820
-6
lines changed

gradle.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ TEST_CONTAINERS_VERSION=1.15.1
1818
MYSQL_CONNECTOR_VERSION=5.1.47
1919
AWAITILITY_VERSION=3.1.5
2020
THREETEN_EXTRA=1.6.0
21+
JTS_VERSION=1.19.0

postgis-jasync/build.gradle.kts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
val KOTLIN_VERSION: String by project
2+
val KOTLIN_COROUTINES_VERSION: String by project
3+
val SL4J_VERSION: String by project
4+
val JODA_VERSION: String by project
5+
val NETTY_VERSION: String by project
6+
val KOTLIN_LOGGING_VERSION: String by project
7+
val SCRAM_CLIENT_VERSION: String by project
8+
val THREETEN_EXTRA: String by project
9+
10+
val JUNIT_VERSION: String by project
11+
val ASSERTJ_VERSION: String by project
12+
val MOCKK_VERSION: String by project
13+
val LOGBACK_VERSION: String by project
14+
val TEST_CONTAINERS_VERSION: String by project
15+
val AWAITILITY_VERSION: String by project
16+
val JTS_VERSION: String by project
17+
18+
dependencies {
19+
compile(project(":db-async-common"))
20+
compile(project(":pool-async"))
21+
compile(project(":postgresql-async"))
22+
compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$KOTLIN_VERSION")
23+
compile("org.jetbrains.kotlinx:kotlinx-coroutines-core:$KOTLIN_COROUTINES_VERSION")
24+
compile("org.slf4j:slf4j-api:$SL4J_VERSION")
25+
compile("joda-time:joda-time:$JODA_VERSION")
26+
compile("io.netty:netty-transport:$NETTY_VERSION")
27+
compile("io.netty:netty-handler:$NETTY_VERSION")
28+
compile("io.github.microutils:kotlin-logging:$KOTLIN_LOGGING_VERSION")
29+
compile("com.ongres.scram:client:$SCRAM_CLIENT_VERSION")
30+
compile("org.threeten:threeten-extra:$THREETEN_EXTRA")
31+
testImplementation("junit:junit:$JUNIT_VERSION")
32+
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$KOTLIN_VERSION")
33+
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$KOTLIN_VERSION")
34+
testImplementation("org.assertj:assertj-core:$ASSERTJ_VERSION")
35+
testImplementation("org.jetbrains.kotlin:kotlin-reflect:$KOTLIN_VERSION")
36+
testImplementation("io.mockk:mockk:$MOCKK_VERSION")
37+
testImplementation("ch.qos.logback:logback-classic:$LOGBACK_VERSION")
38+
testImplementation("org.testcontainers:postgresql:$TEST_CONTAINERS_VERSION")
39+
testImplementation("org.awaitility:awaitility-kotlin:$AWAITILITY_VERSION")
40+
compile("org.locationtech.jts:jts-core:$JTS_VERSION")
41+
}
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
package com.github.jasync.sql.db.postgis
2+
3+
import net.postgis.jdbc.geometry.binary.ByteGetter
4+
import net.postgis.jdbc.geometry.binary.ValueGetter
5+
import org.locationtech.jts.geom.Coordinate
6+
import org.locationtech.jts.geom.CoordinateSequence
7+
import org.locationtech.jts.geom.Geometry
8+
import org.locationtech.jts.geom.GeometryCollection
9+
import org.locationtech.jts.geom.GeometryFactory
10+
import org.locationtech.jts.geom.LineString
11+
import org.locationtech.jts.geom.LinearRing
12+
import org.locationtech.jts.geom.MultiLineString
13+
import org.locationtech.jts.geom.MultiPoint
14+
import org.locationtech.jts.geom.MultiPolygon
15+
import org.locationtech.jts.geom.Point
16+
import org.locationtech.jts.geom.Polygon
17+
import org.locationtech.jts.geom.PrecisionModel
18+
import org.locationtech.jts.geom.impl.PackedCoordinateSequence
19+
20+
/**
21+
* Parse binary representation of geometries. Currently, only text rep (hexed)
22+
* implementation is tested.
23+
*
24+
* It should be easy to add char[] and CharSequence ByteGetter instances,
25+
* although the latter one is not compatible with older jdks.
26+
*
27+
* I did not implement real unsigned 32-bit integers or emulate them with long,
28+
* as both java Arrays and Strings currently can have only 2^31-1 elements
29+
* (bytes), so we cannot even get or build Geometries with more than approx.
30+
* 2^28 coordinates (8 bytes each).
31+
*
32+
*/
33+
@Suppress("UNCHECKED_CAST", "IMPLICIT_NOTHING_TYPE_ARGUMENT_IN_RETURN_POSITION", "UNREACHABLE_CODE")
34+
class JtsBinaryParser {
35+
private val jtsFactory: GeometryFactory = GeometryFactory(PrecisionModel(), 4326)
36+
37+
/**
38+
* Parse a hex encoded geometry
39+
* @param value String containing the hex data to be parsed
40+
* @return the resulting parsed geometry
41+
*/
42+
fun parse(value: String?): Geometry {
43+
val bytes = ByteGetter.StringByteGetter(value)
44+
return parseGeometry(valueGetterForEndian(bytes))
45+
}
46+
47+
/**
48+
* Parse a binary encoded geometry.
49+
* @param value byte array containing the binary encoded geometru
50+
* @return the resulting parsed geometry
51+
*/
52+
fun parse(value: ByteArray?): Geometry {
53+
val bytes = ByteGetter.BinaryByteGetter(value)
54+
return parseGeometry(valueGetterForEndian(bytes))
55+
}
56+
/**
57+
* Parse with a known geometry factory
58+
* @param data ValueGetter for the data to be parsed
59+
* @param srid the SRID to be used for parsing
60+
* @param inheritSrid flag to toggle inheriting SRIDs
61+
* @return The resulting Geometry
62+
*/
63+
/**
64+
* Parse a geometry starting at offset.
65+
* @param data ValueGetter for the data to be parsed
66+
* @return The resulting Geometry
67+
*/
68+
private fun <T : Geometry> parseGeometry(data: ValueGetter, srid: Int = 0, inheritSrid: Boolean = false): T {
69+
var sridVar = srid
70+
val endian = data.byte // skip and test endian flag
71+
require(endian == data.endian) { "Endian inconsistency!" }
72+
val typeword = data.int
73+
val realtype = typeword and 0x1FFFFFFF // cut off high flag bits
74+
val haveZ = typeword and -0x80000000 != 0
75+
val haveM = typeword and 0x40000000 != 0
76+
val haveS = typeword and 0x20000000 != 0
77+
if (haveS) {
78+
val newsrid = Geom.parseSRID(data.int)
79+
sridVar = if (inheritSrid && newsrid != sridVar) {
80+
throw IllegalArgumentException("Inconsistent srids in complex geometry: $sridVar, $newsrid")
81+
} else {
82+
newsrid
83+
}
84+
} else if (!inheritSrid) {
85+
sridVar = Geom.UNKNOWN_SRID
86+
}
87+
val result: Geometry = when (realtype) {
88+
Geom.POINT -> parsePoint(data, haveZ, haveM)
89+
Geom.LINESTRING -> parseLineString(data, haveZ, haveM)
90+
Geom.POLYGON -> parsePolygon(data, haveZ, haveM, sridVar)
91+
Geom.MULTIPOINT -> parseMultiPoint(data, sridVar)
92+
Geom.MULTILINESTRING -> parseMultiLineString(data, sridVar)
93+
Geom.MULTIPOLYGON -> parseMultiPolygon(data, sridVar)
94+
Geom.GEOMETRYCOLLECTION -> parseCollection(data, sridVar)
95+
else -> throw IllegalArgumentException("Unknown Geometry Type!")
96+
}
97+
result.srid = sridVar
98+
return result as T
99+
}
100+
101+
private fun parsePoint(data: ValueGetter, haveZ: Boolean, haveM: Boolean): Point {
102+
val X = data.double
103+
val Y = data.double
104+
val result: Point
105+
result = if (haveZ) {
106+
val Z = data.double
107+
jtsFactory.createPoint(Coordinate(X, Y, Z))
108+
} else {
109+
jtsFactory.createPoint(Coordinate(X, Y))
110+
}
111+
if (haveM) { // skip M value
112+
data.double
113+
}
114+
return result
115+
}
116+
117+
/** Parse an Array of "full" Geometries */
118+
private fun parseGeometryArray(data: ValueGetter, container: Array<out Geometry?>, srid: Int) {
119+
for (i in container.indices) {
120+
container[i] = parseGeometry(data, srid, true)
121+
}
122+
}
123+
124+
/**
125+
* Parse an Array of "slim" Points (without endianness and type, part of
126+
* LinearRing and Linestring, but not MultiPoint!
127+
*
128+
* @param haveZ
129+
* @param haveM
130+
*/
131+
private fun parseCS(data: ValueGetter, haveZ: Boolean, haveM: Boolean): CoordinateSequence {
132+
val count = data.int
133+
val dims = if (haveZ) 3 else 2
134+
val cs: CoordinateSequence = PackedCoordinateSequence.Double(count, dims, 0)
135+
for (i in 0 until count) {
136+
for (d in 0 until dims) {
137+
cs.setOrdinate(i, d, data.double)
138+
}
139+
if (haveM) { // skip M value
140+
data.double
141+
}
142+
}
143+
return cs
144+
}
145+
146+
private fun parseMultiPoint(data: ValueGetter, srid: Int): MultiPoint {
147+
val points = arrayOfNulls<Point>(data.int)
148+
parseGeometryArray(data, points, srid)
149+
return jtsFactory.createMultiPoint(points)
150+
}
151+
152+
private fun parseLineString(data: ValueGetter, haveZ: Boolean, haveM: Boolean): LineString {
153+
return jtsFactory.createLineString(parseCS(data, haveZ, haveM))
154+
}
155+
156+
private fun parseLinearRing(data: ValueGetter, haveZ: Boolean, haveM: Boolean): LinearRing {
157+
return jtsFactory.createLinearRing(parseCS(data, haveZ, haveM))
158+
}
159+
160+
private fun parsePolygon(data: ValueGetter, haveZ: Boolean, haveM: Boolean, srid: Int): Polygon {
161+
val holecount = data.int - 1
162+
val rings = arrayOfNulls<LinearRing>(holecount)
163+
val shell = parseLinearRing(data, haveZ, haveM)
164+
shell.srid = srid
165+
for (i in 0 until holecount) {
166+
rings[i] = parseLinearRing(data, haveZ, haveM)
167+
rings[i]!!.srid = srid
168+
}
169+
return jtsFactory.createPolygon(shell, rings)
170+
}
171+
172+
private fun parseMultiLineString(data: ValueGetter, srid: Int): MultiLineString {
173+
val count = data.int
174+
val strings = arrayOfNulls<LineString>(count)
175+
parseGeometryArray(data, strings, srid)
176+
return jtsFactory.createMultiLineString(strings)
177+
}
178+
179+
private fun parseMultiPolygon(data: ValueGetter, srid: Int): MultiPolygon {
180+
val count = data.int
181+
val polys = arrayOfNulls<Polygon>(count)
182+
parseGeometryArray(data, polys, srid)
183+
return jtsFactory.createMultiPolygon(polys)
184+
}
185+
186+
private fun parseCollection(data: ValueGetter, srid: Int): GeometryCollection {
187+
val count = data.int
188+
val geoms = arrayOfNulls<Geometry>(count)
189+
parseGeometryArray(data, geoms, srid)
190+
return jtsFactory.createGeometryCollection(geoms)
191+
}
192+
193+
companion object {
194+
/**
195+
* Get the appropriate ValueGetter for my endianness
196+
*
197+
* @param bytes
198+
* The appropriate Byte Getter
199+
*
200+
* @return the ValueGetter
201+
*/
202+
fun valueGetterForEndian(bytes: ByteGetter): ValueGetter {
203+
return if (bytes[0] == ValueGetter.XDR.NUMBER.toInt()) { // XDR
204+
ValueGetter.XDR(bytes)
205+
} else if (bytes[0] == ValueGetter.NDR.NUMBER.toInt()) {
206+
ValueGetter.NDR(bytes)
207+
} else {
208+
throw IllegalArgumentException("Unknown Endian type:" + bytes[0])
209+
}
210+
}
211+
}
212+
}
213+
214+
object Geom {
215+
216+
// OpenGIS Geometry types as defined in the OGC WKB Spec
217+
// (May we replace this with an ENUM as soon as JDK 1.5
218+
// has gained widespread usage?)
219+
/** Fake type for linear ring */
220+
const val LINEARRING = 0
221+
222+
/**
223+
* The OGIS geometry type number for points.
224+
*/
225+
const val POINT = 1
226+
227+
/**
228+
* The OGIS geometry type number for lines.
229+
*/
230+
const val LINESTRING = 2
231+
232+
/**
233+
* The OGIS geometry type number for polygons.
234+
*/
235+
const val POLYGON = 3
236+
237+
/**
238+
* The OGIS geometry type number for aggregate points.
239+
*/
240+
const val MULTIPOINT = 4
241+
242+
/**
243+
* The OGIS geometry type number for aggregate lines.
244+
*/
245+
const val MULTILINESTRING = 5
246+
247+
/**
248+
* The OGIS geometry type number for aggregate polygons.
249+
*/
250+
const val MULTIPOLYGON = 6
251+
252+
/**
253+
* The OGIS geometry type number for feature collections.
254+
*/
255+
const val GEOMETRYCOLLECTION = 7
256+
val ALLTYPES = arrayOf(
257+
"", // internally used LinearRing does not have any text in front of
258+
// it
259+
"POINT", "LINESTRING", "POLYGON", "MULTIPOINT", "MULTILINESTRING",
260+
"MULTIPOLYGON", "GEOMETRYCOLLECTION"
261+
)
262+
263+
/**
264+
* The Text representations of the geometry types
265+
*
266+
* @param type int value of the type to lookup
267+
* @return String reprentation of the type.
268+
*/
269+
fun getTypeString(type: Int): String {
270+
return if (type >= 0 && type <= 7) {
271+
ALLTYPES[type]
272+
} else {
273+
throw IllegalArgumentException("Unknown Geometry type$type")
274+
}
275+
}
276+
277+
/**
278+
* Official UNKNOWN srid value
279+
*/
280+
const val UNKNOWN_SRID = 0
281+
282+
/**
283+
* Parse a SRID value, anything `<= 0` is unknown
284+
*
285+
* @param srid the SRID to parse
286+
* @return parsed SRID value
287+
*/
288+
fun parseSRID(input: Int): Int {
289+
var srid = input
290+
if (srid < 0) {
291+
/* TODO: raise a warning ? */
292+
srid = 0
293+
}
294+
return srid
295+
}
296+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.github.jasync.sql.db.postgis
2+
3+
abstract class ByteGetter {
4+
/**
5+
* Get a byte.
6+
*
7+
* @param index the index to get the value from
8+
* @return The result is returned as Int to eliminate sign problems when
9+
* or'ing several values together.
10+
*/
11+
abstract operator fun get(index: Int): Int
12+
class BinaryByteGetter(private val array: ByteArray) : ByteGetter() {
13+
override fun get(index: Int): Int {
14+
return array[index].toInt() and 0xFF // mask out sign-extended bits.
15+
}
16+
}
17+
18+
class StringByteGetter(private val rep: String) : ByteGetter() {
19+
override fun get(index: Int): Int {
20+
var index = index
21+
index *= 2
22+
val high = unhex(rep[index]).toInt()
23+
val low = unhex(rep[index + 1]).toInt()
24+
return (high shl 4) + low
25+
}
26+
27+
companion object {
28+
fun unhex(c: Char): Byte {
29+
return if (c >= '0' && c <= '9') {
30+
(c.code - '0'.code).toByte()
31+
} else if (c >= 'A' && c <= 'F') {
32+
(c.code - 'A'.code + 10).toByte()
33+
} else if (c >= 'a' && c <= 'f') {
34+
(c.code - 'a'.code + 10).toByte()
35+
} else {
36+
throw IllegalArgumentException("No valid Hex char $c")
37+
}
38+
}
39+
}
40+
}
41+
}

0 commit comments

Comments
 (0)