Skip to content

Commit

Permalink
Merge pull request #909 from Kotlin/geo
Browse files Browse the repository at this point in the history
GeoDataFrame init
  • Loading branch information
AndreiKingsley authored Nov 15, 2024
2 parents c66d4b3 + b425064 commit 1c6e323
Show file tree
Hide file tree
Showing 25 changed files with 880 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration
import org.jetbrains.kotlinx.jupyter.api.libraries.resources
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import kotlin.reflect.KType
import kotlin.reflect.full.isSubtypeOf

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

val version = options["v"]

private fun KotlinKernelHost.execute(codeWithConverter: CodeWithConverter, argument: String): VariableName? {
val code = codeWithConverter.with(argument)
return if (code.isNotBlank()) {
val result = execute(code)
if (codeWithConverter.hasConverter) {
result.name
} else {
null
}
} else {
null
}
}

private fun KotlinKernelHost.execute(
codeWithConverter: CodeWithConverter,
property: KProperty<*>,
type: KType,
): VariableName? {
val variableName = "(${property.name}${if (property.returnType.isMarkedNullable) "!!" else ""} as $type)"
return execute(codeWithConverter, variableName)
}

private fun KotlinKernelHost.updateImportDataSchemaVariable(
importDataSchema: ImportDataSchema,
property: KProperty<*>,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.jetbrains.kotlinx.dataframe.jupyter

import org.jetbrains.kotlinx.dataframe.codeGen.CodeWithConverter
import org.jetbrains.kotlinx.jupyter.api.KotlinKernelHost
import org.jetbrains.kotlinx.jupyter.api.VariableName
import kotlin.reflect.KProperty
import kotlin.reflect.KType

internal fun KotlinKernelHost.execute(codeWithConverter: CodeWithConverter, argument: String): VariableName? {
val code = codeWithConverter.with(argument)
return if (code.isNotBlank()) {
val result = execute(code)
if (codeWithConverter.hasConverter) {
result.name
} else {
null
}
} else {
null
}
}

internal fun KotlinKernelHost.execute(
codeWithConverter: CodeWithConverter,
property: KProperty<*>,
type: KType,
): VariableName? {
val variableName = "(${property.name}${if (property.returnType.isMarkedNullable) "!!" else ""} as $type)"
return execute(codeWithConverter, variableName)
}
6 changes: 6 additions & 0 deletions dataframe-geo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## :dataframe-geo

This module, published as `dataframe-geo`, contains all logic and tests for DataFrame to be able to work
with geographical data.

Experimental.
71 changes: 71 additions & 0 deletions dataframe-geo/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import org.jetbrains.kotlin.gradle.tasks.BaseKotlinCompile
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
with(libs.plugins) {
alias(kotlin.jvm)
alias(publisher)
alias(jupyter.api)
alias(ktlint)
alias(dataframe)
alias(ksp)
}
}

group = "org.jetbrains.kotlinx"

repositories {
// geo repository should come before Maven Central
maven("https://repo.osgeo.org/repository/release")
mavenCentral()
}

// https://stackoverflow.com/questions/26993105/i-get-an-error-downloading-javax-media-jai-core1-1-3-from-maven-central
// jai core dependency should be excluded from geotools dependencies and added separately
fun ExternalModuleDependency.excludeJaiCore() = exclude("javax.media", "jai_core")

dependencies {
api(project(":core"))

implementation(libs.geotools.main) { excludeJaiCore() }
implementation(libs.geotools.shapefile) { excludeJaiCore() }
implementation(libs.geotools.geojson) { excludeJaiCore() }
implementation(libs.geotools.referencing) { excludeJaiCore() }
implementation(libs.geotools.epsg.hsql) { excludeJaiCore() }

implementation(libs.jai.core)

implementation(libs.jts.core)
implementation(libs.jts.io.common)

implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)

testImplementation(kotlin("test"))
}

tasks.withType<KotlinCompile>().configureEach {
val friendModule = project(":core")
val jarTask = friendModule.tasks.getByName("jar") as Jar
val jarPath = jarTask.archiveFile.get().asFile.absolutePath
(this as BaseKotlinCompile).friendPaths.from(jarPath)
}

kotlinPublications {
publication {
publicationName = "dataframeGeo"
artifactId = "dataframe-geo"
description = "GeoDataFrame API"
packageName = artifactId
}
}

tasks.processJupyterApiResources {
libraryProducers = listOf("org.jetbrains.kotlinx.dataframe.jupyter.IntegrationGeo")
}

