Skip to content

Conversation

@koperagen
Copy link
Collaborator

fixes #1565

@koperagen koperagen added this to the 1.0.0-Beta5 milestone Dec 15, 2025
@koperagen koperagen self-assigned this Dec 15, 2025
@koperagen koperagen added the geo Anything related to the GeoDataFrame label Dec 15, 2025
simplePointsGeoDf.writeGeoJson(tempFile)

assertEquals(simplePointsGeoDf, GeoDataFrame.readGeoJson(tempFile.toURI().toURL()))
val loadedGeoDataFrame = GeoDataFrame.readGeoJson(tempFile.toURI().toURL())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to add more IO overloads ;)

assertEquals(simplePointsGeoDf, GeoDataFrame.readGeoJson(tempFile.toURI().toURL()))
val loadedGeoDataFrame = GeoDataFrame.readGeoJson(tempFile.toURI().toURL())
assertEquals(simplePointsGeoDf.df, loadedGeoDataFrame.df)
// TODO: Doesn't work because of how equality between CRS is checked by geotools
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proper way to check CRS equality in Geotools is
CRS.equals, or CRS.equalsIgnoreMetadata if you don't want to compare metedata.

But there, shouldn't they be equal?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't help :(
println(CRS.equalsIgnoreMetadata(simplePointsGeoDf.crs, loadedGeoDataFrame.crs))

false

They are in fact identical, two lines just go in different order
image


val featureBuilder = SimpleFeatureBuilder(featureType)

// if ID is present, SortedMap in DefaultFeatureCollection sorts rows by ID lexicographically
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, ok. Is that a known issue in Geotools? Is there an official workaround?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think they did it intentionally, doesn't look like a bug
Got pure geotools repro here. I see that just reading and writing back the same collection is idempotent, i.e. rows go the same order. But problem arises when we extract data from collection (create dataframe) and then re-create feature collection from it. Please have a look.

Output of the program:

Original order:
  feature-0 -> Point 1
  feature-1 -> Point 2

Built feature id: fid-1a9d9e5e_19b2837675d_-8000
Built feature id: fid-1a9d9e5e_19b2837675d_-7fff

Written JSON:
{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[31.5,51.5]},"properties":{"name":"Point 2"},"id":"fid-1a9d9e5e_19b2837675d_-7fff"},{"type":"Feature","geometry":{"type":"Point","coordinates":[30.5,50.5]},"properties":{"name":"Point 1"},"id":"fid-1a9d9e5e_19b2837675d_-8000"}]}

After write+read:
  fid-1a9d9e5e_19b2837675d_-7fff -> Point 2
  fid-1a9d9e5e_19b2837675d_-8000 -> Point 1

Somewhat interesting to see that in original feature collections feature-0 and feature-1 are IDs - but no ID in the original file

package org.jetbrains.kotlinx.dataframe.geo

import org.geotools.data.simple.SimpleFeatureCollection
import org.geotools.feature.DefaultFeatureCollection
import org.geotools.feature.simple.SimpleFeatureBuilder
import org.geotools.feature.simple.SimpleFeatureTypeBuilder
import org.geotools.geojson.feature.FeatureJSON
import org.locationtech.jts.geom.Geometry
import java.io.File

fun main() {
    val geojson = """
     {
      "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"
          }
        }
      ]
    }
    """.trimIndent()

    val featureJSON = FeatureJSON()

    // Read original
    val original = featureJSON.readFeatureCollection(geojson.reader()) as SimpleFeatureCollection
    println("Original order:")
    printNames(original)

    // Extract to intermediate storage (simulating DataFrame)
    val data = mutableListOf<Pair<String, Geometry>>()
    original.features().use { iter ->
        while (iter.hasNext()) {
            val f = iter.next()
            data.add(f.getAttribute("name") as String to f.defaultGeometry as Geometry)
        }
    }

    // Rebuild FeatureCollection from scratch (like toSimpleFeatureCollection does)
    val typeBuilder = SimpleFeatureTypeBuilder()
    typeBuilder.name = "geodata"
    typeBuilder.add("the_geom", Geometry::class.java)
    typeBuilder.add("name", String::class.java)
    val featureType = typeBuilder.buildFeatureType()

    val rebuilt = DefaultFeatureCollection()
    val featureBuilder = SimpleFeatureBuilder(featureType)
    for ((name, geometry) in data) {
        featureBuilder.add(geometry)
        featureBuilder.add(name)
        val feature = featureBuilder.buildFeature(null) // ID is auto-generated
        println("Built feature id: ${feature.id}")
        rebuilt.add(feature)
    }

    // Write to file
    val tempFile = File.createTempFile("test", ".geojson")
    tempFile.outputStream().use { featureJSON.writeFeatureCollection(rebuilt, it) }

    println("\nWritten JSON:")
    println(tempFile.readText())

    // Read back
    val reloaded = featureJSON.readFeatureCollection(tempFile) as SimpleFeatureCollection
    println("\nAfter write+read:")
    printNames(reloaded)

    tempFile.delete()
}

fun printNames(fc: SimpleFeatureCollection) {
    fc.features().use { iter ->
        while (iter.hasNext()) {
            val f = iter.next()
            println("  ${f.id} -> ${f.getAttribute("name")}")
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

geo Anything related to the GeoDataFrame

Projects

None yet

Development

Successfully merging this pull request may close these issues.

writeGeoJson breaks ordering of rows:

3 participants