Skip to content

Commit

Permalink
add Tile.parentAtZoom
Browse files Browse the repository at this point in the history
outerCoordinates holeCoordinates helpers for polygon and polygon coordinates
  • Loading branch information
jillesvangurp committed Feb 4, 2025
1 parent 46f6742 commit 73413c5
Show file tree
Hide file tree
Showing 6 changed files with 52 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,7 @@ class GeoHashUtils {
}
if (includePartial) {
partiallyContained.forEach { hash ->
decodeBbox(hash).polygon().coordinates?.get(0)?.let { ring ->
decodeBbox(hash).polygon().outerCoordinates.let { ring ->
if (ring.firstOrNull { polygonContains(it, arrayOf(coordinates)) } != null) {
fullyContained.add(hash)
}
Expand Down
10 changes: 10 additions & 0 deletions src/commonMain/kotlin/com/jillesvangurp/geo/tiles/Tile.kt
Original file line number Diff line number Diff line change
Expand Up @@ -250,5 +250,15 @@ fun Tile.parentTiles(): List<Tile> {
return parentTiles
}

fun Tile.parentAtZoom(zoom: Int): Tile {
require(zoom in 0 until this.zoom) { "Target zoom must be less than current zoom ($this.zoom)" }

val scale = 1 shl (this.zoom - zoom)
val parentX = this.x / scale
val parentY = this.y / scale

return Tile(parentX, parentY, zoom)
}

fun PointCoordinates.tiles() =
coordinateToTile(lat = this.latitude, lon = this.longitude, zoom = MAX_ZOOM).let { listOf(it) + it.parentTiles() }
22 changes: 21 additions & 1 deletion src/commonMain/kotlin/com/jillesvangurp/geojson/geojson.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.jillesvangurp.geo.GeoGeometry
import com.jillesvangurp.geo.GeoGeometry.Companion.ensureFollowsRightHandSideRule
import com.jillesvangurp.geo.GeoGeometry.Companion.roundToDecimals
import com.jillesvangurp.geo.GeoHashUtils
import com.jillesvangurp.geojson.Geometry.Polygon
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Required
import kotlinx.serialization.SerialName
Expand All @@ -31,6 +32,7 @@ typealias MultiPolygonCoordinates = Array<PolygonCoordinates>
* BoundingBox = [westLongitude,southLatitude,eastLongitude,westLatitude]
*/
typealias BoundingBox = DoubleArray

fun BoundingBox.toGeometry(): Geometry.Polygon {
val coordinates = arrayOf(
arrayOf(
Expand Down Expand Up @@ -61,7 +63,8 @@ fun BoundingBox.contains(point: PointCoordinates): Boolean {
return withinLongitude && withinLatitude
}

fun PolygonCoordinates.contains(point: PointCoordinates): Boolean = GeoGeometry.polygonContains(point.latitude,point.longitude, polygonCoordinatesPoints = this)
fun PolygonCoordinates.contains(point: PointCoordinates): Boolean =
GeoGeometry.polygonContains(point.latitude, point.longitude, polygonCoordinatesPoints = this)

fun Geometry.contains(point: PointCoordinates): Boolean {
return when (this) {
Expand Down Expand Up @@ -141,30 +144,35 @@ fun Geometry.ensureHasAltitude(): Geometry = when (this) {
if (this.coordinates?.size == 3) this
else this.copy(coordinates = this.coordinates?.let { it + doubleArrayOf(0.0) })
}

is Geometry.MultiPoint -> {
this.copy(coordinates = this.coordinates?.map {
if (it.size == 3) it else it + doubleArrayOf(0.0)
}?.toTypedArray())
}

is Geometry.LineString -> {
this.copy(coordinates = this.coordinates?.map {
if (it.size == 3) it else it + doubleArrayOf(0.0)
}?.toTypedArray())
}

is Geometry.MultiLineString -> {
this.copy(coordinates = this.coordinates?.map { line ->
line.map {
if (it.size == 3) it else it + doubleArrayOf(0.0)
}.toTypedArray()
}?.toTypedArray())
}

is Geometry.Polygon -> {
this.copy(coordinates = this.coordinates?.map { ring ->
ring.map {
if (it.size == 3) it else it + doubleArrayOf(0.0)
}.toTypedArray()
}?.toTypedArray())
}

is Geometry.MultiPolygon -> {
this.copy(coordinates = this.coordinates?.map { polygon ->
polygon.map { ring ->
Expand All @@ -174,6 +182,7 @@ fun Geometry.ensureHasAltitude(): Geometry = when (this) {
}.toTypedArray()
}?.toTypedArray())
}

is Geometry.GeometryCollection -> {
this.copy(geometries = this.geometries.map { it.ensureHasAltitude() }.toTypedArray())
}
Expand Down Expand Up @@ -202,9 +211,11 @@ fun PointCoordinates.normalize(): PointCoordinates {
in 90.0..180.0 -> {
180.0 - lat
}

in -180.0..-90.0 -> {
-180.0 - lat
}

else -> {
lat
}
Expand Down Expand Up @@ -327,6 +338,7 @@ fun Geometry.asFeature(
): Feature {
return Feature(this, properties, bbox)
}

fun Geometry.asFeatureWithProperties(
bbox: BoundingBox? = null,
propertiesBuilder: (JsonObjectBuilder.() -> Unit)
Expand Down Expand Up @@ -497,6 +509,7 @@ sealed class Geometry {
override fun toString(): String = Json.encodeToString(Geometry.serializer(), this)
}


@Serializable
@SerialName("MultiPolygon")
data class MultiPolygon(
Expand Down Expand Up @@ -548,6 +561,13 @@ sealed class Geometry {
}
}

val PolygonCoordinates.outerCoordinates get() = this[0]
val PolygonCoordinates.holeCoordinates get() = this.slice(1..<this.size)

val Polygon.outerCoordinates get() = coordinates?.outerCoordinates ?: error("no points found")
val Polygon.holeCoordinates get() = coordinates?.holeCoordinates ?: error("no points found")


@Serializable
data class Feature(
val geometry: Geometry?,
Expand Down
19 changes: 17 additions & 2 deletions src/commonTest/kotlin/com/jillesvangurp/geo/tiles/TileTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import com.jillesvangurp.geo.tiles.Tile.Companion.coordinateToTile
import com.jillesvangurp.geojson.contains
import com.jillesvangurp.geojson.latitude
import com.jillesvangurp.geojson.longitude
import com.jillesvangurp.geojson.outerCoordinates
import com.jillesvangurp.geojson.polygon
import com.jillesvangurp.geojson.toGeometry
import io.kotest.assertions.assertSoftly
import io.kotest.assertions.withClue
Expand All @@ -19,8 +21,8 @@ import kotlin.test.Test
fun randomTileCoordinate() =
doubleArrayOf(Random.nextDouble(-180.0, 180.0), Random.nextDouble(Tile.MIN_LATITUDE, Tile.MAX_LATITUDE))

fun randomTile(): Tile {
val zl = (0..22).random()
fun randomTile(minZoom:Int=0): Tile {
val zl = (minZoom..22).random()
val maxXY = 1 shl zl
return Tile(
x = (0 until maxXY).random(),
Expand Down Expand Up @@ -260,4 +262,17 @@ class TileTest {
}
}
}

@Test
fun shouldBeContainedByParent() {
assertSoftly {
repeat(100) {
val tile = randomTile(5)
val parent = tile.parentAtZoom(tile.zoom - 3)
tile.bbox.polygon().outerCoordinates.forEach { point ->
parent.bbox.contains(point) shouldBe true
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.jillesvangurp.geojson.eastLongitude
import com.jillesvangurp.geojson.latitude
import com.jillesvangurp.geojson.longitude
import com.jillesvangurp.geojson.northLatitude
import com.jillesvangurp.geojson.outerCoordinates
import com.jillesvangurp.geojson.polygon
import com.jillesvangurp.geojson.southLatitude
import com.jillesvangurp.geojson.westLongitude
Expand Down Expand Up @@ -119,7 +120,7 @@ class GeoHashUtilsTest {
}
""".trimIndent()
val p = DEFAULT_JSON.decodeFromString(Geometry.serializer(), concavePolygon) as Geometry.Polygon
val coordinates = p.coordinates?.get(0) ?: throw IllegalStateException()
val coordinates = p.outerCoordinates
val hashes = GeoHashUtils.geoHashesForLinearRing(coordinates = coordinates, includePartial = true)

println(hashes.size)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class RotationTest {
// we'll allow a few meters deviation. Earth is not perfectly spherical
GeoGeometry.distance(oranienburgerTor, moved.centroid()) shouldBeLessThan 10.0
moved as Geometry.Polygon
moved.coordinates?.get(0)!!.forEach {
moved.outerCoordinates.forEach {
// radius of the circle should be similar, it will change a little
val radius = GeoGeometry.distance(moved.centroid(), it)
radius shouldBeGreaterThan 19.0
Expand Down

0 comments on commit 73413c5

Please sign in to comment.