Skip to content

Commit 1c6e323

Browse files
Merge pull request #909 from Kotlin/geo
GeoDataFrame init
2 parents c66d4b3 + b425064 commit 1c6e323

File tree

25 files changed

+880
-26
lines changed

25 files changed

+880
-26
lines changed

core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/Integration.kt

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration
5757
import org.jetbrains.kotlinx.jupyter.api.libraries.resources
5858
import kotlin.reflect.KClass
5959
import kotlin.reflect.KProperty
60-
import kotlin.reflect.KType
6160
import kotlin.reflect.full.isSubtypeOf
6261

6362
/** Users will get an error if their Kotlin Jupyter kernel is older than this version. */
@@ -70,29 +69,6 @@ internal class Integration(private val notebook: Notebook, private val options:
7069

7170
val version = options["v"]
7271

73-
private fun KotlinKernelHost.execute(codeWithConverter: CodeWithConverter, argument: String): VariableName? {
74-
val code = codeWithConverter.with(argument)
75-
return if (code.isNotBlank()) {
76-
val result = execute(code)
77-
if (codeWithConverter.hasConverter) {
78-
result.name
79-
} else {
80-
null
81-
}
82-
} else {
83-
null
84-
}
85-
}
86-
87-
private fun KotlinKernelHost.execute(
88-
codeWithConverter: CodeWithConverter,
89-
property: KProperty<*>,
90-
type: KType,
91-
): VariableName? {
92-
val variableName = "(${property.name}${if (property.returnType.isMarkedNullable) "!!" else ""} as $type)"
93-
return execute(codeWithConverter, variableName)
94-
}
95-
9672
private fun KotlinKernelHost.updateImportDataSchemaVariable(
9773
importDataSchema: ImportDataSchema,
9874
property: KProperty<*>,
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package org.jetbrains.kotlinx.dataframe.jupyter
2+
3+
import org.jetbrains.kotlinx.dataframe.codeGen.CodeWithConverter
4+
import org.jetbrains.kotlinx.jupyter.api.KotlinKernelHost
5+
import org.jetbrains.kotlinx.jupyter.api.VariableName
6+
import kotlin.reflect.KProperty
7+
import kotlin.reflect.KType
8+
9+
internal fun KotlinKernelHost.execute(codeWithConverter: CodeWithConverter, argument: String): VariableName? {
10+
val code = codeWithConverter.with(argument)
11+
return if (code.isNotBlank()) {
12+
val result = execute(code)
13+
if (codeWithConverter.hasConverter) {
14+
result.name
15+
} else {
16+
null
17+
}
18+
} else {
19+
null
20+
}
21+
}
22+
23+
internal fun KotlinKernelHost.execute(
24+
codeWithConverter: CodeWithConverter,
25+
property: KProperty<*>,
26+
type: KType,
27+
): VariableName? {
28+
val variableName = "(${property.name}${if (property.returnType.isMarkedNullable) "!!" else ""} as $type)"
29+
return execute(codeWithConverter, variableName)
30+
}

dataframe-geo/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
## :dataframe-geo
2+
3+
This module, published as `dataframe-geo`, contains all logic and tests for DataFrame to be able to work
4+
with geographical data.
5+
6+
Experimental.

dataframe-geo/build.gradle.kts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import org.jetbrains.kotlin.gradle.tasks.BaseKotlinCompile
2+
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
3+
4+
plugins {
5+
with(libs.plugins) {
6+
alias(kotlin.jvm)
7+
alias(publisher)
8+
alias(jupyter.api)
9+
alias(ktlint)
10+
alias(dataframe)
11+
alias(ksp)
12+
}
13+
}
14+
15+
group = "org.jetbrains.kotlinx"
16+
17+
repositories {
18+
// geo repository should come before Maven Central
19+
maven("https://repo.osgeo.org/repository/release")
20+
mavenCentral()
21+
}
22+
23+
// https://stackoverflow.com/questions/26993105/i-get-an-error-downloading-javax-media-jai-core1-1-3-from-maven-central
24+
// jai core dependency should be excluded from geotools dependencies and added separately
25+
fun ExternalModuleDependency.excludeJaiCore() = exclude("javax.media", "jai_core")
26+
27+
dependencies {
28+
api(project(":core"))
29+
30+
implementation(libs.geotools.main) { excludeJaiCore() }
31+
implementation(libs.geotools.shapefile) { excludeJaiCore() }
32+
implementation(libs.geotools.geojson) { excludeJaiCore() }
33+
implementation(libs.geotools.referencing) { excludeJaiCore() }
34+
implementation(libs.geotools.epsg.hsql) { excludeJaiCore() }
35+
36+
implementation(libs.jai.core)
37+
38+
implementation(libs.jts.core)
39+
implementation(libs.jts.io.common)
40+
41+
implementation(libs.ktor.client.core)
42+
implementation(libs.ktor.client.cio)
43+
implementation(libs.ktor.client.content.negotiation)
44+
implementation(libs.ktor.serialization.kotlinx.json)
45+
46+
testImplementation(kotlin("test"))
47+
}
48+
49+
tasks.withType<KotlinCompile>().configureEach {
50+
val friendModule = project(":core")
51+
val jarTask = friendModule.tasks.getByName("jar") as Jar
52+
val jarPath = jarTask.archiveFile.get().asFile.absolutePath
53+
(this as BaseKotlinCompile).friendPaths.from(jarPath)
54+
}
55+
56+
kotlinPublications {
57+
publication {
58+
publicationName = "dataframeGeo"
59+
artifactId = "dataframe-geo"
60+
description = "GeoDataFrame API"
61+
packageName = artifactId
62+
}
63+
}
64+
65+
tasks.processJupyterApiResources {
66+
libraryProducers = listOf("org.jetbrains.kotlinx.dataframe.jupyter.IntegrationGeo")
67+
}
68+
69+
tasks.test {
70+
useJUnitPlatform()
71+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package org.jetbrains.kotlinx.dataframe.geo
2+
3+
import org.geotools.api.referencing.crs.CoordinateReferenceSystem
4+
import org.geotools.geometry.jts.JTS
5+
import org.geotools.referencing.CRS
6+
import org.jetbrains.kotlinx.dataframe.DataFrame
7+
import org.jetbrains.kotlinx.dataframe.api.update
8+
import org.jetbrains.kotlinx.dataframe.api.with
9+
10+
/**
11+
* A data structure representing a geographical DataFrame, combining spatial data with
12+
* an optional Coordinate Reference System (CRS).
13+
*
14+
* @param T The type parameter extending `WithGeometry`, indicating the presence of a geometry column.
15+
* @property df The underlying `DataFrame` containing geometries.
16+
* @property crs The coordinate reference system associated with the data, if any.
17+
*/
18+
class GeoDataFrame<T : WithGeometry>(val df: DataFrame<T>, val crs: CoordinateReferenceSystem?) {
19+
/**
20+
* Creates a new `GeoDataFrame` with the modified underlying DataFrame.
21+
*
22+
* @param block The block defining the transformations to be applied to the DataFrame.
23+
* @return A new `GeoDataFrame` instance with updated dataframe and the same CRS.
24+
*/
25+
inline fun modify(block: DataFrame<T>.() -> DataFrame<T>): GeoDataFrame<T> = GeoDataFrame(df.block(), crs)
26+
27+
/**
28+
* Transforms the geometries to a specified Coordinate Reference System (CRS).
29+
*
30+
* This function reprojects the geometry data from the current CRS to a target CRS.
31+
* If no target CRS is specified and the `GeoDataFrame` has no CRS, WGS 84 is used by default.
32+
*
33+
* @param targetCrs The target CRS for transformation.
34+
* @return A new `GeoDataFrame` with reprojected geometries and the specified CRS.
35+
*/
36+
fun applyCrs(targetCrs: CoordinateReferenceSystem): GeoDataFrame<T> {
37+
if (crs == null) {
38+
return GeoDataFrame(df, targetCrs)
39+
}
40+
if (targetCrs == this.crs) return this
41+
// Use WGS 84 by default TODO
42+
val sourceCRS: CoordinateReferenceSystem = this.crs
43+
val transform = CRS.findMathTransform(sourceCRS, targetCrs, true)
44+
return GeoDataFrame(
45+
df.update { geometry }.with { JTS.transform(it, transform) },
46+
targetCrs,
47+
)
48+
}
49+
50+
override fun equals(other: Any?): Boolean {
51+
if (this === other) return true
52+
if (javaClass != other?.javaClass) return false
53+
54+
other as GeoDataFrame<*>
55+
56+
if (df != other.df) return false
57+
58+
return when {
59+
crs == null && other.crs == null -> true
60+
crs == null || other.crs == null -> false
61+
else -> CRS.equalsIgnoreMetadata(crs, other.crs)
62+
}
63+
}
64+
65+
override fun hashCode(): Int {
66+
var result = df.hashCode()
67+
result = 31 * result + (crs?.hashCode() ?: 0)
68+
return result
69+
}
70+
71+
override fun toString(): String = "GeoDataFrame(df=$df, crs=$crs)"
72+
73+
companion object {
74+
val DEFAULT_CRS = CRS.decode("EPSG:4326", true)
75+
}
76+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package org.jetbrains.kotlinx.dataframe.geo
2+
3+
import org.jetbrains.kotlinx.dataframe.annotations.DataSchema
4+
import org.locationtech.jts.geom.Geometry
5+
import org.locationtech.jts.geom.LineString
6+
import org.locationtech.jts.geom.MultiLineString
7+
import org.locationtech.jts.geom.MultiPoint
8+
import org.locationtech.jts.geom.MultiPolygon
9+
import org.locationtech.jts.geom.Point
10+
import org.locationtech.jts.geom.Polygon
11+
12+
@DataSchema
13+
interface WithGeometry {
14+
val geometry: Geometry
15+
}
16+
17+
@DataSchema
18+
interface WithPolygonGeometry : WithGeometry {
19+
override val geometry: Polygon
20+
}
21+
22+
@DataSchema
23+
interface WithMultiPolygonGeometry : WithGeometry {
24+
override val geometry: MultiPolygon
25+
}
26+
27+
@DataSchema
28+
interface WithPointGeometry : WithGeometry {
29+
override val geometry: Point
30+
}
31+
32+
@DataSchema
33+
interface WithMultiPointGeometry : WithGeometry {
34+
override val geometry: MultiPoint
35+
}
36+
37+
@DataSchema
38+
interface WithLineStringGeometry : WithGeometry {
39+
override val geometry: LineString
40+
}
41+
42+
@DataSchema
43+
interface WithMultiLineStringGeometry : WithGeometry {
44+
override val geometry: MultiLineString
45+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.jetbrains.kotlinx.dataframe.geo
2+
3+
import org.geotools.geometry.jts.ReferencedEnvelope
4+
import org.jetbrains.kotlinx.dataframe.api.asIterable
5+
import org.jetbrains.kotlinx.dataframe.geo.jts.computeBounds
6+
7+
/**
8+
* Computes the bounding envelope for all geometries in a `GeoDataFrame`,
9+
* considering the specified coordinate reference system (CRS).
10+
*
11+
* @receiver The `GeoDataFrame` containing the geometries for which to compute bounds.
12+
* @return The bounding envelope that includes all geometries,
13+
* associated with the CRS of the `GeoDataFrame`.
14+
*/
15+
fun GeoDataFrame<*>.bounds(): ReferencedEnvelope = ReferencedEnvelope(df.geometry.asIterable().computeBounds(), crs)

0 commit comments

Comments
 (0)