Skip to content

Commit 22d52ec

Browse files
authored
GEOMESA-3575 Fix truncated z-column alignment with z partitions (#3554)
* Fix display of bounds in manage metadata command
1 parent 37ecb45 commit 22d52ec

5 files changed

Lines changed: 83 additions & 22 deletions

File tree

geomesa-fs/geomesa-fs-storage/geomesa-fs-storage-core/src/main/scala/org/locationtech/geomesa/fs/storage/core/StorageMetadata.scala

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
package org.locationtech.geomesa.fs.storage.core
1010

11-
import org.calrissian.mango.types.encoders.lexi.LongEncoder
1211
import org.calrissian.mango.types.{TypeEncoder, TypeRegistry}
1312
import org.geotools.api.feature.simple.SimpleFeatureType
1413
import org.geotools.api.filter.Filter
@@ -18,6 +17,7 @@ import org.locationtech.geomesa.index.index.attribute.AttributeIndexKey
1817
import org.locationtech.jts.geom._
1918

2019
import java.io.Closeable
20+
import java.util.HexFormat
2121

2222
/**
2323
* Metadata interface for managing storage partitions
@@ -172,8 +172,8 @@ object StorageMetadata {
172172
object Z2Encoder extends TypeEncoder[Point, String] {
173173

174174
private val sfc = Z2SFC
175-
private val longEncoder = new LongEncoder()
176175
private val factory = new GeometryFactory()
176+
private val hexFormat = HexFormat.of()
177177

178178
override val getAlias: String = "z2"
179179

@@ -183,11 +183,11 @@ object StorageMetadata {
183183
if (value == null) {
184184
throw new NullPointerException("Null values are not allowed")
185185
}
186-
longEncoder.encode(sfc.index(value.getX, value.getY))
186+
toHex(sfc.index(value.getX, value.getY))
187187
}
188188

189189
override def decode(value: String): Point = {
190-
val (x, y) = sfc.invert(longEncoder.decode(value))
190+
val (x, y) = sfc.invert(fromHex(value))
191191
factory.createPoint(new Coordinate(x, y))
192192
}
193193

@@ -198,7 +198,11 @@ object StorageMetadata {
198198
* @param maxRanges rough upper bound on the number of ranges to return
199199
*/
200200
def ranges(queries: Seq[(Double, Double, Double, Double)], maxRanges: Option[Int] = None): Seq[(String, String)] =
201-
sfc.ranges(queries, maxRanges = maxRanges).map(r => longEncoder.encode(r.lower) -> longEncoder.encode(r.upper))
201+
sfc.ranges(queries, maxRanges = maxRanges).map(r => toHex(r.lower) -> toHex(r.upper))
202+
203+
// since the first two bits are not used in our z values, drop them so that truncating the hex value aligns with our curve
204+
private def toHex(z: Long): String = hexFormat.toHexDigits(z << 2)
205+
private def fromHex(hex: String): Long = HexFormat.fromHexDigitsToLong(hex) >>> 2
202206
}
203207

204208
/**
@@ -207,8 +211,8 @@ object StorageMetadata {
207211
object XZ2Encoder extends TypeEncoder[Geometry, String] {
208212

209213
private val sfc = XZ2SFC
210-
private val longEncoder = new LongEncoder()
211214
private val factory = new GeometryFactory()
215+
private val hexFormat = HexFormat.of()
212216

213217
override val getAlias: String = "xz2"
214218

@@ -222,11 +226,11 @@ object StorageMetadata {
222226
if (env.isNull) {
223227
throw new NullPointerException("Geometry has a null envelope")
224228
}
225-
longEncoder.encode(sfc.index(env.getMinX, env.getMinY, env.getMaxX, env.getMaxY))
229+
hexFormat.toHexDigits(sfc.index(env.getMinX, env.getMinY, env.getMaxX, env.getMaxY))
226230
}
227231

228232
override def decode(value: String): Geometry = {
229-
val (xmin, ymin, xmax, ymax) = sfc.invert(longEncoder.decode(value))
233+
val (xmin, ymin, xmax, ymax) = sfc.invert(HexFormat.fromHexDigitsToLong(value))
230234
val ring = Array(
231235
new Coordinate(xmin, ymin),
232236
new Coordinate(xmin, ymax),
@@ -244,6 +248,6 @@ object StorageMetadata {
244248
* @param maxRanges a rough upper limit on the number of ranges to generate
245249
*/
246250
def ranges(queries: Seq[(Double, Double, Double, Double)], maxRanges: Option[Int] = None): Seq[(String, String)] =
247-
sfc.ranges(queries, maxRanges).map(r => longEncoder.encode(r.lower) -> longEncoder.encode(r.upper))
251+
sfc.ranges(queries, maxRanges).map(r => hexFormat.toHexDigits(r.lower) -> hexFormat.toHexDigits(r.upper))
248252
}
249253
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/***********************************************************************
2+
* Copyright (c) 2013-2025 General Atomics Integrated Intelligence, Inc.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Apache License, Version 2.0
5+
* which accompanies this distribution and is available at
6+
* https://www.apache.org/licenses/LICENSE-2.0
7+
***********************************************************************/
8+
9+
package org.locationtech.geomesa.fs.storage.core
10+
11+
import org.locationtech.geomesa.features.ScalaSimpleFeature
12+
import org.locationtech.geomesa.fs.storage.core.StorageMetadata.Z2Encoder
13+
import org.locationtech.geomesa.utils.geotools.SimpleFeatureTypes
14+
import org.locationtech.geomesa.utils.text.WKTUtils
15+
import org.locationtech.jts.geom.Point
16+
import org.specs2.mutable.SpecificationWithJUnit
17+
18+
import java.util.HexFormat
19+
20+
class StorageMetadataTest extends SpecificationWithJUnit {
21+
22+
private val sft = SimpleFeatureTypes.createType("test", "*geom:Point:srid=4326")
23+
24+
"Z2Encoder" should {
25+
"create truncate-able z values that align with our partition scheme" in {
26+
val ps = PartitionSchemeFactory.load(sft, "z2:bits=4")
27+
foreach(Seq(-67.5, -22.5, 22.5, 67.5)) { lat =>
28+
foreach(Seq(-135, -45, 45, 135)) { lon =>
29+
val pt = WKTUtils.read(s"POINT($lon $lat)").asInstanceOf[Point]
30+
val partition = ps.getPartition(ScalaSimpleFeature.create(sft, "", pt)).value.toInt
31+
val z2 = Z2Encoder.encode(pt)
32+
partition mustEqual HexFormat.fromHexDigit(z2.head)
33+
}
34+
}
35+
}
36+
}
37+
}