tasks.test {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package org.jetbrains.kotlinx.dataframe.geo

import org.geotools.api.referencing.crs.CoordinateReferenceSystem
import org.geotools.geometry.jts.JTS
import org.geotools.referencing.CRS
import org.jetbrains.kotlinx.dataframe.DataFrame
import org.jetbrains.kotlinx.dataframe.api.update
import org.jetbrains.kotlinx.dataframe.api.with

/**
* A data structure representing a geographical DataFrame, combining spatial data with
* an optional Coordinate Reference System (CRS).
*
* @param T The type parameter extending `WithGeometry`, indicating the presence of a geometry column.
* @property df The underlying `DataFrame` containing geometries.
* @property crs The coordinate reference system associated with the data, if any.
*/
class GeoDataFrame<T : WithGeometry>(val df: DataFrame<T>, val crs: CoordinateReferenceSystem?) {
/**
* Creates a new `GeoDataFrame` with the modified underlying DataFrame.
*
* @param block The block defining the transformations to be applied to the DataFrame.
* @return A new `GeoDataFrame` instance with updated dataframe and the same CRS.
*/
inline fun modify(block: DataFrame<T>.() -> DataFrame<T>): GeoDataFrame<T> = GeoDataFrame(df.block(), crs)

/**
* Transforms the geometries to a specified Coordinate Reference System (CRS).
*
* This function reprojects the geometry data from the current CRS to a target CRS.
* If no target CRS is specified and the `GeoDataFrame` has no CRS, WGS 84 is used by default.
*
* @param targetCrs The target CRS for transformation.
* @return A new `GeoDataFrame` with reprojected geometries and the specified CRS.
*/
fun applyCrs(targetCrs: CoordinateReferenceSystem): GeoDataFrame<T> {
if (crs == null) {
return GeoDataFrame(df, targetCrs)
}
if (targetCrs == this.crs) return this
// Use WGS 84 by default TODO
val sourceCRS: CoordinateReferenceSystem = this.crs
val transform = CRS.findMathTransform(sourceCRS, targetCrs, true)
return GeoDataFrame(
df.update { geometry }.with { JTS.transform(it, transform) },
targetCrs,
)
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as GeoDataFrame<*>

if (df != other.df) return false

return when {
crs == null && other.crs == null -> true
crs == null || other.crs == null -> false
else -> CRS.equalsIgnoreMetadata(crs, other.crs)
}
}

override fun hashCode(): Int {
var result = df.hashCode()
result = 31 * result + (crs?.hashCode() ?: 0)
return result
}

override fun toString(): String = "GeoDataFrame(df=$df, crs=$crs)"

companion object {
val DEFAULT_CRS = CRS.decode("EPSG:4326", true)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.jetbrains.kotlinx.dataframe.geo

import org.jetbrains.kotlinx.dataframe.annotations.DataSchema
import org.locationtech.jts.geom.Geometry
import org.locationtech.jts.geom.LineString
import org.locationtech.jts.geom.MultiLineString
import org.locationtech.jts.geom.MultiPoint
import org.locationtech.jts.geom.MultiPolygon
import org.locationtech.jts.geom.Point
import org.locationtech.jts.geom.Polygon

@DataSchema
interface WithGeometry {
val geometry: Geometry
}

@DataSchema
interface WithPolygonGeometry : WithGeometry {
override val geometry: Polygon
}

@DataSchema
interface WithMultiPolygonGeometry : WithGeometry {
override val geometry: MultiPolygon
}

@DataSchema
interface WithPointGeometry : WithGeometry {
override val geometry: Point
}

@DataSchema
interface WithMultiPointGeometry : WithGeometry {
override val geometry: MultiPoint
}

@DataSchema
interface WithLineStringGeometry : WithGeometry {
override val geometry: LineString
}

@DataSchema
interface WithMultiLineStringGeometry : WithGeometry {
override val geometry: MultiLineString
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.jetbrains.kotlinx.dataframe.geo

import org.geotools.geometry.jts.ReferencedEnvelope
import org.jetbrains.kotlinx.dataframe.api.asIterable
import org.jetbrains.kotlinx.dataframe.geo.jts.computeBounds

/**
* Computes the bounding envelope for all geometries in a `GeoDataFrame`,
* considering the specified coordinate reference system (CRS).
*
* @receiver The `GeoDataFrame` containing the geometries for which to compute bounds.
* @return The bounding envelope that includes all geometries,
* associated with the CRS of the `GeoDataFrame`.
*/
fun GeoDataFrame<*>.bounds(): ReferencedEnvelope = ReferencedEnvelope(df.geometry.asIterable().computeBounds(), crs)
Loading

0 comments on commit 1c6e323

Please sign in to comment.