Skip to content

Commit 6231892

Browse files
Merge pull request #449 from Kotlin/geo
Geo
2 parents d72d72f + 28f3f0d commit 6231892

File tree

36 files changed

+1899
-25
lines changed

36 files changed

+1899
-25
lines changed

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ val published = listOf(
3737
"kandy-api",
3838
"kandy-echarts",
3939
"kandy-lets-plot",
40+
"kandy-geo",
4041
"kandy-util"
4142
)
4243

gradle/libs.versions.toml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,21 @@ nexus = "1.3.0"
77
jupyterApi = "0.12.0-250"
88

99
# libraries version
10-
dataframe = "0.14.1"
10+
dataframe = "0.15.0"
1111
serialization = "1.6.3"
1212
datetime = "0.6.0"
1313
html = "0.11.0"
1414
statistics = "0.4.0-dev-8"
1515
letsPlot = "4.7.3"
1616
letsPlotImage = "4.3.3"
1717
mockk = "1.13.10"
18+
geotools = "32.0"
19+
jai-core = "1.1.3"
1820

1921
[libraries]
2022
# Kotlinx libraries
2123
kotlinx-dataframe = { group = "org.jetbrains.kotlinx", name = "dataframe", version.ref = "dataframe" }
24+
kotlinx-dataframe-geo = { group = "org.jetbrains.kotlinx", name = "dataframe-geo", version.ref = "dataframe" }
2225
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
2326
kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "datetime" }
2427
kotlinx-html = { group = "org.jetbrains.kotlinx", name = "kotlinx-html-jvm", version.ref = "html" }
@@ -27,6 +30,15 @@ kotlinx-statistics = { group = "org.jetbrains.kotlinx", name = "kotlin-statistic
2730
lets-plot = { group = "org.jetbrains.lets-plot", name = "lets-plot-kotlin-jvm", version.ref = "letsPlot" }
2831
lets-plot-image = { group = "org.jetbrains.lets-plot", name = "lets-plot-image-export", version.ref = "letsPlotImage" }
2932
lets-plot-awt = { group = "org.jetbrains.lets-plot", name = "platf-awt-jvm", version.ref = "letsPlotImage" }
33+
lets-plot-geotools = { group = "org.jetbrains.lets-plot", name = "lets-plot-kotlin-geotools", version.ref = "letsPlot" }
34+
#Geootools
35+
jai-core = { module = "javax.media:jai-core", version.ref = "jai-core" }
36+
37+
geotools-main = { module = "org.geotools:gt-main", version.ref = "geotools" }
38+
geotools-shapefile = { module = "org.geotools:gt-shapefile", version.ref = "geotools" }
39+
geotools-geojson = { module = "org.geotools:gt-geojson", version.ref = "geotools" }
40+
geotools-referencing = { module = "org.geotools:gt-referencing", version.ref = "geotools" }
41+
geotools-epsg-hsql = { module = "org.geotools:gt-epsg-hsql", version.ref = "geotools" }
3042

3143
# Testing
3244
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }

