1 /***********************************************************************
2  * Copyright (c) 2013-2024 Commonwealth Computer Research, 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  * http://www.opensource.org/licenses/apache2.0.php.
7  ***********************************************************************/
8 
9 package org.locationtech.geomesa.utils.uuid
10 
11 import com.typesafe.scalalogging.LazyLogging
12 import org.geotools.api.feature.simple.{SimpleFeature, SimpleFeatureType}
13 import org.locationtech.geomesa.curve.TimePeriod.TimePeriod
14 import org.locationtech.geomesa.curve.{BinnedTime, Z3SFC}
15 import org.locationtech.geomesa.utils.geotools.RichSimpleFeatureType.RichSimpleFeatureType
16 import org.locationtech.geomesa.utils.index.ByteArrays
17 import org.locationtech.jts.geom.{Geometry, Point}
18 
19 import java.util.{Date, UUID}
20 import scala.util.hashing.MurmurHash3
21 
22 /**
23  * Creates feature id based on the z3 index.
24  */
25 class Z3FeatureIdGenerator extends FeatureIdGenerator {
26   override def createId(sft: SimpleFeatureType, sf: SimpleFeature): String = {
27     if (sft.getGeometryDescriptor == null) {
28       // no geometry in this feature type - just use a random UUID
29       UUID.randomUUID().toString
30     } else {
31       Z3UuidGenerator.createUuid(sft, sf).toString
32     }
33   }
34 }
35 
36 /**
37  * UUID generator that creates UUIDs that sort by z3 index.
38  * UUIDs will be prefixed with a shard number, which will ensure some distribution of values as well
39  * as allow pre-splitting of tables based on hex values.
40  *
41  * Uses variant 2 (IETF) and version 4 (for random UUIDs, although it's not totally random).
42  * See https://en.wikipedia.org/wiki/Universally_unique_identifier#Variants_and_versions
43  *
44  * Format is:
45  *
46  *   4 bits for a shard - enough for a single hex digit
47  *   44 bits of the z3 index value
48  *   4 bits for the UUID version
49  *   12 more bits of the z3 index value
50  *   2 bits for the UUID variant
51  *   62 bits of randomness
52  */
53 object Z3UuidGenerator extends RandomLsbUuidGenerator with LazyLogging {
54 
55   import org.locationtech.geomesa.utils.geotools.Conversions.RichGeometry
56 
57   private val NullGeom = "Cannot meaningfully index a feature with a NULL geometry"
58 
59   /**
60     * Creates a UUID where the first 8 bytes are based on the z3 index of the feature and
61     * the second 8 bytes are based on a random number.
62     *
63     * This provides uniqueness along with locality.
64     *
65     * @param sft simple feature type
66     * @param sf feature
67     * @return
68     */
69   def createUuid(sft: SimpleFeatureType, sf: SimpleFeature): UUID = {
70     val (x, y) = sf.getAttribute(sft.getGeomIndex) match {
71       case null => throw new IllegalArgumentException(NullGeom)
72       case g: Geometry if g.isEmpty => (0d, 0d)
73       case p: Point => (p.getX, p.getY)
74       case g: Geometry => val p = g.safeCentroid(); (p.getX, p.getY)
75     }
76     val time =
77       sft.getDtgIndex
78           .flatMap(i => Option(sf.getAttribute(i)))
79           .map(_.asInstanceOf[Date].getTime)
80           .getOrElse(System.currentTimeMillis())
81 
82     createUuid(x, y, time, sft.getZ3Interval)
83   }
84 
85   /**
86     * Create a UUID based on the raw values that make up the z3
87     *
88     * @param geom geometry
89     * @param time millis since java epoch
90     * @param period z3 time period
91     * @return
92     */
93   def createUuid(geom: Geometry, time: Long, period: TimePeriod): UUID = {
94     if (geom == null) {
95       throw new IllegalArgumentException(NullGeom)
96     } else if (geom.isEmpty) {
97       createUuid(0, 0, time, period)
98     } else {
99       val pt = geom.safeCentroid()
100       createUuid(pt.getX, pt.getY, time, period)
101     }
102   }
103 
104 
105   /**
106    * Create a UUID based on the raw values that make up the z3, optimized for point geometries
107    *
108    * @param pt point
109    * @param time millis since java epoch
110    * @param period z3 time period
111    * @return
112    */
113   def createUuid(pt: Point, time: Long, period: TimePeriod): UUID = {
114     if (pt == null) {
115       throw new IllegalArgumentException(NullGeom)
116     } else if (pt.isEmpty) {
117       createUuid(0, 0, time, period)
118     } else {
119       createUuid(pt.getX, pt.getY, time, period)
120     }
121   }
122 
123   /**
124    * Create a UUID based on the raw values that make up the z3, optimized for point geometries
125    *
126    * @param x x coord
127    * @param y y coord
128    * @param time millis since java epoch
129    * @param period z3 time period
130    * @return
131    */
132   def createUuid(x: Double, y: Double, time: Long, period: TimePeriod): UUID = {
133     // create the random part
134     // this uses the same temp array we use later, so be careful with the order this gets called
135     val leastSigBits = createRandomLsb()
136 
137     val z3 = {
138       val BinnedTime(b, t) = BinnedTime.timeToBinnedTime(period)(time)
139       val z = Z3SFC(period).index(x, y, t)
140       ByteArrays.toBytes(b, z)
141     }
142 
143     // shard is first 4 bits of our uuid (e.g. 1 hex char) - this allows nice pre-splitting
144     val shard = math.abs(MurmurHash3.bytesHash(z3, MurmurHash3.arraySeed) % 16).toByte
145 
146     val msb = getTempByteArray
147     // set the shard bits, then the z3 bits
148     msb(0) = lohi(shard, z3(0))
149     msb(1) = lohi(z3(0), z3(1))
150     msb(2) = lohi(z3(1), z3(2))
151     msb(3) = lohi(z3(2), z3(3))
152     msb(4) = lohi(z3(3), z3(4))
153     msb(5) = lohi(z3(4), z3(5))
154     msb(6) = lohi(0, (z3(5) << 4).asInstanceOf[Byte]) // leave 4 bits for the version
155     msb(7) = z3(6)
156     // we drop the last 4 bytes of the z3 to ensure some randomness
157     // that leaves us 62 bits of randomness, and still gives us ~10 bits per dimension for locality
158 
159     // set the UUID version - we skipped those bits when writing
160     setVersion(msb)
161     // create the long
162     val mostSigBits = ByteArrays.readLong(msb)
163 
164     new UUID(mostSigBits, leastSigBits)
165   }
166 
167   /**
168     * Gets the z3 time period bin based on a z3 uuid
169     *
170     * @param uuid uuid, as bytes
171     * @return
172     */
173   def timeBin(uuid: Array[Byte], offset: Int = 0): Short = timeBin(uuid(offset), uuid(offset + 1), uuid(offset + 2))
174 
175   /**
176     * Gets the z3 time period bin based on a z3 uuid
177     *
178     * @param b0 first byte of the uuid
179     * @param b1 second byte of the uuid
180     * @param b2 third byte of the uuid
181     * @return
182     */
183   def timeBin(b0: Byte, b1: Byte, b2: Byte): Short = {
184     // undo the lo-hi byte merging to get the two bytes for the time period
185     ByteArrays.readShort(Array(lohi(b0, b1), lohi(b1, b2)))
186   }
187 
188   // takes 4 low bits from b1 as the new hi bits, and 4 high bits of b2 as the new low bits, of a new byte
189   private def lohi(b1: Byte, b2: Byte): Byte =
190     ((java.lang.Byte.toUnsignedInt(b1) << 4) | (java.lang.Byte.toUnsignedInt(b2) >>> 4)).asInstanceOf[Byte]
191 }
Line Stmt Id Pos Tree Symbol Tests Code
27 14620 1210 - 1243 Apply java.lang.Object.== sft.getGeometryDescriptor().==(null)
29 14621 1320 - 1346 Apply java.util.UUID.toString java.util.UUID.randomUUID().toString()
29 14622 1320 - 1346 Block java.util.UUID.toString java.util.UUID.randomUUID().toString()
31 14623 1366 - 1410 Apply java.util.UUID.toString Z3UuidGenerator.createUuid(sft, sf).toString()
31 14624 1366 - 1410 Block java.util.UUID.toString Z3UuidGenerator.createUuid(sft, sf).toString()
57 14625 2253 - 2311 Literal <nosymbol> "Cannot meaningfully index a feature with a NULL geometry"
70 14626 2689 - 2689 Select scala.Tuple2._1 x$1._1
70 14627 2692 - 2692 Select scala.Tuple2._2 x$1._2
78 14628 3034 - 3052 Apply org.geotools.api.feature.simple.SimpleFeature.getAttribute sf.getAttribute(i)
78 14629 3027 - 3053 Apply scala.Option.apply scala.Option.apply[Object](sf.getAttribute(i))
79 14630 3070 - 3098 Apply java.util.Date.getTime x$2.asInstanceOf[java.util.Date].getTime()
80 14631 3121 - 3147 Apply java.lang.System.currentTimeMillis java.lang.System.currentTimeMillis()
80 14632 2987 - 3148 Apply scala.Option.getOrElse org.locationtech.geomesa.utils.geotools.RichSimpleFeatureType.RichSimpleFeatureType(sft).getDtgIndex.flatMap[Object](((i: Int) => scala.Option.apply[Object](sf.getAttribute(i)))).map[Long](((x$2: Object) => x$2.asInstanceOf[java.util.Date].getTime())).getOrElse[Long](java.lang.System.currentTimeMillis())
82 14633 3177 - 3194 Select org.locationtech.geomesa.utils.geotools.RichSimpleFeatureType.RichSimpleFeatureType.getZ3Interval org.locationtech.geomesa.utils.geotools.RichSimpleFeatureType.RichSimpleFeatureType(sft).getZ3Interval
82 14634 3154 - 3195 Apply org.locationtech.geomesa.utils.uuid.Z3UuidGenerator.createUuid Z3UuidGenerator.this.createUuid(x, y, time, org.locationtech.geomesa.utils.geotools.RichSimpleFeatureType.RichSimpleFeatureType(sft).getZ3Interval)
94 14635 3485 - 3497 Apply java.lang.Object.== geom.==(null)
95 14636 3507 - 3551 Throw <nosymbol> throw new scala.`package`.IllegalArgumentException(Z3UuidGenerator.this.NullGeom)
95 14637 3507 - 3551 Block <nosymbol> throw new scala.`package`.IllegalArgumentException(Z3UuidGenerator.this.NullGeom)
96 14638 3567 - 3579 Apply org.locationtech.jts.geom.Geometry.isEmpty geom.isEmpty()
96 14646 3563 - 3722 If <nosymbol> if (geom.isEmpty()) Z3UuidGenerator.this.createUuid(0.0, 0.0, time, period) else { val pt: org.locationtech.jts.geom.Point = org.locationtech.geomesa.utils.geotools.Conversions.RichGeometry(geom).safeCentroid(); Z3UuidGenerator.this.createUuid(pt.getX(), pt.getY(), time, period) }
97 14639 3589 - 3619 Apply org.locationtech.geomesa.utils.uuid.Z3UuidGenerator.createUuid Z3UuidGenerator.this.createUuid(0.0, 0.0, time, period)
97 14640 3589 - 3619 Block org.locationtech.geomesa.utils.uuid.Z3UuidGenerator.createUuid Z3UuidGenerator.this.createUuid(0.0, 0.0, time, period)
98 14645 3631 - 3722 Block <nosymbol> { val pt: org.locationtech.jts.geom.Point = org.locationtech.geomesa.utils.geotools.Conversions.RichGeometry(geom).safeCentroid(); Z3UuidGenerator.this.createUuid(pt.getX(), pt.getY(), time, period) }
99 14641 3648 - 3667 Apply org.locationtech.geomesa.utils.geotools.Conversions.RichGeometry.safeCentroid org.locationtech.geomesa.utils.geotools.Conversions.RichGeometry(geom).safeCentroid()
100 14642 3685 - 3692 Apply org.locationtech.jts.geom.Point.getX pt.getX()
100 14643 3694 - 3701 Apply org.locationtech.jts.geom.Point.getY pt.getY()
100 14644 3674 - 3716 Apply org.locationtech.geomesa.utils.uuid.Z3UuidGenerator.createUuid Z3UuidGenerator.this.createUuid(pt.getX(), pt.getY(), time, period)
114 14647 4028 - 4038 Apply java.lang.Object.== pt.==(null)
115 14648 4048 - 4092 Throw <nosymbol> throw new scala.`package`.IllegalArgumentException(Z3UuidGenerator.this.NullGeom)
115 14649 4048 - 4092 Block <nosymbol> throw new scala.`package`.IllegalArgumentException(Z3UuidGenerator.this.NullGeom)
116 14650 4108 - 4118 Apply org.locationtech.jts.geom.Point.isEmpty pt.isEmpty()
116 14657 4104 - 4226 If <nosymbol> if (pt.isEmpty()) Z3UuidGenerator.this.createUuid(0.0, 0.0, time, period) else Z3UuidGenerator.this.createUuid(pt.getX(), pt.getY(), time, period)
117 14651 4128 - 4158 Apply org.locationtech.geomesa.utils.uuid.Z3UuidGenerator.createUuid Z3UuidGenerator.this.createUuid(0.0, 0.0, time, period)
117 14652 4128 - 4158 Block org.locationtech.geomesa.utils.uuid.Z3UuidGenerator.createUuid Z3UuidGenerator.this.createUuid(0.0, 0.0, time, period)
119 14653 4189 - 4196 Apply org.locationtech.jts.geom.Point.getX pt.getX()
119 14654 4198 - 4205 Apply org.locationtech.jts.geom.Point.getY pt.getY()
119 14655 4178 - 4220 Apply org.locationtech.geomesa.utils.uuid.Z3UuidGenerator.createUuid Z3UuidGenerator.this.createUuid(pt.getX(), pt.getY(), time, period)
119 14656 4178 - 4220 Block org.locationtech.geomesa.utils.uuid.Z3UuidGenerator.createUuid Z3UuidGenerator.this.createUuid(pt.getX(), pt.getY(), time, period)
135 14658 4707 - 4724 Apply org.locationtech.geomesa.utils.uuid.RandomLsbUuidGenerator.createRandomLsb Z3UuidGenerator.this.createRandomLsb()
138 14659 4762 - 4762 Select scala.Tuple2._1 x$3._1
138 14660 4765 - 4765 Select scala.Tuple2._2 x$3._2
139 14661 4826 - 4839 Apply org.locationtech.geomesa.curve.Z3SFC.apply org.locationtech.geomesa.curve.Z3SFC.apply(period)
139 14662 4826 - 4854 Apply org.locationtech.geomesa.curve.Z3SFC.index qual$1.index(x$1, x$2, x$3, x$4)
140 14663 4861 - 4885 Apply org.locationtech.geomesa.utils.index.ByteArrays.toBytes org.locationtech.geomesa.utils.index.ByteArrays.toBytes(b, z)
144 14664 5010 - 5063 Apply scala.Int.% scala.util.hashing.MurmurHash3.bytesHash(z3, 1007110753).%(16)
144 14665 5001 - 5071 Select scala.Int.toByte scala.math.`package`.abs(scala.util.hashing.MurmurHash3.bytesHash(z3, 1007110753).%(16)).toByte
146 14666 5087 - 5103 Select org.locationtech.geomesa.utils.uuid.Version4UuidGenerator.getTempByteArray Z3UuidGenerator.this.getTempByteArray
148 14667 5156 - 5157 Literal <nosymbol> 0
148 14668 5173 - 5178 Apply scala.Array.apply z3.apply(0)
148 14669 5161 - 5179 Apply org.locationtech.geomesa.utils.uuid.Z3UuidGenerator.lohi Z3UuidGenerator.this.lohi(shard, z3.apply(0))
148 14670 5152 - 5179 Apply scala.Array.update msb.update(0, Z3UuidGenerator.this.lohi(shard, z3.apply(0)))
149 14671 5188 - 5189 Literal <nosymbol> 1
149 14672 5198 - 5203 Apply scala.Array.apply z3.apply(0)
149 14673 5205 - 5210 Apply scala.Array.apply z3.apply(1)
149 14674 5193 - 5211 Apply org.locationtech.geomesa.utils.uuid.Z3UuidGenerator.lohi Z3UuidGenerator.this.lohi(z3.apply(0), z3.apply(1))
149 14675 5184 - 5211 Apply scala.Array.update msb.update(1, Z3UuidGenerator.this.lohi(z3.apply(0), z3.apply(1)))
150 14676 5220 - 5221 Literal <nosymbol> 2
150 14677 5230 - 5235 Apply scala.Array.apply z3.apply(1)
150 14678 5237 - 5242 Apply scala.Array.apply z3.apply(2)
150 14679 5225 - 5243 Apply org.locationtech.geomesa.utils.uuid.Z3UuidGenerator.lohi Z3UuidGenerator.this.lohi(z3.apply(1), z3.apply(2))
150 14680 5216 - 5243 Apply scala.Array.update msb.update(2, Z3UuidGenerator.this.lohi(z3.apply(1), z3.apply(2)))
151 14681 5252 - 5253 Literal <nosymbol> 3
151 14682 5262 - 5267 Apply scala.Array.apply z3.apply(2)
151 14683 5269 - 5274 Apply scala.Array.apply z3.apply(3)
151 14684 5257 - 5275 Apply org.locationtech.geomesa.utils.uuid.Z3UuidGenerator.lohi Z3UuidGenerator.this.lohi(z3.apply(2), z3.apply(3))
151 14685 5248 - 5275 Apply scala.Array.update msb.update(3, Z3UuidGenerator.this.lohi(z3.apply(2), z3.apply(3)))
152 14686 5284 - 5285 Literal <nosymbol> 4
152 14687 5294 - 5299 Apply scala.Array.apply z3.apply(3)
152 14688 5301 - 5306 Apply scala.Array.apply z3.apply(4)
152 14689 5289 - 5307 Apply org.locationtech.geomesa.utils.uuid.Z3UuidGenerator.lohi Z3UuidGenerator.this.lohi(z3.apply(3), z3.apply(4))
152 14690 5280 - 5307 Apply scala.Array.update msb.update(4, Z3UuidGenerator.this.lohi(z3.apply(3), z3.apply(4)))
153 14691 5316 - 5317 Literal <nosymbol> 5
153 14692 5326 - 5331 Apply scala.Array.apply z3.apply(4)
153 14693 5333 - 5338 Apply scala.Array.apply z3.apply(5)
153 14694 5321 - 5339 Apply org.locationtech.geomesa.utils.uuid.Z3UuidGenerator.lohi Z3UuidGenerator.this.lohi(z3.apply(4), z3.apply(5))
153 14695 5312 - 5339 Apply scala.Array.update msb.update(5, Z3UuidGenerator.this.lohi(z3.apply(4), z3.apply(5)))
154 14696 5348 - 5349 Literal <nosymbol> 6
154 14697 5358 - 5359 Literal <nosymbol> 0
154 14698 5365 - 5366 Literal <nosymbol> 5
154 14699 5371 - 5372 Literal <nosymbol> 4
154 14700 5361 - 5392 TypeApply scala.Any.asInstanceOf z3.apply(5).<<(4).asInstanceOf[Byte]
154 14701 5353 - 5393 Apply org.locationtech.geomesa.utils.uuid.Z3UuidGenerator.lohi Z3UuidGenerator.this.lohi(0, z3.apply(5).<<(4).asInstanceOf[Byte])
154 14702 5344 - 5393 Apply scala.Array.update msb.update(6, Z3UuidGenerator.this.lohi(0, z3.apply(5).<<(4).asInstanceOf[Byte]))
155 14703 5434 - 5435 Literal <nosymbol> 7
155 14704 5439 - 5444 Apply scala.Array.apply z3.apply(6)
155 14705 5430 - 5444 Apply scala.Array.update msb.update(7, z3.apply(6))
160 14706 5683 - 5698 Apply org.locationtech.geomesa.utils.uuid.Version4UuidGenerator.setVersion Z3UuidGenerator.this.setVersion(msb)
162 14707 5744 - 5768 Apply org.locationtech.geomesa.utils.index.ByteArrays.readLong org.locationtech.geomesa.utils.index.ByteArrays.readLong(msb, org.locationtech.geomesa.utils.index.ByteArrays.readLong$default$2)
164 14708 5774 - 5809 Apply java.util.UUID.<init> new java.util.UUID(mostSigBits, leastSigBits)
173 14709 6001 - 6013 Apply scala.Array.apply uuid.apply(offset)
173 14710 6020 - 6030 Apply scala.Int.+ offset.+(1)
173 14711 6015 - 6031 Apply scala.Array.apply uuid.apply(offset.+(1))
173 14712 6038 - 6048 Apply scala.Int.+ offset.+(2)
173 14713 6033 - 6049 Apply scala.Array.apply uuid.apply(offset.+(2))
173 14714 5993 - 6050 Apply org.locationtech.geomesa.utils.uuid.Z3UuidGenerator.timeBin Z3UuidGenerator.this.timeBin(uuid.apply(offset), uuid.apply(offset.+(1)), uuid.apply(offset.+(2)))
185 14715 6418 - 6430 Apply org.locationtech.geomesa.utils.uuid.Z3UuidGenerator.lohi Z3UuidGenerator.this.lohi(b0, b1)
185 14716 6432 - 6444 Apply org.locationtech.geomesa.utils.uuid.Z3UuidGenerator.lohi Z3UuidGenerator.this.lohi(b1, b2)
185 14717 6412 - 6445 Apply scala.Array.apply scala.Array.apply(Z3UuidGenerator.this.lohi(b0, b1), Z3UuidGenerator.this.lohi(b1, b2))
185 14718 6391 - 6446 Apply org.locationtech.geomesa.utils.index.ByteArrays.readShort org.locationtech.geomesa.utils.index.ByteArrays.readShort(scala.Array.apply(Z3UuidGenerator.this.lohi(b0, b1), Z3UuidGenerator.this.lohi(b1, b2)), org.locationtech.geomesa.utils.index.ByteArrays.readShort$default$2)
190 14719 6648 - 6649 Literal <nosymbol> 4
190 14720 6654 - 6692 Apply scala.Int.>>> java.lang.Byte.toUnsignedInt(b2).>>>(4)
190 14721 6610 - 6713 TypeApply scala.Any.asInstanceOf java.lang.Byte.toUnsignedInt(b1).<<(4).|(java.lang.Byte.toUnsignedInt(b2).>>>(4)).asInstanceOf[Byte]