geomesa-fs/geomesa-fs-storage/geomesa-fs-storage-core/src/test/scala/org/locationtech/geomesa/fs/storage/core/metadata/SchemeFilterExtractionTest.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class SchemeFilterExtractionTest extends SpecificationWithJUnit {
4141
filters.head.partitions must
4242
containTheSameElementsAs(Seq(PartitionRange("hours:attribute=dtg", "80066ba0", "80066bb8"), PartitionRange("z2:attribute=geom:bits=2", "3", "4")))
4343
filters.head.columnBounds mustEqual
44-
Seq(ColumnOr(0, Seq(ColumnBound("80000160af049000", "80000160b42aec00"))), ColumnOr(1, Seq(ColumnBound("b000000000000000", "bfffffffffffffff"))))
44+
Seq(ColumnOr(0, Seq(ColumnBound("80000160af049000", "80000160b42aec00"))), ColumnOr(1, Seq(ColumnBound("c000000000000000", "fffffffffffffffc"))))
4545
}
4646
}
4747

geomesa-fs/geomesa-fs-storage/geomesa-fs-storage-core/src/test/scala/org/locationtech/geomesa/fs/storage/core/metadata/TestAbstractMetadata.scala

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ import com.typesafe.scalalogging.LazyLogging
1313
import org.apache.commons.io.FileUtils
1414
import org.geotools.filter.text.ecql.ECQL
1515
import org.locationtech.geomesa.features.ScalaSimpleFeature
16-
import org.locationtech.geomesa.fs.storage.core.StorageMetadata.{ColumnBounds, StorageFile}
16+
import org.locationtech.geomesa.fs.storage.core.StorageMetadata.{ColumnBounds, StorageFile, Z2Encoder}
1717
import org.locationtech.geomesa.utils.geotools.SimpleFeatureTypes
1818
import org.locationtech.geomesa.utils.io.WithClose
19+
import org.locationtech.geomesa.utils.text.{DateParsing, WKTUtils}
20+
import org.locationtech.jts.geom.Point
1921
import org.specs2.mutable.Specification
2022