kandy-api/src/main/kotlin/org/jetbrains/kotlinx/kandy/dsl/internal/dataframe/concatFixed.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ internal fun <T, R> GroupBy<T, R>.concatFixed(): DataFrame<R> {
1313
return mapToFrames {
1414
val rowsCount = group.rowsCount()
1515
val keyColumns = keyNames.filter { it !in group.columnNames() }.map { keyName ->
16-
DataColumn.create(keyName, List(rowsCount) { key[keyName] }, Infer.Type)
16+
DataColumn.createByType(keyName, List(rowsCount) { key[keyName] }, Infer.Type)
1717
}
1818
group.addAll(keyColumns)
1919
}.concat()

kandy-api/src/test/kotlin/org/jetbrains/kotlinx/kandy/builders/dataframe/dataFramePlotBuilder.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ class DataFramePlotBuilderTest {
5555

5656
@Test
5757
fun `test get columns with selector`() {
58-
val nameColumn: DataColumn<String> = DataColumn.create("name", listOf("Alice", "Bob", "Charlie"))
59-
val ageColumn: DataColumn<Int> = DataColumn.create("age", listOf(15, 20, 100))
58+
val nameColumn: DataColumn<String> = DataColumn.createByType("name", listOf("Alice", "Bob", "Charlie"))
59+
val ageColumn: DataColumn<Int> = DataColumn.createByType("age", listOf(15, 20, 100))
6060

6161
val dataFrame = dataFrameOf(nameColumn, ageColumn)
6262

kandy-api/src/test/kotlin/org/jetbrains/kotlinx/kandy/builders/dataframe/groupByPlotBuilder.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ import kotlin.test.assertFalse
2424
import kotlin.test.assertTrue
2525

2626
class GroupByPlotBuilderTest {
27-
private val a = DataColumn.create("a", listOf(1, 4))
28-
private val b = DataColumn.create("b", listOf(2, 5))
29-
private val c = DataColumn.create("c", listOf(3, 6))
27+
private val a = DataColumn.createByType("a", listOf(1, 4))
28+
private val b = DataColumn.createByType("b", listOf(2, 5))
29+
private val c = DataColumn.createByType("c", listOf(3, 6))
3030
private val dataFrame = dataFrameOf(a, b, c)
3131

3232
@Test

kandy-geo/build.gradle.kts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import org.jetbrains.kotlin.gradle.tasks.BaseKotlinCompile
2+
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
3+
4+
plugins {
5+
with(libs.plugins) {
6+
alias(kotlin.jvm)
7+
alias(kotlin.jupyter.api)
8+
alias(korro)
9+
}
10+
}
11+
12+
repositories {
13+
maven("https://repo.osgeo.org/repository/release")
14+
mavenCentral()
15+
}
16+
17+
// https://stackoverflow.com/questions/26993105/i-get-an-error-downloading-javax-media-jai-core1-1-3-from-maven-central
18+
// jai core dependency should be excluded from geotools dependencies and added separately
19+
fun ExternalModuleDependency.excludeJaiCore() = exclude("javax.media", "jai_core")
20+
21+
dependencies {
22+
api(project(":kandy-api"))
23+
api(project(":kandy-lets-plot"))
24+
implementation(libs.lets.plot)
25+
implementation(libs.lets.plot.geotools)
26+
implementation(libs.kotlinx.dataframe)
27+
implementation(libs.kotlinx.dataframe.geo)
28+
29+
implementation(libs.geotools.main) { excludeJaiCore() }
30+
implementation(libs.geotools.shapefile) { excludeJaiCore() }
31+
implementation(libs.geotools.geojson) { excludeJaiCore() }
32+
implementation(libs.geotools.referencing) { excludeJaiCore() }
33+
implementation(libs.geotools.epsg.hsql) { excludeJaiCore() }
34+
35+
implementation(libs.jai.core)
36+
37+
testImplementation(kotlin("test"))
38+
}
39+
40+
// add friend modules to access internal properties
41+
tasks.withType<KotlinCompile>().configureEach {
42+
val friendModules = listOf(project(":kandy-api"), project(":kandy-lets-plot"))
43+
val jarTasks = friendModules.map { it.tasks.getByName("jar") as Jar }
44+
val jarPaths = jarTasks.map { it.archiveFile.get().asFile.absolutePath }
45+
(this as BaseKotlinCompile).friendPaths.from(jarPaths)
46+
}
47+
48+
tasks.test {
49+
useJUnitPlatform()
50+
}
51+
kotlin {
52+
jvmToolchain(11)
53+
}
54+
55+
tasks.processJupyterApiResources {
56+
libraryProducers = listOf("org.jetbrains.kotlinx.kandy.letsplot.geo.jupyter.IntegrationGeo")
57+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "CANNOT_OVERRIDE_INVISIBLE_MEMBER")
2+
3+
package org.jetbrains.kotlinx.kandy.letsplot.geo
4+
5+
import org.jetbrains.kotlinx.kandy.ir.Plot
6+
import org.jetbrains.kotlinx.kandy.letsplot.feature.CoordinatesTransformation
7+
import org.jetbrains.kotlinx.kandy.letsplot.feature.CustomCoordinatesTransformation
8+
import org.jetbrains.kotlinx.kandy.letsplot.internal.X
9+
import org.jetbrains.kotlinx.kandy.letsplot.internal.Y
10+
import org.jetbrains.kotlinx.kandy.letsplot.translator.axes
11+
import org.jetbrains.kotlinx.kandy.letsplot.translator.limits
12+
import org.jetbrains.letsPlot.coord.coordMap
13+
import org.jetbrains.letsPlot.intern.FeatureList
14+
15+
internal class MercatorCoordinatesTransformation : CustomCoordinatesTransformation {
16+
override fun wrap(plot: Plot): FeatureList {
17+
val axes = plot.axes()
18+
val xLimits = axes[X]?.limits()
19+
val yLimits = axes[Y]?.limits()
20+
21+
return FeatureList(listOf(coordMap(xlim = xLimits, ylim = yLimits, flip = false)))
22+
}
23+
}
24+
25+
internal class MercatorFlippedCoordinates : CustomCoordinatesTransformation {
26+
override fun wrap(plot: Plot): FeatureList {
27+
val axes = plot.axes()
28+
val xLimits = axes[X]?.limits()
29+
val yLimits = axes[Y]?.limits()
30+
31+
return FeatureList(listOf(coordMap(xlim = xLimits, ylim = yLimits, flip = true)))
32+
}
33+
}
34+
35+
public fun CoordinatesTransformation.Companion.mercator(): CoordinatesTransformation =
36+
MercatorCoordinatesTransformation()
37+
38+
public fun CoordinatesTransformation.Companion.mercatorFlipped(): CoordinatesTransformation =
39+
MercatorFlippedCoordinates()
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package org.jetbrains.kotlinx.kandy.letsplot.geo.dsl
2+
3+
import org.geotools.geojson.geom.GeometryJSON
4+
import org.jetbrains.kotlinx.dataframe.DataFrame
5+
import org.jetbrains.kotlinx.dataframe.api.remove
6+
import org.jetbrains.kotlinx.dataframe.api.toMap
7+
import org.jetbrains.kotlinx.dataframe.geo.GeoDataFrame
8+
import org.jetbrains.kotlinx.dataframe.geo.WithGeometry
9+
import org.jetbrains.kotlinx.dataframe.geo.geometry
10+
import org.jetbrains.kotlinx.kandy.letsplot.data.GeoSpatialData
11+
import org.jetbrains.letsPlot.spatial.SpatialDataset
12+
13+
internal class GeoData(val geoDataFrame: GeoDataFrame<*>) : GeoSpatialData {
14+
override val dataFrame: DataFrame<WithGeometry> = geoDataFrame.df
15+
16+
companion object {
17+
// TODO(https://github.com/Kotlin/kandy/issues/454)
18+
const val DEFAULT_PRECISION = 10
19+
}
20+
21+
override fun toSpatialDataset(): SpatialDataset {
22+
with(geoDataFrame) {
23+
val geojson = GeometryJSON(DEFAULT_PRECISION)
24+
return SpatialDataset.withGEOJSON(
25+
df.remove { geometry }.toMap(),
26+
df.geometry.toList().map { geojson.toString(it) },
27+
crs = crs?.name?.code
28+
)
29+
}
30+
}
31+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "CANNOT_OVERRIDE_INVISIBLE_MEMBER")
2+
3+
package org.jetbrains.kotlinx.kandy.letsplot.geo.dsl
4+
5+
import org.jetbrains.kotlinx.dataframe.DataFrame
6+
import org.jetbrains.kotlinx.dataframe.api.add
7+
import org.jetbrains.kotlinx.dataframe.geo.GeoDataFrame
8+
import org.jetbrains.kotlinx.dataframe.geo.WithGeometry
9+
import org.jetbrains.kotlinx.dataframe.geo.geometry
10+
import org.jetbrains.kotlinx.kandy.dsl.internal.dataframe.DatasetBuilderImpl
11+
import org.jetbrains.kotlinx.kandy.ir.data.TableData
12+
13+
internal class GeoDataBuilder(
14+
geoData: GeoData
15+
) : DatasetBuilderImpl(null) {
16+
constructor(geodf: GeoDataFrame<*>) : this(GeoData(geodf))
17+
18+
val crs = geoData.geoDataFrame.crs
19+
override val baseDataFrame: DataFrame<WithGeometry> = geoData.dataFrame
20+
21+
@Suppress("UNCHECKED_CAST")
22+
override fun build(): TableData {
23+
if (!buffer.containsColumn("geometry")) {
24+
buffer = buffer.add(baseDataFrame.geometry)
25+
}
26+
return GeoData(GeoDataFrame(buffer as DataFrame<WithGeometry>, crs))
27+
}
28+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
@file:Suppress("CANNOT_OVERRIDE_INVISIBLE_MEMBER")
2+
3+
package org.jetbrains.kotlinx.kandy.letsplot.geo.dsl
4+
5+
import org.jetbrains.kotlinx.dataframe.ColumnsContainer
6+
import org.jetbrains.kotlinx.dataframe.geo.GeoDataFrame
7+
import org.jetbrains.kotlinx.dataframe.geo.WithGeometry
8+
import org.jetbrains.kotlinx.kandy.dsl.internal.DatasetBuilder
9+
import org.jetbrains.kotlinx.kandy.dsl.internal.dataframe.MultiLayerPlotBuilderImpl
10+
11+
/**
12+
* Represents a geo plotting context initialized with a [GeoDataFrame] as its primary dataset.
13+
* The class allows the seamless integration of the inner dataframe's columns into the plotting process.
14+
*
15+
* The implemented [ColumnsContainer] enables the user
16+
* to leverage the columns of the dataframe directly in the plotting process.
17+
*
18+
* In this scope, you can create geo layers that will use the `geometry` column of this GeoDataFrame
19+
* by default.
20+
*
21+
* @param T the type of the GeoDataFrame.
22+
*/
23+
public class GeoDataFramePlotBuilder<T : WithGeometry> @PublishedApi internal constructor(
24+
@PublishedApi
25+
internal val geodf: GeoDataFrame<T>,
26+
) : MultiLayerPlotBuilderImpl(), ColumnsContainer<T> by geodf.df, GeoDataScope {
27+
override val datasetBuilders: MutableList<DatasetBuilder> = mutableListOf(GeoDataBuilder(geodf))
28+
}

0 commit comments

Comments
 (0)