From f0967bb3ebecd83a7dc140b2189b8b043e32c7bf Mon Sep 17 00:00:00 2001 From: Andrei Date: Fri, 4 Oct 2024 13:14:03 +0400 Subject: [PATCH 1/5] GeoDataFrame init --- dataframe-geo/build.gradle.kts | 64 +++++++++ .../kotlinx/dataframe/geo/GeoDataFrame.kt | 29 +++++ .../kotlinx/dataframe/geo/GeoFrame.kt | 35 +++++ .../jetbrains/kotlinx/dataframe/geo/bounds.kt | 9 ++ .../kotlinx/dataframe/geo/geocode/Geocoder.kt | 121 ++++++++++++++++++ .../dataframe/geo/geotools/ToGeoDataFrame.kt | 56 ++++++++ .../geo/geotools/toSimpleFeatureCollection.kt | 46 +++++++ .../kotlinx/dataframe/geo/io/read.kt | 43 +++++++ .../kotlinx/dataframe/geo/io/write.kt | 58 +++++++++ .../kotlinx/dataframe/geo/jts/bounds.kt | 10 ++ .../dataframe/geo/jts/geometryExtenstions.kt | 24 ++++ .../jetbrains/kotlinx/dataframe/geo/toGeo.kt | 9 ++ .../dataframe/jupyter/IntegrationGeo.kt | 84 ++++++++++++ gradle/libs.versions.toml | 23 +++- settings.gradle.kts | 1 + 15 files changed, 610 insertions(+), 2 deletions(-) create mode 100644 dataframe-geo/build.gradle.kts create mode 100644 dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/GeoDataFrame.kt create mode 100644 dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/GeoFrame.kt create mode 100644 dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/bounds.kt create mode 100644 dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geocode/Geocoder.kt create mode 100644 dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geotools/ToGeoDataFrame.kt create mode 100644 dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geotools/toSimpleFeatureCollection.kt create mode 100644 dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/read.kt create mode 100644 dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/write.kt create mode 100644 dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/jts/bounds.kt create mode 100644 dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/jts/geometryExtenstions.kt create mode 100644 dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/toGeo.kt create mode 100644 dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/IntegrationGeo.kt diff --git a/dataframe-geo/build.gradle.kts b/dataframe-geo/build.gradle.kts new file mode 100644 index 000000000..34ec9a11a --- /dev/null +++ b/dataframe-geo/build.gradle.kts @@ -0,0 +1,64 @@ +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) + } +} + +group = "org.jetbrains.kotlinx" + +repositories { + // geo repositories should come before Maven Central + maven("https://maven.geotoolkit.org") + 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) + +} + +tasks.withType().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) +} + +tasks.processJupyterApiResources { + libraryProducers = listOf("org.jetbrains.kotlinx.dataframe.jupyter.IntegrationGeo") +} + + +tasks.test { + useJUnitPlatform() +} +kotlin { + jvmToolchain(11) +} diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/GeoDataFrame.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/GeoDataFrame.kt new file mode 100644 index 000000000..a70d1d53c --- /dev/null +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/GeoDataFrame.kt @@ -0,0 +1,29 @@ +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 + +class GeoDataFrame(val df: DataFrame, val crs: CoordinateReferenceSystem?) { + fun update(updateBlock: DataFrame.() -> DataFrame): GeoDataFrame { + return GeoDataFrame(df.updateBlock(), crs) + } + + fun applyCRS(targetCRS: CoordinateReferenceSystem? = null): GeoDataFrame { + if (targetCRS == this.crs) return this + // Use WGS 84 by default TODO + val sourceCRS: CoordinateReferenceSystem = this.crs ?: DEFAULT_CRS + val transform = CRS.findMathTransform(sourceCRS, targetCRS, true) + return GeoDataFrame( + df.update { geometry }.with { JTS.transform(it, transform) }, + targetCRS + ) + } + + companion object { + val DEFAULT_CRS = CRS.decode("EPSG:4326", true) + } +} diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/GeoFrame.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/GeoFrame.kt new file mode 100644 index 000000000..ba0fab552 --- /dev/null +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/GeoFrame.kt @@ -0,0 +1,35 @@ +package org.jetbrains.kotlinx.dataframe.geo + +import org.jetbrains.kotlinx.dataframe.ColumnsContainer +import org.jetbrains.kotlinx.dataframe.DataColumn +import org.jetbrains.kotlinx.dataframe.annotations.DataSchema +import org.locationtech.jts.geom.Geometry +import org.locationtech.jts.geom.MultiPolygon +import org.locationtech.jts.geom.Polygon + +@DataSchema +interface GeoFrame { + val geometry: Geometry +} + +@DataSchema +interface PolygonGeoFrame : GeoFrame { + override val geometry: Polygon +} + +@DataSchema +interface MultiPolygonGeoFrame : GeoFrame { + override val geometry: MultiPolygon +} + +@get:JvmName("geometry") +val ColumnsContainer.geometry: DataColumn + get() = get("geometry") as DataColumn + +@get:JvmName("geometryPolygon") +val ColumnsContainer.geometry: DataColumn + get() = get("geometry") as DataColumn + +@get:JvmName("geometryMultiPolygon") +val ColumnsContainer.geometry: DataColumn + get() = get("geometry") as DataColumn diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/bounds.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/bounds.kt new file mode 100644 index 000000000..4b057f3ed --- /dev/null +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/bounds.kt @@ -0,0 +1,9 @@ +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 + +fun GeoDataFrame<*>.bounds(): ReferencedEnvelope { + return ReferencedEnvelope(df.geometry.asIterable().computeBounds(), crs) +} diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geocode/Geocoder.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geocode/Geocoder.kt new file mode 100644 index 000000000..5674ef615 --- /dev/null +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geocode/Geocoder.kt @@ -0,0 +1,121 @@ +package org.jetbrains.kotlinx.dataframe.geo.geocode + +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.client.statement.bodyAsText +import io.ktor.http.ContentType +import io.ktor.http.contentType +import io.ktor.serialization.kotlinx.json.json +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import org.jetbrains.kotlinx.dataframe.api.dataFrameOf +import org.jetbrains.kotlinx.dataframe.geo.GeoDataFrame +import org.jetbrains.kotlinx.dataframe.geo.toGeo +import org.locationtech.jts.geom.Geometry +import org.locationtech.jts.geom.GeometryFactory +import org.locationtech.jts.io.geojson.GeoJsonReader + + +object Geocoder { + + private val url = "https://geo2.datalore.jetbrains.com/map_data/geocoding" + + private fun countryQuery(country: String) = """ { + "region_query_names" : [ "$country" ], + "region_query_countries" : null, + "region_query_states" : null, + "region_query_counties" : null, + "ambiguity_resolver" : { + "ambiguity_resolver_ignoring_strategy" : null, + "ambiguity_resolver_box" : null, + "ambiguity_resolver_closest_coord" : null + } + } + """.trimIndent() + + private fun geocodeQuery(countries: List) = """ +{ + "version" : 3, + "mode" : "by_geocoding", + "feature_options" : [ "limit", "position", "centroid" ], + "resolution" : null, + "view_box" : null, + "fetched_ids" : null, + "region_queries" : [ + ${countries.joinToString(",\n") { countryQuery(it) }} + ], + "scope" : [ ], + "level" : "country", + "namesake_example_limit" : 10, + "allow_ambiguous" : false +} +""".trimIndent() + + private fun idsQuery(ids: List) = """ + {"version": 3, + "mode": "by_id", + "feature_options": ["boundary"], + "resolution": 5, + "view_box": null, + "fetched_ids": null, + "ids": [${ids.joinToString(", ") { "\"" + it + "\"" }}]} + """.trimIndent() + + private val client = HttpClient(CIO) { + install(ContentNegotiation) { + json(Json { + prettyPrint = true + isLenient = true + }) + } + } + + fun geocodeCountries(countries: List): GeoDataFrame<*> { + + val query = geocodeQuery(countries) + val foundNames = mutableListOf() + val geometries = mutableListOf() + runBlocking { + val responseString = client.post(url) { + contentType(ContentType.Application.Json) + // headers[HttpHeaders.AcceptEncoding] = "gzip" + setBody(query) + }.bodyAsText() + val ids = mutableListOf() + + Json.parseToJsonElement(responseString).jsonObject["data"]!!.jsonObject["answers"]!!.jsonArray.forEach { + it.jsonObject["features"]!!.jsonArray.single().jsonObject.also { + foundNames.add(it["name"]!!.jsonPrimitive.content) + ids.add(it["id"]!!.jsonPrimitive.content) + } + } + val idsQuery = idsQuery(ids) + + val responseStringGeometries = client.post(url) { + contentType(ContentType.Application.Json) + // headers[HttpHeaders.AcceptEncoding] = "gzip" + setBody(idsQuery) + }.bodyAsText() + + val geoJsonReader = GeoJsonReader(GeometryFactory()) + Json.parseToJsonElement(responseStringGeometries).jsonObject["data"]!!.jsonObject["answers"]!!.jsonArray.forEach { + it.jsonObject["features"]!!.jsonArray.single().jsonObject.also { + val boundary = it["boundary"]!!.jsonPrimitive.content + geometries.add(geoJsonReader.read(boundary)) + } + } + + } + return dataFrameOf( + "country" to countries, + "foundName" to foundNames, + "geometry" to geometries, + ).toGeo() + } +} diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geotools/ToGeoDataFrame.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geotools/ToGeoDataFrame.kt new file mode 100644 index 000000000..ba79545b9 --- /dev/null +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geotools/ToGeoDataFrame.kt @@ -0,0 +1,56 @@ +package org.jetbrains.kotlinx.dataframe.geo.geotools + +import org.geotools.api.feature.simple.SimpleFeature +import org.geotools.api.feature.simple.SimpleFeatureType +import org.geotools.api.feature.type.GeometryDescriptor +import org.geotools.api.referencing.crs.CoordinateReferenceSystem +import org.geotools.data.simple.SimpleFeatureCollection +import org.jetbrains.kotlinx.dataframe.DataColumn +import org.jetbrains.kotlinx.dataframe.DataFrame +import org.jetbrains.kotlinx.dataframe.api.Infer +import org.jetbrains.kotlinx.dataframe.api.toDataFrame +import org.jetbrains.kotlinx.dataframe.geo.GeoDataFrame +import org.jetbrains.kotlinx.dataframe.geo.GeoFrame +import org.locationtech.jts.geom.Geometry + +fun SimpleFeatureCollection.toGeoDataFrame(): GeoDataFrame<*> { + + require(schema is SimpleFeatureType) { + "GeoTools: SimpleFeatureType expected but was: ${schema::class.simpleName}" + } + val attributeDescriptors = (schema as SimpleFeatureType).attributeDescriptors + + val dataAttributes = attributeDescriptors?.filter { it !is GeometryDescriptor }?.map { it!! } ?: emptyList() + val geometryAttribute = attributeDescriptors?.find { it is GeometryDescriptor } + ?: throw IllegalArgumentException("No geometry attribute") + + // In GeoJSON the crs attribute is optional + val crs: CoordinateReferenceSystem? = (geometryAttribute as GeometryDescriptor).coordinateReferenceSystem + + val data = dataAttributes.associate { it.localName to ArrayList() } + val geometries = ArrayList() + + features().use { + while (it.hasNext()) { + val feature = it.next() + require(feature is SimpleFeature) { + "GeoTools: SimpleFeature expected but was: ${feature::class.simpleName}" + } + val featureGeometry = feature.getAttribute(geometryAttribute.name) + + require(featureGeometry is Geometry) { + "Not a geometry: [${geometryAttribute.name}] = ${featureGeometry?.javaClass?.simpleName} (feature id: ${feature.id})" + } + // TODO require(featureGeometry.isValid) { "Invalid geometry, feature id: ${feature.id}" } + + for (dataAttribute in dataAttributes) { + data[dataAttribute.localName]?.add(feature.getAttribute(dataAttribute.name)) + } + geometries.add(featureGeometry) + } + } + + val geometryColumn = DataColumn.create("geometry", geometries, Infer.Type) + + return GeoDataFrame((data.toDataFrame() + geometryColumn) as DataFrame, crs) +} diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geotools/toSimpleFeatureCollection.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geotools/toSimpleFeatureCollection.kt new file mode 100644 index 000000000..06ef8b3d7 --- /dev/null +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geotools/toSimpleFeatureCollection.kt @@ -0,0 +1,46 @@ +package org.jetbrains.kotlinx.dataframe.geo.geotools + +import org.geotools.api.feature.simple.SimpleFeature +import org.geotools.data.collection.ListFeatureCollection +import org.geotools.data.simple.SimpleFeatureCollection +import org.geotools.feature.simple.SimpleFeatureBuilder +import org.geotools.feature.simple.SimpleFeatureTypeBuilder +import org.jetbrains.kotlinx.dataframe.api.forEach +import org.jetbrains.kotlinx.dataframe.api.map +import org.jetbrains.kotlinx.dataframe.api.single +import org.jetbrains.kotlinx.dataframe.geo.GeoDataFrame +import org.locationtech.jts.geom.Geometry + +fun GeoDataFrame<*>.toSimpleFeatureCollection( + name: String? = null, + singleGeometryType: Boolean = false +): SimpleFeatureCollection { + val typeBuilder = SimpleFeatureTypeBuilder() + typeBuilder.name = name ?: "geodata" + typeBuilder.setCRS(crs) + val geometryClass = if (singleGeometryType) { + // todo singleOrNull() ?: error() + df["geometry"].map { it!!::class.java }.distinct().single() + } else Geometry::class.java + typeBuilder.add("the_geom", geometryClass) + df.columnNames().filter { it != "geometry" }.forEach { colName -> + typeBuilder.add(colName, String::class.java) + } + val featureType = typeBuilder.buildFeatureType() + + val featureCollection = ListFeatureCollection(featureType) + + val featureBuilder = SimpleFeatureBuilder(featureType) + + df.forEach { row -> + val geometry = row["geometry"] + featureBuilder.add(geometry) + df.columnNames().filter { it != "geometry" }.forEach { colName -> + featureBuilder.add(row[colName]) + } + val feature: SimpleFeature = featureBuilder.buildFeature(null) + featureCollection.add(feature) + } + + return featureCollection +} diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/read.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/read.kt new file mode 100644 index 000000000..4bb604385 --- /dev/null +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/read.kt @@ -0,0 +1,43 @@ +package org.jetbrains.kotlinx.dataframe.geo.io + +import org.geotools.data.shapefile.ShapefileDataStoreFactory +import org.geotools.data.simple.SimpleFeatureCollection +import org.geotools.geojson.feature.FeatureJSON +import org.jetbrains.kotlinx.dataframe.DataFrame +import org.jetbrains.kotlinx.dataframe.geo.GeoDataFrame +import org.jetbrains.kotlinx.dataframe.geo.geotools.toGeoDataFrame +import org.jetbrains.kotlinx.dataframe.io.asURL +import java.net.URL + + +fun GeoDataFrame.Companion.readGeoJSON(path: String): GeoDataFrame<*> { + return readGeoJSON(asURL(path)) +} + +fun GeoDataFrame.Companion.readGeoJSON(url: URL): GeoDataFrame<*> { + return (FeatureJSON().readFeatureCollection(url.openStream()) as SimpleFeatureCollection).toGeoDataFrame() +} + +fun DataFrame.Companion.readGeoJSON(path: String): GeoDataFrame<*> { + return GeoDataFrame.readGeoJSON(path) +} + +fun DataFrame.Companion.readGeoJSON(url: URL): GeoDataFrame<*> { + return GeoDataFrame.readGeoJSON(url) +} + +fun GeoDataFrame.Companion.readShapefile(path: String): GeoDataFrame<*> { + return readShapefile(asURL(path)) +} + +fun GeoDataFrame.Companion.readShapefile(url: URL): GeoDataFrame<*> { + return ShapefileDataStoreFactory().createDataStore(url).featureSource.features.toGeoDataFrame() +} + +fun DataFrame.Companion.readShapefile(path: String): GeoDataFrame<*> { + return GeoDataFrame.readShapefile(path) +} + +fun DataFrame.Companion.readShapefile(url: URL): GeoDataFrame<*> { + return GeoDataFrame.readShapefile(url) +} diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/write.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/write.kt new file mode 100644 index 000000000..1886cc687 --- /dev/null +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/write.kt @@ -0,0 +1,58 @@ +package org.jetbrains.kotlinx.dataframe.geo.io + +import org.geotools.api.data.FileDataStoreFinder +import org.geotools.api.data.SimpleFeatureStore +import org.geotools.api.data.Transaction +import org.geotools.geojson.feature.FeatureJSON +import org.jetbrains.kotlinx.dataframe.geo.GeoDataFrame +import org.jetbrains.kotlinx.dataframe.geo.geotools.toSimpleFeatureCollection +import java.io.File + + +fun GeoDataFrame<*>.writeGeoJson(path: String): Unit = writeGeoJson(File(path)) + +fun GeoDataFrame<*>.writeGeoJson(file: File) { + val featureJSON = FeatureJSON() + file.outputStream().use { outputStream -> + featureJSON.writeFeatureCollection(toSimpleFeatureCollection(), outputStream) + } +} + +fun GeoDataFrame<*>.writeShapefile(directoryPath: String): Unit = writeShapefile(File(directoryPath)) + +fun GeoDataFrame<*>.writeShapefile(directory: File) { + + if (!directory.exists()) { + directory.mkdirs() + } + val fileName = directory.name + + val file = File(directory, "$fileName.shp") + + val creationParams = mutableMapOf() + creationParams["url"] = file.toURI().toURL() + + + val factory = FileDataStoreFinder.getDataStoreFactory("shp") + val dataStore = factory.createNewDataStore(creationParams) + + val featureCollection = toSimpleFeatureCollection(fileName, true) + + val schema = featureCollection.schema + + dataStore.createSchema(schema) + + val featureSource = dataStore.getFeatureSource(fileName) as SimpleFeatureStore + val transaction = Transaction.AUTO_COMMIT + + try { + featureSource.addFeatures(featureCollection) + transaction.commit() + } catch (e: Exception) { + e.printStackTrace() + transaction.rollback() + } finally { + transaction.close() + } + +} diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/jts/bounds.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/jts/bounds.kt new file mode 100644 index 000000000..36dd2968d --- /dev/null +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/jts/bounds.kt @@ -0,0 +1,10 @@ +package org.jetbrains.kotlinx.dataframe.geo.jts + +import org.locationtech.jts.geom.Envelope +import org.locationtech.jts.geom.Geometry + +fun Iterable.computeBounds(): Envelope { + val bounds = Envelope() + forEach { geometry -> bounds.expandToInclude(geometry.envelopeInternal) } + return bounds +} diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/jts/geometryExtenstions.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/jts/geometryExtenstions.kt new file mode 100644 index 000000000..3814577f8 --- /dev/null +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/jts/geometryExtenstions.kt @@ -0,0 +1,24 @@ +package org.jetbrains.kotlinx.dataframe.geo.jts + +import org.locationtech.jts.geom.Geometry +import org.locationtech.jts.geom.util.AffineTransformation + +fun Geometry.scale(value: Double): Geometry { + + val centroid = centroid.coordinate + + val moveToOrigin = AffineTransformation + .translationInstance(-centroid.x, -centroid.y) + + val scale = AffineTransformation.scaleInstance(value, value) + + val moveBack = AffineTransformation.translationInstance(centroid.x, centroid.y) + + val transformation = moveToOrigin.compose(scale).compose(moveBack) + + return transformation.transform(this) +} + +fun Geometry.translate(valueX: Double, valueY: Double): Geometry { + return AffineTransformation().translate(valueX, valueY).transform(this) +} diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/toGeo.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/toGeo.kt new file mode 100644 index 000000000..eaee7bd23 --- /dev/null +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/toGeo.kt @@ -0,0 +1,9 @@ +package org.jetbrains.kotlinx.dataframe.geo + +import org.geotools.api.referencing.crs.CoordinateReferenceSystem +import org.jetbrains.kotlinx.dataframe.AnyFrame +import org.jetbrains.kotlinx.dataframe.DataFrame + +fun AnyFrame.toGeo(crs: CoordinateReferenceSystem? = null): GeoDataFrame<*> = GeoDataFrame( + this as DataFrame, crs +) diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/IntegrationGeo.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/IntegrationGeo.kt new file mode 100644 index 000000000..0dc273caf --- /dev/null +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/IntegrationGeo.kt @@ -0,0 +1,84 @@ +@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + +package org.jetbrains.kotlinx.dataframe.jupyter + + +import org.jetbrains.kotlinx.dataframe.codeGen.CodeWithConverter +import org.jetbrains.kotlinx.dataframe.geo.GeoDataFrame +import org.jetbrains.kotlinx.dataframe.geo.GeoFrame +import org.jetbrains.kotlinx.dataframe.geo.MultiPolygonGeoFrame +import org.jetbrains.kotlinx.dataframe.geo.PolygonGeoFrame +import org.jetbrains.kotlinx.dataframe.impl.codeGen.ReplCodeGeneratorImpl +import org.jetbrains.kotlinx.jupyter.api.FieldHandler +import org.jetbrains.kotlinx.jupyter.api.FieldHandlerExecution +import org.jetbrains.kotlinx.jupyter.api.KotlinKernelHost +import org.jetbrains.kotlinx.jupyter.api.VariableName +import org.jetbrains.kotlinx.jupyter.api.libraries.FieldHandlerFactory +import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration +import kotlin.reflect.KProperty +import kotlin.reflect.KType +import kotlin.reflect.full.isSubtypeOf +import kotlin.reflect.typeOf + +internal class IntegrationGeo : JupyterIntegration() { + + // TODO make internal in core and use here + 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 + } + } + + // TODO make internal in core and use here + 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) + } + + override fun Builder.onLoaded() { + import("org.jetbrains.kotlinx.dataframe.geo.*") + import("org.jetbrains.kotlinx.dataframe.geo.io.*") + import("org.jetbrains.kotlinx.dataframe.geo.jts.*") + import("org.jetbrains.kotlinx.dataframe.geo.geotools.*") + import("org.jetbrains.kotlinx.dataframe.geo.geocode.*") + onLoaded { + useSchema() + useSchema() + useSchema() + } + val replCodeGeneratorImpl = ReplCodeGeneratorImpl() + replCodeGeneratorImpl.process(GeoFrame::class) + replCodeGeneratorImpl.process(PolygonGeoFrame::class) + replCodeGeneratorImpl.process(MultiPolygonGeoFrame::class) + val execution = FieldHandlerFactory.createUpdateExecution> { geo, kProperty -> + // TODO rewrite + val generatedDf = execute( + codeWithConverter = replCodeGeneratorImpl.process(geo.df, kProperty), + "(${kProperty.name}.df as DataFrame<*>)" + ) + val name = execute("GeoDataFrame($generatedDf, ${kProperty.name}.crs)").name + name + } + + + addTypeConverter(object : FieldHandler { + override val execution: FieldHandlerExecution<*> = execution + + override fun accepts(value: Any?, property: KProperty<*>): Boolean { + return property.returnType.isSubtypeOf(typeOf>()) + } + }) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9d32ab1ce..75f83f2ab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,11 +50,15 @@ dependencyVersions = "0.51.0" plugin-publish = "1.2.1" shadow = "8.1.1" android-gradle-api = "7.3.1" # Can't be updated to 7.4.0+ due to Java 8 compatibility -ktor-server-netty = "2.3.12" +ktor = "2.3.12" kotlin-compile-testing = "1.6.0" duckdb = "1.0.0" buildconfig = "5.4.0" +geotools = "32.0" +jai-core = "1.1.3" +jts = "1.20.0" + [libraries] ksp-gradle = { group = "com.google.devtools.ksp", name = "symbol-processing-gradle-plugin", version.ref = "ksp" } ksp-api = { group = "com.google.devtools.ksp", name = "symbol-processing-api", version.ref = "ksp" } @@ -103,6 +107,21 @@ arrow-vector = { group = "org.apache.arrow", name = "arrow-vector", version.ref arrow-memory = { group = "org.apache.arrow", name = "arrow-memory-unsafe", version.ref = "arrow" } arrow-c-data = { group = "org.apache.arrow", name = "arrow-c-data", version.ref = "arrow" } +geotools-main = { module = "org.geotools:gt-main", version.ref = "geotools" } +geotools-shapefile = { module = "org.geotools:gt-shapefile", version.ref = "geotools" } +geotools-geojson = { module = "org.geotools:gt-geojson", version.ref = "geotools" } +geotools-referencing = { module = "org.geotools:gt-referencing", version.ref = "geotools" } +geotools-epsg-hsql = { module = "org.geotools:gt-epsg-hsql", version.ref = "geotools" } + +jai-core = { module = "javax.media:jai-core", version.ref = "jai-core" } + +jts-core = { module = "org.locationtech.jts:jts-core", version.ref = "jts" } +jts-io-common = { module = "org.locationtech.jts.io:jts-io-common", version.ref = "jts" } + +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } +ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } +ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } kotlinpoet = { group = "com.squareup", name = "kotlinpoet", version.ref = "kotlinpoet" } swagger = { group = "io.swagger.parser.v3", name = "swagger-parser", version.ref = "openapi" } @@ -113,7 +132,7 @@ android-gradle-api = { group = "com.android.tools.build", name = "gradle-api", v android-gradle = { group = "com.android.tools.build", name = "gradle", version.ref = "android-gradle-api" } kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin" } kotlin-gradle-plugin-api = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin-api" } -ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty", version.ref = "ktor-server-netty" } +ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty", version.ref = "ktor" } kotlin-compile-testing = { group = "com.github.tschuchortdev", name = "kotlin-compile-testing", version.ref = "kotlin-compile-testing" } kotlin-compile-testing-ksp = { group = "com.github.tschuchortdev", name = "kotlin-compile-testing-ksp", version.ref = "kotlin-compile-testing" } kotlin-compiler = { group = "org.jetbrains.kotlin", name = "kotlin-compiler", version.ref = "kotlin" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 43ed0bd81..e98a006d8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -41,3 +41,4 @@ plugins { } include("dataframe-excel") include("core") +include("dataframe-geo") From 3458ac57f50c7f49041169d9349245ea1aea65b6 Mon Sep 17 00:00:00 2001 From: Andrei Date: Fri, 4 Oct 2024 13:32:54 +0400 Subject: [PATCH 2/5] GeoDataFrame add publication --- dataframe-geo/build.gradle.kts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dataframe-geo/build.gradle.kts b/dataframe-geo/build.gradle.kts index 34ec9a11a..d7edbaf5e 100644 --- a/dataframe-geo/build.gradle.kts +++ b/dataframe-geo/build.gradle.kts @@ -51,6 +51,15 @@ tasks.withType().configureEach { (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") } From b724f9d977487dddaed93382c2a67d5cad610f3c Mon Sep 17 00:00:00 2001 From: Andrei Date: Wed, 6 Nov 2024 02:02:03 +0400 Subject: [PATCH 3/5] PR changes & docs --- .../kotlinx/dataframe/jupyter/Integration.kt | 24 ------- .../kotlinx/dataframe/jupyter/execute.kt | 30 ++++++++ dataframe-geo/build.gradle.kts | 7 +- .../kotlinx/dataframe/geo/GeoDataFrame.kt | 38 ++++++++-- .../geo/{GeoFrame.kt => WithGeometry.kt} | 12 ++-- .../jetbrains/kotlinx/dataframe/geo/bounds.kt | 8 +++ .../{ToGeoDataFrame.kt => toGeoDataFrame.kt} | 6 +- .../kotlinx/dataframe/geo/io/read.kt | 14 ++-- .../kotlinx/dataframe/geo/io/write.kt | 6 +- .../kotlinx/dataframe/geo/jts/bounds.kt | 7 ++ .../dataframe/geo/jts/geometryExtenstions.kt | 65 +++++++++++++++-- .../jetbrains/kotlinx/dataframe/geo/toGeo.kt | 14 +++- .../dataframe/jupyter/IntegrationGeo.kt | 53 ++++---------- .../kotlinx/dataframe/geo/io/IOTest.kt | 67 ++++++++++++++++++ .../src/test/resources/simple_points.geojson | 25 +++++++ .../resources/simple_points/simple_points.dbf | Bin 0 -> 575 bytes .../resources/simple_points/simple_points.fix | Bin 0 -> 37 bytes .../resources/simple_points/simple_points.prj | 0 .../resources/simple_points/simple_points.shp | Bin 0 -> 156 bytes .../resources/simple_points/simple_points.shx | Bin 0 -> 116 bytes 20 files changed, 282 insertions(+), 94 deletions(-) create mode 100644 core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/execute.kt rename dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/{GeoFrame.kt => WithGeometry.kt} (68%) rename dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geotools/{ToGeoDataFrame.kt => toGeoDataFrame.kt} (94%) create mode 100644 dataframe-geo/src/test/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/IOTest.kt create mode 100644 dataframe-geo/src/test/resources/simple_points.geojson create mode 100644 dataframe-geo/src/test/resources/simple_points/simple_points.dbf create mode 100644 dataframe-geo/src/test/resources/simple_points/simple_points.fix create mode 100644 dataframe-geo/src/test/resources/simple_points/simple_points.prj create mode 100644 dataframe-geo/src/test/resources/simple_points/simple_points.shp create mode 100644 dataframe-geo/src/test/resources/simple_points/simple_points.shx diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/Integration.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/Integration.kt index 012e6a613..a3b6c63ef 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/Integration.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/Integration.kt @@ -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. */ @@ -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<*>, diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/execute.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/execute.kt new file mode 100644 index 000000000..def44d63a --- /dev/null +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/execute.kt @@ -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) +} diff --git a/dataframe-geo/build.gradle.kts b/dataframe-geo/build.gradle.kts index d7edbaf5e..795eb0957 100644 --- a/dataframe-geo/build.gradle.kts +++ b/dataframe-geo/build.gradle.kts @@ -6,14 +6,14 @@ plugins { alias(kotlin.jvm) alias(publisher) alias(jupyter.api) + //alias(ktlint) } } group = "org.jetbrains.kotlinx" repositories { - // geo repositories should come before Maven Central - maven("https://maven.geotoolkit.org") + // geo repository should come before Maven Central maven("https://repo.osgeo.org/repository/release") mavenCentral() } @@ -22,7 +22,6 @@ repositories { // jai core dependency should be excluded from geotools dependencies and added separately fun ExternalModuleDependency.excludeJaiCore() = exclude("javax.media", "jai_core") - dependencies { api(project(":core")) @@ -42,6 +41,7 @@ dependencies { implementation(libs.ktor.client.content.negotiation) implementation(libs.ktor.serialization.kotlinx.json) + testImplementation(kotlin("test")) } tasks.withType().configureEach { @@ -64,7 +64,6 @@ tasks.processJupyterApiResources { libraryProducers = listOf("org.jetbrains.kotlinx.dataframe.jupyter.IntegrationGeo") } - tasks.test { useJUnitPlatform() } diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/GeoDataFrame.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/GeoDataFrame.kt index a70d1d53c..4519dd138 100644 --- a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/GeoDataFrame.kt +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/GeoDataFrame.kt @@ -7,22 +7,50 @@ import org.jetbrains.kotlinx.dataframe.DataFrame import org.jetbrains.kotlinx.dataframe.api.update import org.jetbrains.kotlinx.dataframe.api.with -class GeoDataFrame(val df: DataFrame, val crs: CoordinateReferenceSystem?) { +/** + * 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(val df: DataFrame, val crs: CoordinateReferenceSystem?) { + /** + * Updates the `GeoDataFrame` using a specified transformation block on the underlying DataFrame. + * + * @param updateBlock The block defining the transformations to be applied to the DataFrame. + * @return A new `GeoDataFrame` instance with updated data and the same CRS. + */ fun update(updateBlock: DataFrame.() -> DataFrame): GeoDataFrame { return GeoDataFrame(df.updateBlock(), crs) } - fun applyCRS(targetCRS: CoordinateReferenceSystem? = null): GeoDataFrame { - if (targetCRS == this.crs) return this + /** + * 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 { + if (targetCrs == this.crs) return this // Use WGS 84 by default TODO val sourceCRS: CoordinateReferenceSystem = this.crs ?: DEFAULT_CRS - val transform = CRS.findMathTransform(sourceCRS, targetCRS, true) + val transform = CRS.findMathTransform(sourceCRS, targetCrs, true) return GeoDataFrame( df.update { geometry }.with { JTS.transform(it, transform) }, - targetCRS + targetCrs ) } + override fun equals(other: Any?): Boolean { + if (other !is GeoDataFrame<*>) return false + return df == other.df && crs == other.crs + } + companion object { val DEFAULT_CRS = CRS.decode("EPSG:4326", true) } diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/GeoFrame.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/WithGeometry.kt similarity index 68% rename from dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/GeoFrame.kt rename to dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/WithGeometry.kt index ba0fab552..158a9ee58 100644 --- a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/GeoFrame.kt +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/WithGeometry.kt @@ -8,28 +8,28 @@ import org.locationtech.jts.geom.MultiPolygon import org.locationtech.jts.geom.Polygon @DataSchema -interface GeoFrame { +interface WithGeometry { val geometry: Geometry } @DataSchema -interface PolygonGeoFrame : GeoFrame { +interface WithPolygon : WithGeometry { override val geometry: Polygon } @DataSchema -interface MultiPolygonGeoFrame : GeoFrame { +interface WithMultiPolygon : WithGeometry { override val geometry: MultiPolygon } @get:JvmName("geometry") -val ColumnsContainer.geometry: DataColumn +val ColumnsContainer.geometry: DataColumn get() = get("geometry") as DataColumn @get:JvmName("geometryPolygon") -val ColumnsContainer.geometry: DataColumn +val ColumnsContainer.geometry: DataColumn get() = get("geometry") as DataColumn @get:JvmName("geometryMultiPolygon") -val ColumnsContainer.geometry: DataColumn +val ColumnsContainer.geometry: DataColumn get() = get("geometry") as DataColumn diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/bounds.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/bounds.kt index 4b057f3ed..675b244ad 100644 --- a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/bounds.kt +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/bounds.kt @@ -4,6 +4,14 @@ 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 { return ReferencedEnvelope(df.geometry.asIterable().computeBounds(), crs) } diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geotools/ToGeoDataFrame.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geotools/toGeoDataFrame.kt similarity index 94% rename from dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geotools/ToGeoDataFrame.kt rename to dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geotools/toGeoDataFrame.kt index ba79545b9..a4c1f6643 100644 --- a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geotools/ToGeoDataFrame.kt +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geotools/toGeoDataFrame.kt @@ -10,7 +10,7 @@ import org.jetbrains.kotlinx.dataframe.DataFrame import org.jetbrains.kotlinx.dataframe.api.Infer import org.jetbrains.kotlinx.dataframe.api.toDataFrame import org.jetbrains.kotlinx.dataframe.geo.GeoDataFrame -import org.jetbrains.kotlinx.dataframe.geo.GeoFrame +import org.jetbrains.kotlinx.dataframe.geo.WithGeometry import org.locationtech.jts.geom.Geometry fun SimpleFeatureCollection.toGeoDataFrame(): GeoDataFrame<*> { @@ -24,7 +24,7 @@ fun SimpleFeatureCollection.toGeoDataFrame(): GeoDataFrame<*> { val geometryAttribute = attributeDescriptors?.find { it is GeometryDescriptor } ?: throw IllegalArgumentException("No geometry attribute") - // In GeoJSON the crs attribute is optional + // In GeoJSON, the crs attribute is optional val crs: CoordinateReferenceSystem? = (geometryAttribute as GeometryDescriptor).coordinateReferenceSystem val data = dataAttributes.associate { it.localName to ArrayList() } @@ -52,5 +52,5 @@ fun SimpleFeatureCollection.toGeoDataFrame(): GeoDataFrame<*> { val geometryColumn = DataColumn.create("geometry", geometries, Infer.Type) - return GeoDataFrame((data.toDataFrame() + geometryColumn) as DataFrame, crs) + return GeoDataFrame((data.toDataFrame() + geometryColumn) as DataFrame, crs) } diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/read.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/read.kt index 4bb604385..e10f5d08a 100644 --- a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/read.kt +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/read.kt @@ -10,20 +10,20 @@ import org.jetbrains.kotlinx.dataframe.io.asURL import java.net.URL -fun GeoDataFrame.Companion.readGeoJSON(path: String): GeoDataFrame<*> { - return readGeoJSON(asURL(path)) +fun GeoDataFrame.Companion.readGeoJson(path: String): GeoDataFrame<*> { + return readGeoJson(asURL(path)) } -fun GeoDataFrame.Companion.readGeoJSON(url: URL): GeoDataFrame<*> { +fun GeoDataFrame.Companion.readGeoJson(url: URL): GeoDataFrame<*> { return (FeatureJSON().readFeatureCollection(url.openStream()) as SimpleFeatureCollection).toGeoDataFrame() } -fun DataFrame.Companion.readGeoJSON(path: String): GeoDataFrame<*> { - return GeoDataFrame.readGeoJSON(path) +fun DataFrame.Companion.readGeoJson(path: String): GeoDataFrame<*> { + return GeoDataFrame.readGeoJson(path) } -fun DataFrame.Companion.readGeoJSON(url: URL): GeoDataFrame<*> { - return GeoDataFrame.readGeoJSON(url) +fun DataFrame.Companion.readGeoJson(url: URL): GeoDataFrame<*> { + return GeoDataFrame.readGeoJson(url) } fun GeoDataFrame.Companion.readShapefile(path: String): GeoDataFrame<*> { diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/write.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/write.kt index 1886cc687..e21787d0e 100644 --- a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/write.kt +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/write.kt @@ -3,6 +3,7 @@ package org.jetbrains.kotlinx.dataframe.geo.io import org.geotools.api.data.FileDataStoreFinder import org.geotools.api.data.SimpleFeatureStore import org.geotools.api.data.Transaction +import org.geotools.feature.simple.SimpleFeatureTypeBuilder import org.geotools.geojson.feature.FeatureJSON import org.jetbrains.kotlinx.dataframe.geo.GeoDataFrame import org.jetbrains.kotlinx.dataframe.geo.geotools.toSimpleFeatureCollection @@ -12,6 +13,8 @@ import java.io.File fun GeoDataFrame<*>.writeGeoJson(path: String): Unit = writeGeoJson(File(path)) fun GeoDataFrame<*>.writeGeoJson(file: File) { + + // TODO: adds ids that breaks order of reading val featureJSON = FeatureJSON() file.outputStream().use { outputStream -> featureJSON.writeFeatureCollection(toSimpleFeatureCollection(), outputStream) @@ -39,8 +42,9 @@ fun GeoDataFrame<*>.writeShapefile(directory: File) { val featureCollection = toSimpleFeatureCollection(fileName, true) val schema = featureCollection.schema + val schemaWithCrs = SimpleFeatureTypeBuilder.retype(schema, crs ?: GeoDataFrame.DEFAULT_CRS) - dataStore.createSchema(schema) + dataStore.createSchema(schemaWithCrs) val featureSource = dataStore.getFeatureSource(fileName) as SimpleFeatureStore val transaction = Transaction.AUTO_COMMIT diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/jts/bounds.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/jts/bounds.kt index 36dd2968d..a86969464 100644 --- a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/jts/bounds.kt +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/jts/bounds.kt @@ -3,6 +3,13 @@ package org.jetbrains.kotlinx.dataframe.geo.jts import org.locationtech.jts.geom.Envelope import org.locationtech.jts.geom.Geometry +/** + * Computes the bounding envelope for a collection of geometries. + * + * + * @receiver The collection of geometries for which to compute the bounds. + * @return The minimal envelope that encompasses all geometries in the collection. + */ fun Iterable.computeBounds(): Envelope { val bounds = Envelope() forEach { geometry -> bounds.expandToInclude(geometry.envelopeInternal) } diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/jts/geometryExtenstions.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/jts/geometryExtenstions.kt index 3814577f8..0b26c0b99 100644 --- a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/jts/geometryExtenstions.kt +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/jts/geometryExtenstions.kt @@ -3,22 +3,77 @@ package org.jetbrains.kotlinx.dataframe.geo.jts import org.locationtech.jts.geom.Geometry import org.locationtech.jts.geom.util.AffineTransformation -fun Geometry.scale(value: Double): Geometry { +/** + * Scales the geometry around its center using the same scaling factor for both axes. + * + * @param factor The scaling factor for both the X and Y axes. + * @return A new geometry scaled around its center. + */ +fun Geometry.scaleAroundCenter(factor: Double): Geometry { + return scaleAroundCenter(factor, factor) +} +/** + * Scales the geometry around its center using different scaling factors for the X and Y axes. + * + * @param xFactor The scaling factor for the X axis. + * @param yFactor The scaling factor for the Y axis. + * @return A new geometry scaled around its center. + */ +fun Geometry.scaleAroundCenter(xFactor: Double, yFactor: Double): Geometry { val centroid = centroid.coordinate val moveToOrigin = AffineTransformation .translationInstance(-centroid.x, -centroid.y) - - val scale = AffineTransformation.scaleInstance(value, value) - + val scale = AffineTransformation.scaleInstance(xFactor, yFactor) val moveBack = AffineTransformation.translationInstance(centroid.x, centroid.y) - val transformation = moveToOrigin.compose(scale).compose(moveBack) return transformation.transform(this) } +/** + * Translates (moves) the geometry by the specified distances along the X and Y axes. + * + * @param valueX The translation distance along the X axis. + * @param valueY The translation distance along the Y axis. + * @return A new geometry translated by the specified distances. + */ fun Geometry.translate(valueX: Double, valueY: Double): Geometry { return AffineTransformation().translate(valueX, valueY).transform(this) } + +/** + * Rotates the geometry around its center by the specified angle in radians. + * + * @param angleRadians The rotation angle in radians. + * @return A new geometry rotated around its center. + */ +fun Geometry.rotate(angleRadians: Double): Geometry { + val centroid = centroid.coordinate + + val moveToOrigin = AffineTransformation.translationInstance(-centroid.x, -centroid.y) + val rotate = AffineTransformation.rotationInstance(angleRadians) + val moveBack = AffineTransformation.translationInstance(centroid.x, centroid.y) + + val transformation = moveToOrigin.compose(rotate).compose(moveBack) + return transformation.transform(this) +} + +/** + * Reflects the geometry across the X axis, inverting its horizontal position. + * + * @return A new geometry reflected across the X axis. + */ +fun Geometry.reflectX(): Geometry { + return scaleAroundCenter(-1.0, 1.0) +} + +/** + * Reflects the geometry across the Y axis, inverting its vertical position. + * + * @return A new geometry reflected across the Y axis. + */ +fun Geometry.reflectY(): Geometry { + return scaleAroundCenter(1.0, -1.0) +} diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/toGeo.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/toGeo.kt index eaee7bd23..3bf1d7652 100644 --- a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/toGeo.kt +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/toGeo.kt @@ -4,6 +4,18 @@ import org.geotools.api.referencing.crs.CoordinateReferenceSystem import org.jetbrains.kotlinx.dataframe.AnyFrame import org.jetbrains.kotlinx.dataframe.DataFrame +/** + * Transforms an `AnyFrame` (a general-purpose DataFrame) into a `GeoDataFrame` + * by interpreting it as a `DataFrame` containing geometry data. Optionally, a + * Coordinate Reference System (CRS) can be specified. + * + * @receiver The input DataFrame to be converted into a `GeoDataFrame`. + * @param crs The coordinate reference system to associate with the `GeoDataFrame`. + * If null, no specific CRS is applied. + * @return The resulting `GeoDataFrame` with geometry and, if provided, an associated CRS. + * + * Note: The `AnyFrame` must contain a `geometry` column to be converted successfully. + */ fun AnyFrame.toGeo(crs: CoordinateReferenceSystem? = null): GeoDataFrame<*> = GeoDataFrame( - this as DataFrame, crs + this as DataFrame, crs ) diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/IntegrationGeo.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/IntegrationGeo.kt index 0dc273caf..54cd5a624 100644 --- a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/IntegrationGeo.kt +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/IntegrationGeo.kt @@ -3,50 +3,27 @@ package org.jetbrains.kotlinx.dataframe.jupyter -import org.jetbrains.kotlinx.dataframe.codeGen.CodeWithConverter import org.jetbrains.kotlinx.dataframe.geo.GeoDataFrame -import org.jetbrains.kotlinx.dataframe.geo.GeoFrame -import org.jetbrains.kotlinx.dataframe.geo.MultiPolygonGeoFrame -import org.jetbrains.kotlinx.dataframe.geo.PolygonGeoFrame +import org.jetbrains.kotlinx.dataframe.geo.WithGeometry +import org.jetbrains.kotlinx.dataframe.geo.WithMultiPolygon +import org.jetbrains.kotlinx.dataframe.geo.WithPolygon import org.jetbrains.kotlinx.dataframe.impl.codeGen.ReplCodeGeneratorImpl import org.jetbrains.kotlinx.jupyter.api.FieldHandler import org.jetbrains.kotlinx.jupyter.api.FieldHandlerExecution -import org.jetbrains.kotlinx.jupyter.api.KotlinKernelHost -import org.jetbrains.kotlinx.jupyter.api.VariableName import org.jetbrains.kotlinx.jupyter.api.libraries.FieldHandlerFactory import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration import kotlin.reflect.KProperty -import kotlin.reflect.KType import kotlin.reflect.full.isSubtypeOf import kotlin.reflect.typeOf +/** + * DataFrame Jupyter integration for geo module. + * + * Adds all necessary imports. + * Adds type converter for inner dataframe in `GeoDataFrame`. + */ internal class IntegrationGeo : JupyterIntegration() { - // TODO make internal in core and use here - 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 - } - } - - // TODO make internal in core and use here - 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) - } - override fun Builder.onLoaded() { import("org.jetbrains.kotlinx.dataframe.geo.*") import("org.jetbrains.kotlinx.dataframe.geo.io.*") @@ -54,14 +31,14 @@ internal class IntegrationGeo : JupyterIntegration() { import("org.jetbrains.kotlinx.dataframe.geo.geotools.*") import("org.jetbrains.kotlinx.dataframe.geo.geocode.*") onLoaded { - useSchema() - useSchema() - useSchema() + useSchema() + useSchema() + useSchema() } val replCodeGeneratorImpl = ReplCodeGeneratorImpl() - replCodeGeneratorImpl.process(GeoFrame::class) - replCodeGeneratorImpl.process(PolygonGeoFrame::class) - replCodeGeneratorImpl.process(MultiPolygonGeoFrame::class) + replCodeGeneratorImpl.process(WithGeometry::class) + replCodeGeneratorImpl.process(WithPolygon::class) + replCodeGeneratorImpl.process(WithMultiPolygon::class) val execution = FieldHandlerFactory.createUpdateExecution> { geo, kProperty -> // TODO rewrite val generatedDf = execute( diff --git a/dataframe-geo/src/test/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/IOTest.kt b/dataframe-geo/src/test/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/IOTest.kt new file mode 100644 index 000000000..42ee5c709 --- /dev/null +++ b/dataframe-geo/src/test/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/IOTest.kt @@ -0,0 +1,67 @@ +package org.jetbrains.kotlinx.dataframe.geo.io + +import org.jetbrains.kotlinx.dataframe.api.dataFrameOf +import org.jetbrains.kotlinx.dataframe.geo.GeoDataFrame +import org.jetbrains.kotlinx.dataframe.geo.toGeo +import org.locationtech.jts.geom.Coordinate +import org.locationtech.jts.geom.GeometryFactory +import java.io.File +import java.nio.file.Files +import kotlin.test.Test +import kotlin.test.assertEquals + +class IOTest { + + private val simplePointsDf = run { + val geometryFactory = GeometryFactory() + val point1 = geometryFactory.createPoint(Coordinate(30.5, 50.5)) + val point2 = geometryFactory.createPoint(Coordinate(31.5, 51.5)) + dataFrameOf("name", "geometry")( + "Point 1", point1, + "Point 2", point2 + ) + } + private val simplePointsGeoDf = simplePointsDf.toGeo(null) + private val classLoader = (this::class as Any).javaClass.classLoader + + @Test + fun readGeoJson() { + val jsonURL = classLoader.getResource("./simple_points.geojson")!! + val geodf = GeoDataFrame.readGeoJson(jsonURL) + + assertEquals(simplePointsDf, geodf.df) + assert(geodf.crs == null) + } + + /* + TODO: doesn't work for now - writers adds ids that breaks order when read features + @Test + fun writeGeoJson() { + val tempFile = Files.createTempFile("simple_points", ".json").toFile() + simplePointsGeoDf.writeGeoJson(tempFile) + + assertEquals(simplePointsGeoDf, GeoDataFrame.readGeoJson(tempFile.toURI().toURL())) + + tempFile.deleteOnExit() + }*/ + + @Test + fun readShapefile() { + val shapefileURL = classLoader.getResource("./simple_points/simple_points.shp")!! + val geodf = GeoDataFrame.readShapefile(shapefileURL) + + assertEquals(simplePointsDf, geodf.df) + assert(geodf.crs == null) + } + + @Test + fun writeShapefile() { + val tempDir = Files.createTempDirectory("shapefiles").toFile() + val tempShapefileDir = File(tempDir, "simple_points").also { it.mkdir() } + simplePointsGeoDf.writeShapefile(tempShapefileDir) + val shapefile = File("${tempShapefileDir.path}/simple_points.shp") + assertEquals(simplePointsGeoDf, GeoDataFrame.readShapefile(shapefile.toURI().toURL())) + + tempDir.deleteOnExit() + } +} diff --git a/dataframe-geo/src/test/resources/simple_points.geojson b/dataframe-geo/src/test/resources/simple_points.geojson new file mode 100644 index 000000000..bb92ad815 --- /dev/null +++ b/dataframe-geo/src/test/resources/simple_points.geojson @@ -0,0 +1,25 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [30.5, 50.5] + }, + "properties": { + "name": "Point 1" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [31.5, 51.5] + }, + "properties": { + "name": "Point 2" + } + } + ] +} diff --git a/dataframe-geo/src/test/resources/simple_points/simple_points.dbf b/dataframe-geo/src/test/resources/simple_points/simple_points.dbf new file mode 100644 index 0000000000000000000000000000000000000000..4e4e34633b3f2ef8260222a87057d10d7937d357 GIT binary patch literal 575 zcmZRU;AUlFU|?`$_zxs8L0)2RDpb@NME^rm!mALFpP5&pV5l%c0mS=8Bh>c*R3}Fi literal 0 HcmV?d00001 diff --git a/dataframe-geo/src/test/resources/simple_points/simple_points.fix b/dataframe-geo/src/test/resources/simple_points/simple_points.fix new file mode 100644 index 0000000000000000000000000000000000000000..bb4d942186bad6ec55b6c347781b7c28f68917a6 GIT binary patch literal 37 UcmZQ%fB+^a4Wby4*dTF6005u>2mk;8 literal 0 HcmV?d00001 diff --git a/dataframe-geo/src/test/resources/simple_points/simple_points.prj b/dataframe-geo/src/test/resources/simple_points/simple_points.prj new file mode 100644 index 000000000..e69de29bb diff --git a/dataframe-geo/src/test/resources/simple_points/simple_points.shp b/dataframe-geo/src/test/resources/simple_points/simple_points.shp new file mode 100644 index 0000000000000000000000000000000000000000..2f7d40f3c56621df5bbaa9963af01b4e179a56ef GIT binary patch literal 156 zcmZQzQ0HR64*Xs)GcYj10p$|a$U`d<=$QUjhW-+14A0p$|a$U`d>W5l{wXj{}GX0C)lg?EnA( literal 0 HcmV?d00001 From 0ff0ee83c520d9ae46e8fd845cafc6a9e18a1ac6 Mon Sep 17 00:00:00 2001 From: Andrei Date: Wed, 6 Nov 2024 02:11:19 +0400 Subject: [PATCH 4/5] fix crs equals checks --- .../jetbrains/kotlinx/dataframe/geo/GeoDataFrame.kt | 11 ++++++++++- .../org/jetbrains/kotlinx/dataframe/geo/io/IOTest.kt | 3 +-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/GeoDataFrame.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/GeoDataFrame.kt index 4519dd138..90a178944 100644 --- a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/GeoDataFrame.kt +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/GeoDataFrame.kt @@ -36,6 +36,9 @@ class GeoDataFrame(val df: DataFrame, val crs: CoordinateRe * @return A new `GeoDataFrame` with reprojected geometries and the specified CRS. */ fun applyCrs(targetCrs: CoordinateReferenceSystem): GeoDataFrame { + if (crs == null) { + return GeoDataFrame(df, targetCrs) + } if (targetCrs == this.crs) return this // Use WGS 84 by default TODO val sourceCRS: CoordinateReferenceSystem = this.crs ?: DEFAULT_CRS @@ -47,8 +50,14 @@ class GeoDataFrame(val df: DataFrame, val crs: CoordinateRe } override fun equals(other: Any?): Boolean { + if (this === other) return true if (other !is GeoDataFrame<*>) return false - return df == other.df && crs == other.crs + + return df == other.df && when { + crs == null && other.crs == null -> true + crs == null || other.crs == null -> false + else -> CRS.equalsIgnoreMetadata(crs, other.crs) + } } companion object { diff --git a/dataframe-geo/src/test/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/IOTest.kt b/dataframe-geo/src/test/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/IOTest.kt index 42ee5c709..5041fd8a4 100644 --- a/dataframe-geo/src/test/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/IOTest.kt +++ b/dataframe-geo/src/test/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/IOTest.kt @@ -21,7 +21,7 @@ class IOTest { "Point 2", point2 ) } - private val simplePointsGeoDf = simplePointsDf.toGeo(null) + private val simplePointsGeoDf = simplePointsDf.toGeo(GeoDataFrame.DEFAULT_CRS) private val classLoader = (this::class as Any).javaClass.classLoader @Test @@ -61,7 +61,6 @@ class IOTest { simplePointsGeoDf.writeShapefile(tempShapefileDir) val shapefile = File("${tempShapefileDir.path}/simple_points.shp") assertEquals(simplePointsGeoDf, GeoDataFrame.readShapefile(shapefile.toURI().toURL())) - tempDir.deleteOnExit() } } From 289a1fb98d3edd17e1f40b07276b96b235fc40c9 Mon Sep 17 00:00:00 2001 From: Andrei Date: Wed, 6 Nov 2024 18:32:20 +0400 Subject: [PATCH 5/5] docs & format --- dataframe-geo/build.gradle.kts | 5 +- .../kotlinx/dataframe/geo/GeoDataFrame.kt | 25 ++++--- .../kotlinx/dataframe/geo/WithGeometry.kt | 9 ++- .../jetbrains/kotlinx/dataframe/geo/bounds.kt | 4 +- .../kotlinx/dataframe/geo/geocode/Geocoder.kt | 67 ++++++++++--------- .../dataframe/geo/geotools/toGeoDataFrame.kt | 12 +++- .../geo/geotools/toSimpleFeatureCollection.kt | 13 +++- .../kotlinx/dataframe/geo/io/read.kt | 35 +++------- .../kotlinx/dataframe/geo/io/write.kt | 5 -- ...ryExtenstions.kt => geometryExtensions.kt} | 17 ++--- .../jetbrains/kotlinx/dataframe/geo/toGeo.kt | 9 ++- .../dataframe/jupyter/IntegrationGeo.kt | 9 +-- .../kotlinx/dataframe/geo/io/IOTest.kt | 6 +- 13 files changed, 108 insertions(+), 108 deletions(-) rename dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/jts/{geometryExtenstions.kt => geometryExtensions.kt} (87%) diff --git a/dataframe-geo/build.gradle.kts b/dataframe-geo/build.gradle.kts index 795eb0957..34608e568 100644 --- a/dataframe-geo/build.gradle.kts +++ b/dataframe-geo/build.gradle.kts @@ -6,7 +6,7 @@ plugins { alias(kotlin.jvm) alias(publisher) alias(jupyter.api) - //alias(ktlint) + alias(ktlint) } } @@ -67,6 +67,3 @@ tasks.processJupyterApiResources { tasks.test { useJUnitPlatform() } -kotlin { - jvmToolchain(11) -} diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/GeoDataFrame.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/GeoDataFrame.kt index 90a178944..b9458d73c 100644 --- a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/GeoDataFrame.kt +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/GeoDataFrame.kt @@ -17,14 +17,12 @@ import org.jetbrains.kotlinx.dataframe.api.with */ class GeoDataFrame(val df: DataFrame, val crs: CoordinateReferenceSystem?) { /** - * Updates the `GeoDataFrame` using a specified transformation block on the underlying DataFrame. + * Creates a new `GeoDataFrame` using a specified transformation block on the underlying DataFrame. * * @param updateBlock The block defining the transformations to be applied to the DataFrame. * @return A new `GeoDataFrame` instance with updated data and the same CRS. */ - fun update(updateBlock: DataFrame.() -> DataFrame): GeoDataFrame { - return GeoDataFrame(df.updateBlock(), crs) - } + fun update(updateBlock: DataFrame.() -> DataFrame): GeoDataFrame = GeoDataFrame(df.updateBlock(), crs) /** * Transforms the geometries to a specified Coordinate Reference System (CRS). @@ -41,11 +39,11 @@ class GeoDataFrame(val df: DataFrame, val crs: CoordinateRe } if (targetCrs == this.crs) return this // Use WGS 84 by default TODO - val sourceCRS: CoordinateReferenceSystem = this.crs ?: DEFAULT_CRS + val sourceCRS: CoordinateReferenceSystem = this.crs val transform = CRS.findMathTransform(sourceCRS, targetCrs, true) return GeoDataFrame( df.update { geometry }.with { JTS.transform(it, transform) }, - targetCrs + targetCrs, ) } @@ -53,13 +51,18 @@ class GeoDataFrame(val df: DataFrame, val crs: CoordinateRe if (this === other) return true if (other !is GeoDataFrame<*>) return false - return df == other.df && when { - crs == null && other.crs == null -> true - crs == null || other.crs == null -> false - else -> CRS.equalsIgnoreMetadata(crs, other.crs) - } + return df == other.df && + when { + crs == null && other.crs == null -> true + crs == null || other.crs == null -> false + else -> CRS.equalsIgnoreMetadata(crs, other.crs) + } } + override fun toString(): String = super.toString() + + override fun hashCode(): Int = super.hashCode() + companion object { val DEFAULT_CRS = CRS.decode("EPSG:4326", true) } diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/WithGeometry.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/WithGeometry.kt index 158a9ee58..d3017d2ff 100644 --- a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/WithGeometry.kt +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/WithGeometry.kt @@ -22,14 +22,17 @@ interface WithMultiPolygon : WithGeometry { override val geometry: MultiPolygon } +@Suppress("UNCHECKED_CAST") @get:JvmName("geometry") -val ColumnsContainer.geometry: DataColumn +val ColumnsContainer.geometry: DataColumn get() = get("geometry") as DataColumn +@Suppress("UNCHECKED_CAST") @get:JvmName("geometryPolygon") -val ColumnsContainer.geometry: DataColumn +val ColumnsContainer.geometry: DataColumn get() = get("geometry") as DataColumn +@Suppress("UNCHECKED_CAST") @get:JvmName("geometryMultiPolygon") -val ColumnsContainer.geometry: DataColumn +val ColumnsContainer.geometry: DataColumn get() = get("geometry") as DataColumn diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/bounds.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/bounds.kt index 675b244ad..b624a83ba 100644 --- a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/bounds.kt +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/bounds.kt @@ -12,6 +12,4 @@ import org.jetbrains.kotlinx.dataframe.geo.jts.computeBounds * @return The bounding envelope that includes all geometries, * associated with the CRS of the `GeoDataFrame`. */ -fun GeoDataFrame<*>.bounds(): ReferencedEnvelope { - return ReferencedEnvelope(df.geometry.asIterable().computeBounds(), crs) -} +fun GeoDataFrame<*>.bounds(): ReferencedEnvelope = ReferencedEnvelope(df.geometry.asIterable().computeBounds(), crs) diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geocode/Geocoder.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geocode/Geocoder.kt index 5674ef615..279a470a8 100644 --- a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geocode/Geocoder.kt +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geocode/Geocoder.kt @@ -21,25 +21,27 @@ import org.locationtech.jts.geom.Geometry import org.locationtech.jts.geom.GeometryFactory import org.locationtech.jts.io.geojson.GeoJsonReader - object Geocoder { private val url = "https://geo2.datalore.jetbrains.com/map_data/geocoding" - private fun countryQuery(country: String) = """ { - "region_query_names" : [ "$country" ], - "region_query_countries" : null, - "region_query_states" : null, - "region_query_counties" : null, - "ambiguity_resolver" : { - "ambiguity_resolver_ignoring_strategy" : null, - "ambiguity_resolver_box" : null, - "ambiguity_resolver_closest_coord" : null - } - } - """.trimIndent() + private fun countryQuery(country: String) = + """ + { + "region_query_names" : [ "$country" ], + "region_query_countries" : null, + "region_query_states" : null, + "region_query_counties" : null, + "ambiguity_resolver" : { + "ambiguity_resolver_ignoring_strategy" : null, + "ambiguity_resolver_box" : null, + "ambiguity_resolver_closest_coord" : null + } + } + """.trimIndent() - private fun geocodeQuery(countries: List) = """ + private fun geocodeQuery(countries: List) = + """ { "version" : 3, "mode" : "by_geocoding", @@ -55,29 +57,31 @@ object Geocoder { "namesake_example_limit" : 10, "allow_ambiguous" : false } -""".trimIndent() + """.trimIndent() - private fun idsQuery(ids: List) = """ - {"version": 3, - "mode": "by_id", - "feature_options": ["boundary"], - "resolution": 5, - "view_box": null, - "fetched_ids": null, - "ids": [${ids.joinToString(", ") { "\"" + it + "\"" }}]} - """.trimIndent() + private fun idsQuery(ids: List) = + """ + {"version": 3, + "mode": "by_id", + "feature_options": ["boundary"], + "resolution": 5, + "view_box": null, + "fetched_ids": null, + "ids": [${ids.joinToString(", ") { "\"" + it + "\"" }}]} + """.trimIndent() private val client = HttpClient(CIO) { install(ContentNegotiation) { - json(Json { - prettyPrint = true - isLenient = true - }) + json( + Json { + prettyPrint = true + isLenient = true + }, + ) } } fun geocodeCountries(countries: List): GeoDataFrame<*> { - val query = geocodeQuery(countries) val foundNames = mutableListOf() val geometries = mutableListOf() @@ -104,13 +108,14 @@ object Geocoder { }.bodyAsText() val geoJsonReader = GeoJsonReader(GeometryFactory()) - Json.parseToJsonElement(responseStringGeometries).jsonObject["data"]!!.jsonObject["answers"]!!.jsonArray.forEach { + Json.parseToJsonElement( + responseStringGeometries, + ).jsonObject["data"]!!.jsonObject["answers"]!!.jsonArray.forEach { it.jsonObject["features"]!!.jsonArray.single().jsonObject.also { val boundary = it["boundary"]!!.jsonPrimitive.content geometries.add(geoJsonReader.read(boundary)) } } - } return dataFrameOf( "country" to countries, diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geotools/toGeoDataFrame.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geotools/toGeoDataFrame.kt index a4c1f6643..3a63108f6 100644 --- a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geotools/toGeoDataFrame.kt +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geotools/toGeoDataFrame.kt @@ -13,8 +13,17 @@ import org.jetbrains.kotlinx.dataframe.geo.GeoDataFrame import org.jetbrains.kotlinx.dataframe.geo.WithGeometry import org.locationtech.jts.geom.Geometry +/** + * Converts this SimpleFeatureCollection to a GeoDataFrame. + * + * This method transforms the SimpleFeatureCollection into a GeoDataFrame, extracting both + * spatial (geometry) and non-spatial attributes, and associates them with an optional + * Coordinate Reference System (CRS) if available. + * + * @return a GeoDataFrame containing the data from this SimpleFeatureCollection, including + * geometries and other attributes, and an associated CRS if present. + */ fun SimpleFeatureCollection.toGeoDataFrame(): GeoDataFrame<*> { - require(schema is SimpleFeatureType) { "GeoTools: SimpleFeatureType expected but was: ${schema::class.simpleName}" } @@ -52,5 +61,6 @@ fun SimpleFeatureCollection.toGeoDataFrame(): GeoDataFrame<*> { val geometryColumn = DataColumn.create("geometry", geometries, Infer.Type) + @Suppress("UNCHECKED_CAST") return GeoDataFrame((data.toDataFrame() + geometryColumn) as DataFrame, crs) } diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geotools/toSimpleFeatureCollection.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geotools/toSimpleFeatureCollection.kt index 06ef8b3d7..044897745 100644 --- a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geotools/toSimpleFeatureCollection.kt +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/geotools/toSimpleFeatureCollection.kt @@ -11,9 +11,16 @@ import org.jetbrains.kotlinx.dataframe.api.single import org.jetbrains.kotlinx.dataframe.geo.GeoDataFrame import org.locationtech.jts.geom.Geometry +/** + * Converts the `GeoDataFrame` to a `SimpleFeatureCollection`. + * + * @param name Optional name for the `SimpleFeatureCollection`. Defaults to "geodata" if not specified. + * @param singleGeometryType Whether to enforce a single geometry type within the collection. Defaults to false. + * @return A `SimpleFeatureCollection` representing the `GeoDataFrame`. + */ fun GeoDataFrame<*>.toSimpleFeatureCollection( name: String? = null, - singleGeometryType: Boolean = false + singleGeometryType: Boolean = false, ): SimpleFeatureCollection { val typeBuilder = SimpleFeatureTypeBuilder() typeBuilder.name = name ?: "geodata" @@ -21,7 +28,9 @@ fun GeoDataFrame<*>.toSimpleFeatureCollection( val geometryClass = if (singleGeometryType) { // todo singleOrNull() ?: error() df["geometry"].map { it!!::class.java }.distinct().single() - } else Geometry::class.java + } else { + Geometry::class.java + } typeBuilder.add("the_geom", geometryClass) df.columnNames().filter { it != "geometry" }.forEach { colName -> typeBuilder.add(colName, String::class.java) diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/read.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/read.kt index e10f5d08a..0a10e32de 100644 --- a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/read.kt +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/read.kt @@ -9,35 +9,20 @@ import org.jetbrains.kotlinx.dataframe.geo.geotools.toGeoDataFrame import org.jetbrains.kotlinx.dataframe.io.asURL import java.net.URL +fun GeoDataFrame.Companion.readGeoJson(path: String): GeoDataFrame<*> = readGeoJson(asURL(path)) -fun GeoDataFrame.Companion.readGeoJson(path: String): GeoDataFrame<*> { - return readGeoJson(asURL(path)) -} +fun GeoDataFrame.Companion.readGeoJson(url: URL): GeoDataFrame<*> = + (FeatureJSON().readFeatureCollection(url.openStream()) as SimpleFeatureCollection).toGeoDataFrame() -fun GeoDataFrame.Companion.readGeoJson(url: URL): GeoDataFrame<*> { - return (FeatureJSON().readFeatureCollection(url.openStream()) as SimpleFeatureCollection).toGeoDataFrame() -} +fun DataFrame.Companion.readGeoJson(path: String): GeoDataFrame<*> = GeoDataFrame.readGeoJson(path) -fun DataFrame.Companion.readGeoJson(path: String): GeoDataFrame<*> { - return GeoDataFrame.readGeoJson(path) -} +fun DataFrame.Companion.readGeoJson(url: URL): GeoDataFrame<*> = GeoDataFrame.readGeoJson(url) -fun DataFrame.Companion.readGeoJson(url: URL): GeoDataFrame<*> { - return GeoDataFrame.readGeoJson(url) -} +fun GeoDataFrame.Companion.readShapefile(path: String): GeoDataFrame<*> = readShapefile(asURL(path)) -fun GeoDataFrame.Companion.readShapefile(path: String): GeoDataFrame<*> { - return readShapefile(asURL(path)) -} +fun GeoDataFrame.Companion.readShapefile(url: URL): GeoDataFrame<*> = + ShapefileDataStoreFactory().createDataStore(url).featureSource.features.toGeoDataFrame() -fun GeoDataFrame.Companion.readShapefile(url: URL): GeoDataFrame<*> { - return ShapefileDataStoreFactory().createDataStore(url).featureSource.features.toGeoDataFrame() -} +fun DataFrame.Companion.readShapefile(path: String): GeoDataFrame<*> = GeoDataFrame.readShapefile(path) -fun DataFrame.Companion.readShapefile(path: String): GeoDataFrame<*> { - return GeoDataFrame.readShapefile(path) -} - -fun DataFrame.Companion.readShapefile(url: URL): GeoDataFrame<*> { - return GeoDataFrame.readShapefile(url) -} +fun DataFrame.Companion.readShapefile(url: URL): GeoDataFrame<*> = GeoDataFrame.readShapefile(url) diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/write.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/write.kt index e21787d0e..24a4b8d02 100644 --- a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/write.kt +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/write.kt @@ -9,11 +9,9 @@ import org.jetbrains.kotlinx.dataframe.geo.GeoDataFrame import org.jetbrains.kotlinx.dataframe.geo.geotools.toSimpleFeatureCollection import java.io.File - fun GeoDataFrame<*>.writeGeoJson(path: String): Unit = writeGeoJson(File(path)) fun GeoDataFrame<*>.writeGeoJson(file: File) { - // TODO: adds ids that breaks order of reading val featureJSON = FeatureJSON() file.outputStream().use { outputStream -> @@ -24,7 +22,6 @@ fun GeoDataFrame<*>.writeGeoJson(file: File) { fun GeoDataFrame<*>.writeShapefile(directoryPath: String): Unit = writeShapefile(File(directoryPath)) fun GeoDataFrame<*>.writeShapefile(directory: File) { - if (!directory.exists()) { directory.mkdirs() } @@ -35,7 +32,6 @@ fun GeoDataFrame<*>.writeShapefile(directory: File) { val creationParams = mutableMapOf() creationParams["url"] = file.toURI().toURL() - val factory = FileDataStoreFinder.getDataStoreFactory("shp") val dataStore = factory.createNewDataStore(creationParams) @@ -58,5 +54,4 @@ fun GeoDataFrame<*>.writeShapefile(directory: File) { } finally { transaction.close() } - } diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/jts/geometryExtenstions.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/jts/geometryExtensions.kt similarity index 87% rename from dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/jts/geometryExtenstions.kt rename to dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/jts/geometryExtensions.kt index 0b26c0b99..74716d518 100644 --- a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/jts/geometryExtenstions.kt +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/jts/geometryExtensions.kt @@ -9,9 +9,7 @@ import org.locationtech.jts.geom.util.AffineTransformation * @param factor The scaling factor for both the X and Y axes. * @return A new geometry scaled around its center. */ -fun Geometry.scaleAroundCenter(factor: Double): Geometry { - return scaleAroundCenter(factor, factor) -} +fun Geometry.scaleAroundCenter(factor: Double): Geometry = scaleAroundCenter(factor, factor) /** * Scales the geometry around its center using different scaling factors for the X and Y axes. @@ -39,9 +37,8 @@ fun Geometry.scaleAroundCenter(xFactor: Double, yFactor: Double): Geometry { * @param valueY The translation distance along the Y axis. * @return A new geometry translated by the specified distances. */ -fun Geometry.translate(valueX: Double, valueY: Double): Geometry { - return AffineTransformation().translate(valueX, valueY).transform(this) -} +fun Geometry.translate(valueX: Double, valueY: Double): Geometry = + AffineTransformation().translate(valueX, valueY).transform(this) /** * Rotates the geometry around its center by the specified angle in radians. @@ -65,15 +62,11 @@ fun Geometry.rotate(angleRadians: Double): Geometry { * * @return A new geometry reflected across the X axis. */ -fun Geometry.reflectX(): Geometry { - return scaleAroundCenter(-1.0, 1.0) -} +fun Geometry.reflectX(): Geometry = scaleAroundCenter(-1.0, 1.0) /** * Reflects the geometry across the Y axis, inverting its vertical position. * * @return A new geometry reflected across the Y axis. */ -fun Geometry.reflectY(): Geometry { - return scaleAroundCenter(1.0, -1.0) -} +fun Geometry.reflectY(): Geometry = scaleAroundCenter(1.0, -1.0) diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/toGeo.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/toGeo.kt index 3bf1d7652..564141200 100644 --- a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/toGeo.kt +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/geo/toGeo.kt @@ -16,6 +16,9 @@ import org.jetbrains.kotlinx.dataframe.DataFrame * * Note: The `AnyFrame` must contain a `geometry` column to be converted successfully. */ -fun AnyFrame.toGeo(crs: CoordinateReferenceSystem? = null): GeoDataFrame<*> = GeoDataFrame( - this as DataFrame, crs -) +@Suppress("UNCHECKED_CAST") +fun AnyFrame.toGeo(crs: CoordinateReferenceSystem? = null): GeoDataFrame<*> = + GeoDataFrame( + this as DataFrame, + crs, + ) diff --git a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/IntegrationGeo.kt b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/IntegrationGeo.kt index 54cd5a624..b4f13ecc9 100644 --- a/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/IntegrationGeo.kt +++ b/dataframe-geo/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/IntegrationGeo.kt @@ -2,7 +2,6 @@ package org.jetbrains.kotlinx.dataframe.jupyter - import org.jetbrains.kotlinx.dataframe.geo.GeoDataFrame import org.jetbrains.kotlinx.dataframe.geo.WithGeometry import org.jetbrains.kotlinx.dataframe.geo.WithMultiPolygon @@ -43,19 +42,17 @@ internal class IntegrationGeo : JupyterIntegration() { // TODO rewrite val generatedDf = execute( codeWithConverter = replCodeGeneratorImpl.process(geo.df, kProperty), - "(${kProperty.name}.df as DataFrame<*>)" + "(${kProperty.name}.df as DataFrame<*>)", ) val name = execute("GeoDataFrame($generatedDf, ${kProperty.name}.crs)").name name } - addTypeConverter(object : FieldHandler { override val execution: FieldHandlerExecution<*> = execution - override fun accepts(value: Any?, property: KProperty<*>): Boolean { - return property.returnType.isSubtypeOf(typeOf>()) - } + override fun accepts(value: Any?, property: KProperty<*>): Boolean = + property.returnType.isSubtypeOf(typeOf>()) }) } } diff --git a/dataframe-geo/src/test/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/IOTest.kt b/dataframe-geo/src/test/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/IOTest.kt index 5041fd8a4..dc28300f9 100644 --- a/dataframe-geo/src/test/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/IOTest.kt +++ b/dataframe-geo/src/test/kotlin/org/jetbrains/kotlinx/dataframe/geo/io/IOTest.kt @@ -17,8 +17,10 @@ class IOTest { val point1 = geometryFactory.createPoint(Coordinate(30.5, 50.5)) val point2 = geometryFactory.createPoint(Coordinate(31.5, 51.5)) dataFrameOf("name", "geometry")( - "Point 1", point1, - "Point 2", point2 + "Point 1", + point1, + "Point 2", + point2, ) } private val simplePointsGeoDf = simplePointsDf.toGeo(GeoDataFrame.DEFAULT_CRS)