2123
import java.net.URI
@@ -33,20 +35,35 @@ abstract class TestAbstractMetadata extends Specification with LazyLogging {
3335

3436
// note: ensure that partitions and bounds line up
3537
val partition1 = partitions("2026-03-05T00:00:00.000Z", "POINT (10 10)") // dtg 8000019cbb4b4c00/8000019cbb823a80
36-
val columnBounds1 = Seq(ColumnBounds(0, "name5", "name6"), ColumnBounds(1, "8000019cbb4b4c00", "8000019cbb823a80"),
37-
ColumnBounds(2, "b00239e50239e502"/*POINT(1 2)*/, "b085d4c185d4c185"/*POINT(11 12)*/))
38+
val columnBounds1 = Seq(
39+
ColumnBounds(0, "name5", "name6"),
40+
ColumnBounds(1, dateBounds("2026-03-05T00:00"), dateBounds("2026-03-05T01:00")),
41+
ColumnBounds(2, z2Bounds("POINT(1 2)"), z2Bounds("POINT(11 12)"))
42+
)
3843

3944
val partition2 = partitions("2026-03-04T00:00:00.000Z", "POINT (-10 10)") // dtg 8000019cb624f000/8000019cb65bde80
40-
val columnBounds2a = Seq(ColumnBounds(0, "name0", "name4"), ColumnBounds(1, "8000019cb624f000", "8000019cb6406740")/* first 30 minutes */,
41-
ColumnBounds(2, "a55229b45229b452"/*POINT(-11 2)*/, "a5d5c490d5c490d5"/*POINT(-1 12)*/))
42-
val columnBounds2b = Seq(ColumnBounds(0, "name0", "name4"), ColumnBounds(1, "8000019cb624f000", "8000019cb6406740")/* first 30 minutes */,
43-
ColumnBounds(2, "a5c0d485c0d485c0"/*POINT(-20 12)*/, "a5ed4fc5ed4fc5ed"/*POINT(-12 20)*/))
45+
val columnBounds2a = Seq(
46+
ColumnBounds(0, "name0", "name4"),
47+
ColumnBounds(1, dateBounds("2026-03-04T00:00"), dateBounds("2026-03-04T00:30"))/* first 30 minutes */,
48+
ColumnBounds(2, z2Bounds("POINT(-11 2)"), z2Bounds("POINT(-1 12)"))
49+
)
50+
val columnBounds2b = Seq(
51+
ColumnBounds(0, "name0", "name4"),
52+
ColumnBounds(1, dateBounds("2026-03-04T00:00"), dateBounds("2026-03-04T00:30"))/* first 30 minutes */,
53+
ColumnBounds(2, z2Bounds("POINT(-20 12)"), z2Bounds("POINT(-12 20)"))
54+
)
4455

4556
val partition3 = partitions("2026-03-03T00:00:00.000Z", "POINT (10 -10)") // dtg 8000019cb0fe9400/8000019cb1358280
46-
val columnBounds3a = Seq(ColumnBounds(0, "name7", "name9"), ColumnBounds(1, "8000019cb0fe9400", "8000019cb11a0b40")/* first 30 minutes */,
47-
ColumnBounds(2, "9a2a3b6f2a3b6f2a"/*POINT(1 -12)*/, "9aadd64badd64bad"/*POINT(11 -2)*/))
48-
val columnBounds3b = Seq(ColumnBounds(0, "name7", "name9"), ColumnBounds(1, "8000019cb11a0b40", "8000019cb1358280")/* last 30 minutes */,
49-
ColumnBounds(2, "9a2a3b6f2a3b6f2a"/*POINT(1 -12)*/, "9aadd64badd64bad"/*POINT(11 -2)*/))
57+
val columnBounds3a = Seq(
58+
ColumnBounds(0, "name7", "name9"),
59+
ColumnBounds(1, dateBounds("2026-03-03T00:00"), dateBounds("2026-03-03T00:30"))/* first 30 minutes */,
60+
ColumnBounds(2, z2Bounds("POINT(1 -12)"), z2Bounds("POINT(11 -2)"))
61+
)
62+
val columnBounds3b = Seq(
63+
ColumnBounds(0, "name7", "name9"),
64+
ColumnBounds(1, dateBounds("2026-03-03T00:30"), dateBounds("2026-03-03T01:00"))/* last 30 minutes */,
65+
ColumnBounds(2, z2Bounds("POINT(1 -12)"), z2Bounds("POINT(11 -2)"))
66+
)
5067

5168
val f1 = StorageFile("file1", partition1, 0L, bounds = columnBounds1, timestamp = System.currentTimeMillis() - 100)
5269
val f2a = StorageFile("file2a", partition2, 1L, bounds = columnBounds2a, timestamp = f1.timestamp + 10)
@@ -56,6 +73,9 @@ abstract class TestAbstractMetadata extends Specification with LazyLogging {
5673

5774
val files = Seq(f1, f2a, f2b, f3a, f3b)
5875

76+
private def dateBounds(dtg: String): String = StorageMetadata.TypeRegistry.encode(DateParsing.parseDate(dtg + ":00.000Z"))
77+
private def z2Bounds(pt: String): String = Z2Encoder.encode(WKTUtils.read(pt).asInstanceOf[Point])
78+
5979
private def partitions(dtg: String, geom: String): Partition = {
6080
val sf = ScalaSimpleFeature.create(sft, "", "", dtg, geom)
6181
Partition(schemes.map(_.getPartition(sf)))

geomesa-fs/geomesa-fs-tools/src/main/scala/org/locationtech/geomesa/fs/tools/ingest/FsManageMetadataCommand.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ object FsManageMetadataCommand extends LazyLogging {
9494

9595
case other => other
9696
}
97-
s"${metadata.sft.getDescriptor(b.attribute).getLocalName.padTo(padding, " ")} [ $lower,$upper ]"
97+
s"${metadata.sft.getDescriptor(b.attribute).getLocalName.padTo(padding, ' ')} [ $lower,$upper ]"
9898
}
9999
Command.user.info(s"File bounds:\n ${bounds.mkString("\n ")}")
100100
}

0 commit comments

Comments
 (0)