├── img ├── curves-3D-scores.png ├── curves-3d-prorated.png ├── curves-4D-scores.png ├── curves-4d-prorated.png ├── parcoords-teaser.png ├── cville-covering-35-h.png ├── cville-covering-35-r.png ├── cville-covering-35-z.png ├── readme-composed-curve-example.png └── readme-composed-curve-example.dot ├── .gitignore ├── src ├── main │ └── scala │ │ └── org │ │ └── eichelberger │ │ └── sfc │ │ ├── utils │ │ ├── Timing.scala │ │ ├── BitManipulations.scala │ │ ├── Lexicographics.scala │ │ ├── CompositionParser.scala │ │ ├── LocalityEstimator.scala │ │ ├── CompositionWKT.scala │ │ └── RenderSource.scala │ │ ├── examples │ │ ├── quickstart │ │ │ ├── Example2.scala │ │ │ ├── Example1.scala │ │ │ └── Example3.scala │ │ ├── Geohash.scala │ │ └── composition │ │ │ └── contrast │ │ │ └── StackingVariants.scala │ │ ├── study │ │ ├── composition │ │ │ ├── ContiguousRangesStudy.scala │ │ │ ├── CompositionSampleData.scala │ │ │ ├── SchemaStudy.scala │ │ │ └── StackingVariantsStudy.scala │ │ ├── locality │ │ │ ├── LocalityEstimatorStudy.scala │ │ │ └── LocalityAnalysisStudy.scala │ │ ├── planner │ │ │ └── PlannerStudy.scala │ │ └── OutputFormats.scala │ │ ├── ZCurve.scala │ │ ├── RowMajorCurve.scala │ │ ├── planners │ │ ├── OffSquareQuadTreePlanner.scala │ │ ├── SquareQuadTreePlanner.scala │ │ └── ZCurvePlanner.scala │ │ ├── ComposedCurve.scala │ │ └── Dimensions.scala └── test │ ├── scala │ └── org │ │ └── eichelberger │ │ └── sfc │ │ ├── RowMajorCurveTest.scala │ │ ├── utils │ │ ├── BitManipulationsTest.scala │ │ ├── LocalityEstimatorTest.scala │ │ ├── LexicographicTest.scala │ │ ├── CompositionParserTest.scala │ │ └── RenderSourceTest.scala │ │ ├── examples │ │ ├── GeohashTest.scala │ │ └── composition │ │ │ └── contrast │ │ │ └── StackingVariantsTest.scala │ │ ├── GenericCurveValidation.scala │ │ ├── ComposedCurveTest.scala │ │ ├── DimensionTest.scala │ │ ├── SpaceFillingCurveTest.scala │ │ ├── ZCurveTest.scala │ │ └── CompactHilbertCurveTest.scala │ └── resources │ └── composite-curve-parcoord │ └── composite-parcoord.html ├── pom.xml └── LICENSE /img/curves-3D-scores.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cne1x/sfcs/HEAD/img/curves-3D-scores.png -------------------------------------------------------------------------------- /img/curves-3d-prorated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cne1x/sfcs/HEAD/img/curves-3d-prorated.png -------------------------------------------------------------------------------- /img/curves-4D-scores.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cne1x/sfcs/HEAD/img/curves-4D-scores.png -------------------------------------------------------------------------------- /img/curves-4d-prorated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cne1x/sfcs/HEAD/img/curves-4d-prorated.png -------------------------------------------------------------------------------- /img/parcoords-teaser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cne1x/sfcs/HEAD/img/parcoords-teaser.png -------------------------------------------------------------------------------- /img/cville-covering-35-h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cne1x/sfcs/HEAD/img/cville-covering-35-h.png -------------------------------------------------------------------------------- /img/cville-covering-35-r.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cne1x/sfcs/HEAD/img/cville-covering-35-r.png -------------------------------------------------------------------------------- /img/cville-covering-35-z.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cne1x/sfcs/HEAD/img/cville-covering-35-z.png -------------------------------------------------------------------------------- /img/readme-composed-curve-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cne1x/sfcs/HEAD/img/readme-composed-curve-example.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | *.jar 4 | 5 | # IntelliJ specific 6 | .idea/** 7 | *.iml 8 | 9 | # sbt specific 10 | .cache/ 11 | .history/ 12 | .lib/ 13 | dist/* 14 | target/ 15 | lib_managed/ 16 | src_managed/ 17 | project/boot/ 18 | project/plugins/project/ 19 | 20 | # Scala-IDE specific 21 | .scala_dependencies 22 | .worksheet 23 | -------------------------------------------------------------------------------- /src/main/scala/org/eichelberger/sfc/utils/Timing.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.utils 2 | 3 | object Timing { 4 | def time[T](a: () => T): (T, Long) = { 5 | val nanoStart = System.nanoTime() 6 | val result = a() 7 | val nanoStop = System.nanoTime() 8 | val nanosElapsed = nanoStop - nanoStart 9 | val msElapsed = nanosElapsed / 1e6.toLong 10 | (result, msElapsed) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /img/readme-composed-curve-example.dot: -------------------------------------------------------------------------------- 1 | digraph G { 2 | node [ shape="rectangle" style="filled" fillcolor="#FFFFFF" ] 3 | 4 | R -> t 5 | R -> H 6 | H -> x 7 | H -> y 8 | 9 | R [ label="row-major curve" fillcolor="#CCCCCC" ] 10 | H [ label="compact Hilbert curve" fillcolor="#CCCCCC" ] 11 | t [ label="date-time (20 bits)" ] 12 | x [ label="longitude (18 bits)" ] 13 | y [ label="latitude (17 bits)" ] 14 | } 15 | -------------------------------------------------------------------------------- /src/main/scala/org/eichelberger/sfc/examples/quickstart/Example2.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.examples.quickstart 2 | 3 | object Example2 extends App { 4 | import org.eichelberger.sfc._ 5 | import org.eichelberger.sfc.SpaceFillingCurve._ 6 | 7 | // create a 4D curve 8 | val zCurve = new ZCurve(OrdinalVector(10, 20, 15, 4)) 9 | 10 | // map from an input point to a hashed point 11 | val idx = zCurve.index(OrdinalVector(7, 28, 2001, 8)) 12 | println(s"(7, 28, 2001, 8) -> $idx") 13 | 14 | // invert the map back to inputs 15 | val point = zCurve.inverseIndex(idx) 16 | println(s"$idx <- $point") 17 | } 18 | -------------------------------------------------------------------------------- /src/main/scala/org/eichelberger/sfc/examples/quickstart/Example1.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.examples.quickstart 2 | 3 | object Example1 extends App { 4 | import org.eichelberger.sfc.examples.Geohash 5 | 6 | // create the curve 7 | val totalPrecision = 35 // bits to be allocated between longitude and latitude 8 | val geohash = new Geohash(totalPrecision) 9 | 10 | // compute a hash from a (lon, lat) point 11 | val hashCville = geohash.pointToHash(Seq(-78.49, 38.04)) 12 | println(s"Representative Charlottesville point hashes to $hashCville") 13 | 14 | // compute the (inverse) cell from the hash 15 | val cellCville = geohash.hashToCell(hashCville) 16 | println(s"Representative Charlottesville hash corresponds to cell $cellCville") 17 | } 18 | -------------------------------------------------------------------------------- /src/main/scala/org/eichelberger/sfc/examples/Geohash.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.examples 2 | 3 | import org.eichelberger.sfc.utils.Lexicographics 4 | import Lexicographics._ 5 | import org.eichelberger.sfc.SpaceFillingCurve._ 6 | import org.eichelberger.sfc._ 7 | import org.eichelberger.sfc.Dimensions._ 8 | 9 | 10 | /** 11 | * Simple example of how to construct a standard Geohash (see geohash.org) 12 | * from the primitive pieces available in this project. 13 | */ 14 | 15 | class Geohash(precisions: Long) 16 | extends ComposedCurve( 17 | new ZCurve(OrdinalVector(precisions - (precisions >> 1L), precisions >> 1L)), 18 | Seq( 19 | DefaultDimensions.createLongitude(precisions - (precisions >> 1L)), 20 | DefaultDimensions.createLatitude(precisions >> 1L) 21 | ) 22 | ) { 23 | } -------------------------------------------------------------------------------- /src/main/scala/org/eichelberger/sfc/utils/BitManipulations.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.utils 2 | 3 | object BitManipulations { 4 | // if the first on-bit is at position P, then this routine returns a 5 | // mask in which all of the bits from 0..P are turned on, and all of 6 | // the bits from P+1..63 are off 7 | def usedMask(x: Long): Long = { 8 | var y = x | (x >> 1L) 9 | y = y | (y >> 2L) 10 | y = y | (y >> 4L) 11 | y = y | (y >> 8L) 12 | y = y | (y >> 16L) 13 | y | (y >> 32L) 14 | } 15 | 16 | def sharedBitPrefix(a: Long, b: Long): Long = 17 | a & ~usedMask(a ^ b) 18 | 19 | def commonBlockMin(a: Long, b: Long): Long = 20 | a & ~usedMask(a ^ b) 21 | 22 | def commonBlockMax(a: Long, b: Long): Long = { 23 | val mask = usedMask(a ^ b) 24 | (a & ~mask) | mask 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/test/scala/org/eichelberger/sfc/RowMajorCurveTest.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc 2 | 3 | import com.typesafe.scalalogging.slf4j.LazyLogging 4 | import org.eichelberger.sfc.CompactHilbertCurve.Mask 5 | import org.eichelberger.sfc.SpaceFillingCurve.{OrdinalVector, SpaceFillingCurve, _} 6 | import org.junit.runner.RunWith 7 | import org.specs2.mutable.Specification 8 | import org.specs2.runner.JUnitRunner 9 | 10 | @RunWith(classOf[JUnitRunner]) 11 | class RowMajorCurveTest extends Specification with GenericCurveValidation with LazyLogging { 12 | sequential 13 | 14 | def curveName = "RowmajorCurve" 15 | 16 | def createCurve(precisions: OrdinalNumber*): SpaceFillingCurve = 17 | RowMajorCurve(precisions.toOrdinalVector) 18 | 19 | "rowmajor space-filling curves" should { 20 | "satisfy the ordering constraints" >> { 21 | timeTestOrderings() must beTrue 22 | } 23 | 24 | "identify sub-ranges correctly" >> { 25 | val sfc = createCurve(3, 3) 26 | val query = Query(Seq(OrdinalRanges(OrdinalPair(1, 2)), OrdinalRanges(OrdinalPair(1, 3)))) 27 | val ranges = sfc.getRangesCoveringQuery(query).toList 28 | 29 | for (i <- 0 until ranges.size) { 30 | println(s"[rowmajor ranges: query $query] range $i = ${ranges(i)}") 31 | } 32 | 33 | ranges(0) must equalTo(OrdinalPair(9, 11)) 34 | ranges(1) must equalTo(OrdinalPair(17, 19)) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/scala/org/eichelberger/sfc/utils/BitManipulationsTest.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.utils 2 | 3 | import com.typesafe.scalalogging.slf4j.LazyLogging 4 | import org.junit.runner.RunWith 5 | import org.specs2.mutable.Specification 6 | import org.specs2.runner.JUnitRunner 7 | 8 | import BitManipulations._ 9 | 10 | @RunWith(classOf[JUnitRunner]) 11 | class BitManipulationsTest extends Specification with LazyLogging { 12 | "static methods" should { 13 | "usedMask" >> { 14 | // single bits 15 | for (pos <- 0 to 62) { 16 | val v = 1L << pos.toLong 17 | val actual = usedMask(v) 18 | val expected = (1L << (pos + 1L)) - 1L 19 | println(s"[usedMask single bit] pos $pos, value $v, actual $actual, expected $expected") 20 | actual must equalTo(expected) 21 | } 22 | 23 | // full bit masks 24 | for (pos <- 0 to 62) { 25 | val expected = (1L << (pos.toLong + 1L)) - 1L 26 | val actual = usedMask(expected) 27 | println(s"[usedMask full bit masks] pos $pos, value $expected, actual $actual, expected $expected") 28 | actual must equalTo(expected) 29 | } 30 | 31 | usedMask(0) must equalTo(0) 32 | } 33 | 34 | "sharedBitPrefix" >> { 35 | sharedBitPrefix(2, 3) must equalTo(2) 36 | sharedBitPrefix(178, 161) must equalTo(160) 37 | } 38 | 39 | "common block extrema" >> { 40 | commonBlockMin(178, 161) must equalTo(160) 41 | commonBlockMax(178, 161) must equalTo(191) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/scala/org/eichelberger/sfc/utils/LocalityEstimatorTest.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.utils 2 | 3 | import com.typesafe.scalalogging.slf4j.LazyLogging 4 | import org.eichelberger.sfc.{CompactHilbertCurve, RowMajorCurve, ZCurve} 5 | import org.junit.runner.RunWith 6 | import org.specs2.mutable.Specification 7 | import org.specs2.runner.JUnitRunner 8 | 9 | @RunWith(classOf[JUnitRunner]) 10 | class LocalityEstimatorTest extends Specification with LazyLogging { 11 | sequential 12 | 13 | "locality" should { 14 | "evaluate on square 2D curves" >> { 15 | (1 to 6).foreach { p => 16 | val locR = LocalityEstimator(RowMajorCurve(p, p)).locality 17 | println(s"[LOCALITY R($p, $p)] $locR") 18 | 19 | val locZ = LocalityEstimator(ZCurve(p, p)).locality 20 | println(s"[LOCALITY Z($p, $p)] $locZ") 21 | 22 | val locH = LocalityEstimator(CompactHilbertCurve(p, p)).locality 23 | println(s"[LOCALITY H($p, $p)] $locH") 24 | } 25 | 26 | 1 must beEqualTo(1) 27 | } 28 | 29 | "evaluate on non-square 2D curves" >> { 30 | (1 to 6).foreach { p => 31 | val locR = LocalityEstimator(RowMajorCurve(p << 1L, p)).locality 32 | println(s"[LOCALITY R(${p*2}, $p)] $locR") 33 | 34 | val locZ = LocalityEstimator(ZCurve(p << 1L, p)).locality 35 | println(s"[LOCALITY Z(${p*2}, $p)] $locZ") 36 | 37 | val locH = LocalityEstimator(CompactHilbertCurve(p << 1L, p)).locality 38 | println(s"[LOCALITY H(${p*2}, $p)] $locH") 39 | } 40 | 41 | 1 must beEqualTo(1) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/scala/org/eichelberger/sfc/study/composition/ContiguousRangesStudy.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.study.composition 2 | 3 | import org.eichelberger.sfc.SpaceFillingCurve.{OrdinalVector, OrdinalPair} 4 | import org.eichelberger.sfc.utils.CompositionParser 5 | 6 | object ContiguousRangesStudy extends App { 7 | def combinationsPerDimension(cardinality: Long) = 8 | (cardinality * (cardinality + 1L)) >> 1L 9 | 10 | def combinationsPerCube(cardinalities: Seq[Long]) = 11 | cardinalities.foldLeft(1L)((acc, cardinality) => acc * combinationsPerDimension(cardinality)) 12 | 13 | def queryIterator(precisions: OrdinalVector) = new Iterator[Seq[OrdinalPair]] { 14 | val n = precisions.size 15 | val cardinalities: Seq[Long] = precisions.toSeq.map(p => 1L << p) 16 | val MaxCardinality = combinationsPerCube(cardinalities) 17 | 18 | var state: Long = 0 19 | 20 | def getDimRange(n: Long, c: Long): OrdinalPair = { 21 | var inc = c 22 | var sum = inc 23 | while (sum <= n) { 24 | inc -= 1 25 | sum += inc 26 | } 27 | val min = c - inc 28 | val max = c - sum + n 29 | OrdinalPair(min, max) 30 | } 31 | 32 | def hasNext: Boolean = state < MaxCardinality 33 | 34 | def next(): Seq[OrdinalPair] = { 35 | var stateCopy = state 36 | val result = for (i <- 0 until n) yield { 37 | val dimIndex = stateCopy % cardinalities(i) 38 | stateCopy /= cardinalities(i).toLong 39 | getDimRange(dimIndex, cardinalities(i)) 40 | } 41 | state += 1L 42 | result 43 | } 44 | } 45 | 46 | val Precision = 3 47 | 48 | val curves = Seq( 49 | s"R($Precision, $Precision, $Precision)", 50 | s"Z($Precision, $Precision, $Precision)", 51 | s"H($Precision, $Precision, $Precision)", 52 | s"R($Precision, H($Precision, $Precision))", 53 | s"R(H($Precision, $Precision), $Precision)" 54 | ).map(cString => CompositionParser.buildWholeNumberCurve(cString)) 55 | } 56 | -------------------------------------------------------------------------------- /src/main/scala/org/eichelberger/sfc/examples/quickstart/Example3.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.examples.quickstart 2 | 3 | object Example3 extends App { 4 | import org.eichelberger.sfc._ 5 | import org.eichelberger.sfc.Dimensions._ 6 | import org.eichelberger.sfc.SpaceFillingCurve._ 7 | import org.joda.time.{DateTimeZone, DateTime} 8 | 9 | // create the dimensions that can manage user-space 10 | val x = DefaultDimensions.createLongitude(18) // ~153 meters per cell (@ equator) 11 | val y = DefaultDimensions.createLatitude(17) // ~153 meters per cell 12 | val t = DefaultDimensions.createNearDateTime( 13 | new DateTime(1970, 1, 1, 0, 0, 0, DateTimeZone.forID("UTC")), 14 | new DateTime(2010, 12, 31, 23, 59, 59, DateTimeZone.forID("UTC")), 15 | 20 16 | ) 17 | 18 | // compose the curve with dimensions 19 | val curve = new ComposedCurve( 20 | RowMajorCurve(20, 35), 21 | Seq( 22 | t, 23 | new ComposedCurve( 24 | CompactHilbertCurve(18, 17), 25 | Seq(x, y) 26 | ) 27 | ) 28 | ) 29 | 30 | // hashing points in user space 31 | val point = Seq( 32 | new DateTime(1998, 4, 7, 21, 15, 11, DateTimeZone.forID("UTC")), // t 33 | -78.49, // x 34 | 38.04 // y 35 | ) 36 | val hash = curve.pointToHash(point) 37 | println(s"$point -> $hash") 38 | 39 | // fetching user-space cells from hash value 40 | val cell = curve.hashToCell(hash) 41 | println(s"$cell <- $hash") 42 | 43 | // identify the top-level index-ranges that cover a query 44 | val query = Cell(Seq( 45 | DefaultDimensions.createNearDateTime( 46 | new DateTime(1998, 6, 15, 0, 0, 0, DateTimeZone.forID("UTC")), 47 | new DateTime(1998, 7, 15, 23, 59, 59, DateTimeZone.forID("UTC")), 48 | 0 49 | ), 50 | DefaultDimensions.createDimension("x", -80.0, -79.0, 0), 51 | DefaultDimensions.createDimension("y", 38.0, 39.0, 0) 52 | )) 53 | val ranges = curve.getRangesCoveringCell(query).toList 54 | println(s"Number of ranges: ${ranges.size}") 55 | val totalCells = ranges.map(_.size).sum 56 | println(s"Total cells in ranges: $totalCells") 57 | } 58 | -------------------------------------------------------------------------------- /src/main/scala/org/eichelberger/sfc/study/locality/LocalityEstimatorStudy.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.study.locality 2 | 3 | import org.eichelberger.sfc.examples.composition.contrast.FactoryXYZT 4 | import org.eichelberger.sfc.study.{ColumnSpec, OutputMetadata, MirroredTSV} 5 | import org.eichelberger.sfc.{ComposedCurve, CompactHilbertCurve, ZCurve, RowMajorCurve} 6 | import org.eichelberger.sfc.SpaceFillingCurve._ 7 | import org.eichelberger.sfc.utils.LocalityEstimator 8 | 9 | object LocalityEstimatorStudy 10 | extends MirroredTSV( 11 | "/tmp/locality.tsv", 12 | OutputMetadata(Seq( 13 | ColumnSpec("top.curve", isQuoted = true), 14 | ColumnSpec("curve", isQuoted = true), 15 | ColumnSpec("dimensions", isQuoted = false), 16 | ColumnSpec("total.precision", isQuoted = false), 17 | ColumnSpec("plys", isQuoted = false), 18 | ColumnSpec("locality", isQuoted = false), 19 | ColumnSpec("normalized.locality", isQuoted = false), 20 | ColumnSpec("locality.inv", isQuoted = false), 21 | ColumnSpec("normalized.locality.inv", isQuoted = false), 22 | ColumnSpec("sample.size", isQuoted = false), 23 | ColumnSpec("sample.coverage", isQuoted = false) 24 | )), 25 | writeHeader = true 26 | ) with App { 27 | 28 | def test(curve: ComposedCurve): Unit = { 29 | val loc = LocalityEstimator(curve).locality 30 | val data = Seq( 31 | curve.name.take(1), 32 | curve.name, 33 | curve.numLeafNodes, 34 | curve.M, 35 | curve.plys, 36 | loc.locality, 37 | loc.normalizedLocality, 38 | loc.localityInverse, 39 | loc.normalizedLocalityInverse, 40 | loc.sampleSize, 41 | loc.coverage 42 | ) 43 | println(data) 44 | } 45 | 46 | for (totalPrecision <- 4 to 40 by 4) { 47 | // 4D, horizontal 48 | FactoryXYZT(totalPrecision, 1).getCurves.foreach(curve => test(curve)) 49 | 50 | // 4D, mixed (2, 2) 51 | FactoryXYZT(totalPrecision, 2).getCurves.foreach(curve => test(curve)) 52 | 53 | // 4D, mixed (3, 1) 54 | FactoryXYZT(totalPrecision, -2).getCurves.foreach(curve => test(curve)) 55 | 56 | // 4D, vertical 57 | FactoryXYZT(totalPrecision, 3).getCurves.foreach(curve => test(curve)) 58 | } 59 | 60 | close() 61 | } 62 | -------------------------------------------------------------------------------- /src/main/scala/org/eichelberger/sfc/ZCurve.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc 2 | 3 | import com.typesafe.scalalogging.slf4j.LazyLogging 4 | import org.eichelberger.sfc.planners.{SquareQuadTreePlanner, ZCurvePlanner} 5 | import org.eichelberger.sfc.utils.Lexicographics 6 | import Lexicographics._ 7 | import org.eichelberger.sfc.SpaceFillingCurve._ 8 | 9 | object ZCurve { 10 | def apply(x: OrdinalNumber*): ZCurve = new ZCurve(OrdinalVector(x: _*)) 11 | } 12 | 13 | case class ZCurve(precisions: OrdinalVector) extends SpaceFillingCurve with SquareQuadTreePlanner with Lexicographic with LazyLogging { 14 | import org.eichelberger.sfc.ZCurve._ 15 | 16 | val name = "Z" 17 | 18 | // pre-compute bit-to-dimension assignments 19 | val bitAssignments: Seq[(Int, Int)] = (0 until M).foldLeft((precisions.toSeq, 0, Seq[(Int,Int)]()))((acc, bitPos) => acc match { 20 | case (precisionsLeft, dimension, assignmentsSoFar) => 21 | var nextDim = dimension 22 | var i = 0 23 | while (precisionsLeft(nextDim) < 1 && i < n) { 24 | i = i - 1 25 | nextDim = (nextDim + 1) % n 26 | } 27 | require(precisionsLeft(nextDim) > 0, s"Next dimension $nextDim has a remaining count of ${precisionsLeft(nextDim)}; expected a number greater than zero") 28 | 29 | val nextPrecisionsLeft = precisionsLeft.take(nextDim) ++ Seq(precisionsLeft(nextDim) - 1) ++ precisionsLeft.drop(nextDim + 1) 30 | val nextDimension = (nextDim + 1) % n 31 | val nextAssignments = assignmentsSoFar ++ Seq((nextDim, nextPrecisionsLeft(nextDim).toInt)) 32 | (nextPrecisionsLeft, nextDimension, nextAssignments) 33 | })._3 34 | 35 | def index(point: OrdinalVector): OrdinalNumber = { 36 | var result = 0L 37 | var bitPos = 0 38 | while (bitPos < M) { 39 | val (dimNum, bitPosInner) = bitAssignments(bitPos) 40 | result = (result << 1L) | bitAt(point(dimNum), bitPosInner) 41 | bitPos = bitPos + 1 42 | } 43 | result 44 | } 45 | 46 | def inverseIndex(ordinal: OrdinalNumber): OrdinalVector = { 47 | var vector = List.fill(n)(0L).toOrdinalVector 48 | var i = 0 49 | while (i < M) { 50 | val (d, _) = bitAssignments(i) 51 | val newValue = (vector(d) << 1L) | bitAt(ordinal, M - 1 - i) 52 | vector = vector.set(d, newValue) 53 | i = i + 1 54 | } 55 | vector 56 | } 57 | 58 | def getRangesCoveringQuery(query: Query): Iterator[OrdinalPair] = 59 | getRangesCoveringQueryOnSquare(query) 60 | } -------------------------------------------------------------------------------- /src/main/scala/org/eichelberger/sfc/utils/Lexicographics.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.utils 2 | 3 | import org.eichelberger.sfc.SpaceFillingCurve._ 4 | 5 | object Lexicographics { 6 | case class Alphabet(symbols: Seq[String]) { 7 | val symbolMap = symbols.zipWithIndex.toMap 8 | 9 | def apply(x: Int): String = symbols(x) 10 | 11 | def size: Int = symbols.size 12 | 13 | val bitsPerSymbol: Int = { 14 | val bps = Math.round(Math.log(size) / Math.log(2)).toInt 15 | require((1L << bps) == size, s"Bits-per-symbol of $bps is not equivalent to an alphabet of $size symbols") 16 | bps 17 | } 18 | } 19 | 20 | val DefaultAlphabets: Map[Int, Alphabet] = Map( 21 | 1 -> "01", 22 | 2 -> "0123", 23 | 3 -> "01234567", 24 | 4 -> "0123456789abcdef", 25 | 5 -> "0123456789bcdefghjkmnpqrstuvwxyz", 26 | 6 -> "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" 27 | ) 28 | 29 | implicit def string2seq(s: String): Alphabet = 30 | Alphabet(s.split("").toSeq) 31 | 32 | trait Lexicographic { 33 | // the total bits precision summed across all dimensions 34 | def M: Int 35 | 36 | val alphabet: Alphabet = 37 | DefaultAlphabets.get(M) match { 38 | case Some(a) => a 39 | case None => 40 | // see if you can use an existing alphabet 41 | val irs = (6 to 1 by -1).zip((6 to 1 by -1).map(M % _)) 42 | val bestOpt = irs.find { case (i, r) => r == 0 } 43 | // fall back to using a base-2 encoding 44 | DefaultAlphabets(bestOpt.getOrElse((1,1))._1) 45 | } 46 | 47 | val lexStringLength: Int = { 48 | require(M % alphabet.bitsPerSymbol == 0, s"The total precision ($M) is not evenly divisible by the alphabet size (${alphabet.size})") 49 | M / alphabet.bitsPerSymbol 50 | } 51 | 52 | val lexMask: OrdinalNumber = (1L << alphabet.bitsPerSymbol) - 1L 53 | 54 | def toBitSource(x: OrdinalNumber): Iterator[OrdinalNumber] = 55 | (1 to lexStringLength).iterator.map(i => (x >> (M - i * alphabet.bitsPerSymbol)) & lexMask) 56 | 57 | def lexEncodeIndex(ordinal: OrdinalNumber): String = 58 | toBitSource(ordinal).map(value => alphabet(value.toInt)).mkString 59 | 60 | def lexDecodeIndex(hash: String): OrdinalNumber = { 61 | hash.foldLeft(0L)((ord, symbol) => { 62 | val bits = alphabet.symbolMap(symbol.toString) 63 | (ord << alphabet.bitsPerSymbol) | bits 64 | }) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/scala/org/eichelberger/sfc/RowMajorCurve.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc 2 | 3 | import com.typesafe.scalalogging.slf4j.LazyLogging 4 | import org.eichelberger.sfc.utils.Lexicographics 5 | import Lexicographics.Lexicographic 6 | import org.eichelberger.sfc.SpaceFillingCurve._ 7 | 8 | object RowMajorCurve { 9 | def apply(x: OrdinalNumber*): RowMajorCurve = new RowMajorCurve(OrdinalVector(x: _*)) 10 | } 11 | 12 | /** 13 | * Assumes that the dimensions are listed in order from most 14 | * significant (first) to least significant (last). 15 | * 16 | * If you think about this, it's really just bit-ordering: 17 | * The most significant bits are first, followed by the less 18 | * significant bits, and the least significant bits bring up 19 | * the end. 20 | */ 21 | case class RowMajorCurve(precisions: OrdinalVector) extends SpaceFillingCurve with Lexicographic with LazyLogging { 22 | import org.eichelberger.sfc.RowMajorCurve._ 23 | 24 | val name = "R" 25 | 26 | val bitMasks = precisions.x.map(p => (1L << p) - 1L) 27 | 28 | def index(point: OrdinalVector): OrdinalNumber = { 29 | var i = 0 30 | var acc = 0L 31 | while (i < precisions.size) { 32 | acc = (acc << precisions(i)) | (point(i) & bitMasks(i)) 33 | i = i + 1 34 | } 35 | acc 36 | } 37 | 38 | def inverseIndex(ordinal: OrdinalNumber): OrdinalVector = { 39 | var i = precisions.size - 1 40 | var point = OrdinalVector() 41 | var ord = ordinal 42 | while (i >= 0) { 43 | point = point ++ (ord & bitMasks(i)) 44 | ord = ord >> precisions(i) 45 | i = i - 1 46 | } 47 | point.reverse 48 | } 49 | 50 | def getRangesCoveringQuery(query: Query): Iterator[OrdinalPair] = { 51 | // quick check for "everything" 52 | if (isEverything(query)) 53 | return Seq(OrdinalPair(0, size - 1L)).iterator 54 | 55 | // naive: assume none of the dimensions is full-range 56 | // (if they are, the range-consolidation should fix it, albeit more slowly 57 | // than if we handled it up front) 58 | 59 | val allRangeSets: Seq[OrdinalRanges] = query.toSeq 60 | val lastRangeSet: OrdinalRanges = allRangeSets.last 61 | val prefinalRangeSets: Seq[OrdinalRanges] = 62 | if (allRangeSets.size > 1) allRangeSets.dropRight(1) 63 | else Seq() 64 | 65 | // only consider combinations preceding the last (least significant) dimension 66 | val itr = rangesCombinationsIterator(prefinalRangeSets) 67 | val ranges = itr.flatMap(vec => { 68 | lastRangeSet.toSeq.map(r => { 69 | val minIdx = index(vec ++ r.min) 70 | val maxIdx = index(vec ++ r.max) 71 | OrdinalPair(minIdx, maxIdx) 72 | }) 73 | }) 74 | 75 | // final clean-up 76 | consolidatedRangeIterator(ranges) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/scala/org/eichelberger/sfc/utils/LexicographicTest.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.utils 2 | 3 | import com.typesafe.scalalogging.slf4j.LazyLogging 4 | import org.eichelberger.sfc.SpaceFillingCurve.{OrdinalVector, ords2ordvec} 5 | import org.eichelberger.sfc.{DefaultDimensions, ZCurve} 6 | import org.junit.runner.RunWith 7 | import org.specs2.mutable.Specification 8 | import org.specs2.runner.JUnitRunner 9 | 10 | @RunWith(classOf[JUnitRunner]) 11 | class LexicographicTest extends Specification with LazyLogging { 12 | sequential 13 | 14 | "Lexicographical encoding" should { 15 | val precisions = new ords2ordvec(Seq(18L, 17L)).toOrdinalVector 16 | 17 | val sfc = ZCurve(precisions) 18 | 19 | val Longitude = DefaultDimensions.createLongitude(18L) 20 | val Latitude = DefaultDimensions.createLatitude(17L) 21 | 22 | "work for a known point" >> { 23 | val x = -78.488407 24 | val y = 38.038668 25 | 26 | val point = OrdinalVector(Longitude.index(x), Latitude.index(y)) 27 | val idx = sfc.index(point) 28 | val gh = sfc.lexEncodeIndex(idx) 29 | 30 | gh must equalTo("dqb0muw") 31 | } 32 | 33 | "be consistent round-trip" >> { 34 | val xs = (-180.0 to 180.0 by 33.3333).toSeq ++ Seq(180.0) 35 | val ys = (-90.0 to 90.0 by 33.3333).toSeq ++ Seq(90.0) 36 | for (x <- xs; y <- ys) { 37 | val ix = Longitude.index(x) 38 | val iy = Latitude.index(y) 39 | val point = OrdinalVector(ix, iy) 40 | val idx = sfc.index(point) 41 | val gh = sfc.lexEncodeIndex(idx) 42 | val idx2 = sfc.lexDecodeIndex(gh) 43 | idx2 must equalTo(idx) 44 | val point2 = sfc.inverseIndex(idx2) 45 | point2(0) must equalTo(ix) 46 | point2(1) must equalTo(iy) 47 | val rx = Longitude.inverseIndex(ix) 48 | val ry = Latitude.inverseIndex(iy) 49 | 50 | val sx = x.formatted("%8.3f") 51 | val sy = y.formatted("%8.3f") 52 | val sidx = idx.formatted("%20d") 53 | println(s"[LEXI ROUND-TRIP] POINT($sx $sy) -> $sidx = $gh -> ($rx, $ry)") 54 | } 55 | 56 | // degenerate 57 | 1 must equalTo(1) 58 | } 59 | } 60 | 61 | "multiple lexicographical encoders" should { 62 | "return different results for different base resolutions" >> { 63 | val x = -78.488407 64 | val y = 38.038668 65 | 66 | for (xBits <- 1 to 30; yBits <- xBits - 1 to xBits if yBits > 0) { 67 | val precisions = new ords2ordvec(Seq(xBits, yBits)).toOrdinalVector 68 | val sfc = ZCurve(precisions) 69 | 70 | val Longitude = DefaultDimensions.createLongitude(xBits) 71 | val Latitude = DefaultDimensions.createLatitude(yBits) 72 | 73 | val idx = sfc.index(OrdinalVector(Longitude.index(x), Latitude.index(y))) 74 | val gh = sfc.lexEncodeIndex(idx) 75 | val idx2 = sfc.lexDecodeIndex(gh) 76 | 77 | idx2 must equalTo(idx) 78 | 79 | println(s"[LEXI ACROSS RESOLUTIONS] mx $xBits + my $yBits = base ${sfc.alphabet.size}, idx $idx -> gh $gh -> $idx2") 80 | } 81 | 82 | // degenerate 83 | 1 must equalTo(1) 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/test/scala/org/eichelberger/sfc/examples/GeohashTest.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.examples 2 | 3 | import com.typesafe.scalalogging.slf4j.LazyLogging 4 | import org.eichelberger.sfc.SpaceFillingCurve._ 5 | import org.eichelberger.sfc.study.composition.CompositionSampleData._ 6 | import org.eichelberger.sfc.utils.Timing 7 | import org.eichelberger.sfc.{DefaultDimensions, Dimension} 8 | import org.junit.runner.RunWith 9 | import org.specs2.mutable.Specification 10 | import org.specs2.runner.JUnitRunner 11 | 12 | @RunWith(classOf[JUnitRunner]) 13 | class GeohashTest extends Specification with LazyLogging { 14 | val xCville = -78.488407 15 | val yCville = 38.038668 16 | 17 | "Geohash example" should { 18 | val geohash = new Geohash(35) 19 | 20 | "encode/decode round-trip for an interior point" >> { 21 | // encode 22 | val hash = geohash.pointToHash(Seq(xCville, yCville)) 23 | hash must equalTo("dqb0muw") 24 | 25 | // decode 26 | val cell = geohash.hashToCell(hash) 27 | println(s"[Geohash example, Charlottesville] POINT($xCville $yCville) -> $hash -> $cell") 28 | cell(0).containsAny(xCville) must beTrue 29 | cell(1).containsAny(yCville) must beTrue 30 | } 31 | 32 | "encode/decode properly at the four corners and the center" >> { 33 | for (x <- Seq(-180.0, 0.0, 180.0); y <- Seq(-90.0, 0.0, 90.0)) { 34 | // encode 35 | val hash = geohash.pointToHash(Seq(x, y)) 36 | 37 | // decode 38 | val cell = geohash.hashToCell(hash) 39 | println(s"[Geohash example, extrema] POINT($x $y) -> $hash -> $cell") 40 | cell(0).containsAny(x) must beTrue 41 | cell(1).containsAny(y) must beTrue 42 | } 43 | 44 | // degenerate test outcome 45 | 1 must equalTo(1) 46 | } 47 | 48 | def getCvilleRanges(curve: Geohash): (OrdinalPair, OrdinalPair, Iterator[OrdinalPair]) = { 49 | val lonIdxRange = OrdinalPair( 50 | curve.children(0).asInstanceOf[Dimension[Double]].index(bboxCville._1), 51 | curve.children(1).asInstanceOf[Dimension[Double]].index(bboxCville._3) 52 | ) 53 | val latIdxRange = OrdinalPair( 54 | curve.children(0).asInstanceOf[Dimension[Double]].index(bboxCville._2), 55 | curve.children(1).asInstanceOf[Dimension[Double]].index(bboxCville._4) 56 | ) 57 | val query = Query(Seq(OrdinalRanges(lonIdxRange), OrdinalRanges(latIdxRange))) 58 | val cellQuery = Cell(Seq( 59 | DefaultDimensions.createDimension("x", bboxCville._1, bboxCville._3, 0), 60 | DefaultDimensions.createDimension("y", bboxCville._2, bboxCville._4, 0) 61 | )) 62 | (lonIdxRange, latIdxRange, curve.getRangesCoveringCell(cellQuery)) 63 | } 64 | 65 | "generate valid selection indexes" >> { 66 | val (_, _, ranges) = getCvilleRanges(geohash) 67 | 68 | ranges.size must equalTo(90) 69 | } 70 | 71 | "report range efficiency" >> { 72 | def atPrecision(xBits: OrdinalNumber, yBits: OrdinalNumber): (Long, Long) = { 73 | val curve = new Geohash(xBits + yBits) 74 | val (lonRange, latRange, ranges) = getCvilleRanges(curve) 75 | (lonRange.size * latRange.size, ranges.size.toLong) 76 | } 77 | 78 | for (dimPrec <- 10 to 25) { 79 | val ((numCells, numRanges), ms) = Timing.time{ () => atPrecision(dimPrec, dimPrec - 1) } 80 | println(s"[ranges across scales, Charlottesville] precision ($dimPrec, ${dimPrec - 1}) -> $numCells / $numRanges = ${numCells / numRanges} in $ms milliseconds") 81 | } 82 | 83 | 1 must equalTo(1) 84 | } 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/main/scala/org/eichelberger/sfc/planners/OffSquareQuadTreePlanner.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.planners 2 | 3 | import org.eichelberger.sfc.SpaceFillingCurve._ 4 | 5 | trait OffSquareQuadTreePlanner { 6 | this: SpaceFillingCurve => 7 | 8 | // this method should work for Compact Hilbert, but is too generic to 9 | // be very efficient 10 | 11 | // this method works well for significantly non-square spaces 12 | 13 | def getRangesCoveringQueryOffSquare(query: Query): Iterator[OrdinalPair] = { 14 | // quick check for "everything" 15 | if (isEverything(query)) 16 | return Seq(OrdinalPair(0, size - 1L)).iterator 17 | 18 | // for each dimension, partition the ranges into bin pairs 19 | val covers = (0 until n).map(dimension => { 20 | val dimRanges: OrdinalRanges = query.toSeq(dimension) 21 | val dimCovers: Seq[OrdinalPair] = 22 | dimRanges.toSeq.flatMap(range => bitCoverages(range, precisions(dimension))) 23 | dimCovers 24 | }) 25 | 26 | // cross all of the dimensions, finding the contiguous cubes 27 | val counts = OrdinalVector(covers.map(_.size.toOrdinalNumber):_*) 28 | val itr = combinationsIterator(counts) 29 | val cubes: Iterator[OrdinalRanges] = itr.flatMap(combination => { 30 | // assemble the list of coverages from the combination 31 | val coverList = combination.toSeq.zipWithIndex.map { 32 | case (coord, dimension) => covers(dimension).toSeq(coord.toInt) 33 | } 34 | // use the minimum bin-size among these covers 35 | val incSize = coverList.map(_.max).min 36 | 37 | // cover all of the sub-cubes by this increment 38 | val lowerLeft = coverList.map(_.min) 39 | val counts = OrdinalVector(coverList.map(_.max / incSize):_*) 40 | val cubeCornerItr = combinationsIterator(counts) 41 | val innerCubes = cubeCornerItr.map(cubeCorner => { 42 | OrdinalRanges(lowerLeft.zip(cubeCorner.toSeq).map { 43 | case (left, factor) => 44 | val cubeDimMin = left + incSize * factor 45 | val cubeDimMax = left + incSize * (factor + 1L) - 1L 46 | OrdinalPair(cubeDimMin, cubeDimMax) 47 | }:_*) 48 | }) 49 | innerCubes 50 | }) 51 | 52 | def isPoint(cube: OrdinalRanges): Boolean = 53 | cube.toSeq.head.min == cube.toSeq.head.max 54 | 55 | // for each contiguous cube, find the corners, and return (min, max) as that cube's index range 56 | val dimFactors: List[OrdinalPair] = List.fill(n)(OrdinalPair(0, 1)) 57 | val ranges: Iterator[OrdinalPair] = cubes.toSeq.map(cube => { 58 | // if this is a point, use it 59 | if (isPoint(cube)) { 60 | // this is a single point 61 | val coord: OrdinalVector = cube.toSeq.map(_.min).toOrdinalVector 62 | val idx = index(coord) 63 | OrdinalPair(idx, idx) 64 | } 65 | else { 66 | // this doesn't represent a single point, but you know that it's 67 | // square on a power-of-two boundary, so all of the least significant 68 | // bits are necessarily inside (meaning that you only have to sample 69 | // one) 70 | val minSpan = cube.toSeq.map { 71 | case OrdinalPair(a, b) => b - a + 1 72 | }.min 73 | val precisionFree = Math.round(Math.log(minSpan) / Math.log(2.0)).toInt * n 74 | val point = OrdinalVector(cube.toSeq.map(_.min):_*) 75 | val idx = index(point) 76 | val idxBase = idx >>> precisionFree 77 | OrdinalPair( 78 | idxBase << precisionFree, 79 | ((idxBase + 1L) << precisionFree) - 1L 80 | ) 81 | } 82 | }).toIterator 83 | 84 | consolidatedRangeIterator(ranges) 85 | } 86 | } -------------------------------------------------------------------------------- /src/test/resources/composite-curve-parcoord/composite-parcoord.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

Instructions

15 | 16 |

select a subset of a dimension by dragging over the portion of the coordinate you wish to keep; 17 | de-select by single-clicking anywhere else on the coordinate

18 | 19 |

reorder the dimensions by dragging one coordinate

20 | 21 |

invert a dimensions by double-clicking on its header

22 | 23 |

color by a (numeric) dimension by clicking on its header

24 | 25 |
26 | 27 | 96 | 97 |

Acknowledgements

98 | 99 |

Thanks to...

100 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/test/scala/org/eichelberger/sfc/GenericCurveValidation.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc 2 | 3 | import org.eichelberger.sfc.SpaceFillingCurve.{OrdinalVector, SpaceFillingCurve, _} 4 | import org.eichelberger.sfc.utils.Timing 5 | 6 | import scala.collection.mutable 7 | import scala.util.{Success, Try} 8 | 9 | trait GenericCurveValidation { 10 | 11 | def curveName: String 12 | 13 | def createCurve(precisions: OrdinalNumber*): SpaceFillingCurve 14 | 15 | // utility method for converting a single cell index (not Hilbert ordered, 16 | // but naively ordered one dimension at a time) to a point 17 | def idx2pt(sfc: SpaceFillingCurve, idx: Long): OrdinalVector = { 18 | val rawCards = sfc.precisions.cardinalities.toSeq 19 | val cumCards: Seq[Long] = rawCards.dropRight(1).foldLeft(Seq[Long](1))((acc, m) => 20 | acc ++ Seq(acc.last * m)) 21 | val idxs = cumCards.foldRight((Seq[Long](), idx))((cs, acc) => { 22 | val (accList, accIdx) = acc 23 | val placeValue = Math.floor(accIdx / cs).toLong 24 | (accList ++ Seq(placeValue), accIdx % cs) 25 | })._1 26 | CompactHilbertCurve(idxs.reverse:_*).precisions 27 | } 28 | 29 | def testOrderings(): Boolean = { 30 | def testOrdering(sfcOpt: Option[SpaceFillingCurve]): Boolean = { 31 | sfcOpt.foreach(sfc => { 32 | val id = sfc.precisions.cardinalities.toSeq.mkString(" x ") 33 | println(s"Validating $curveName $id space...") 34 | 35 | var everSeen = mutable.BitSet() 36 | 37 | var i = 0 38 | while (i < sfc.size.toInt) { 39 | val point = idx2pt(sfc, i) 40 | val h = sfc.index(point) 41 | val point2 = sfc.inverseIndex(h) 42 | 43 | // validate the index range 44 | if (h >= sfc.size) throw new Exception(s"Index overflow: $h >= ${sfc.size}") 45 | if (h < 0) throw new Exception(s"Index underflow: $h < 0") 46 | 47 | // must have an inverse that maps to this cell's point 48 | if (point2 != point) throw new Exception(s"Invalid round-trip: input $point -> #$h -> return $point2") 49 | 50 | // accumulate this index 51 | if (everSeen(h.toInt)) throw new Exception(s"Index collision: input $point -> $h") 52 | everSeen = everSeen + h.toInt 53 | 54 | i = i + 1 55 | } 56 | 57 | i = 0 58 | while (i < sfc.size.toInt) { 59 | if (!everSeen(i)) throw new Exception(s"Unseen index value: $i") 60 | i = i + 1 61 | } 62 | }) 63 | 64 | // if you get here, the test was successful 65 | true 66 | } 67 | 68 | def considerTestingOrdering(ms: Int*): Unit = { 69 | testOrdering(Try { 70 | val pp: Seq[OrdinalNumber] = ms.map(_.toOrdinalNumber) 71 | createCurve(pp:_*) 72 | }.toOption) 73 | } 74 | 75 | // 2-dimensions: allocate all valid index values exactly once 76 | def test2d(): Boolean = { 77 | for (m0 <- 1 to 5; m1 <- 1 to 5) 78 | considerTestingOrdering(m0, m1) 79 | true 80 | } 81 | 82 | // 3-dimensions: allocate all valid index values exactly once 83 | def test3d(): Boolean = { 84 | for (m0 <- 1 to 5; m1 <- 1 to 5; m2 <- 1 to 5) 85 | considerTestingOrdering(m0, m1, m2) 86 | true 87 | } 88 | 89 | // 4-dimensions: allocate all valid index values exactly once 90 | def test4d(): Boolean = { 91 | for (m0 <- 1 to 4; m1 <- 1 to 4; m2 <- 1 to 4; m3 <- 1 to 4) 92 | considerTestingOrdering(m0, m1, m2, m3) 93 | true 94 | } 95 | 96 | // actually conduct the tests; will throw exceptions if not everything works 97 | test2d() && test3d() && test4d() 98 | } 99 | 100 | def timeTestOrderings(): Boolean = { 101 | val (result, msDuration) = Timing.time(testOrderings) 102 | println(s"\n[CURVE VALIDATION TIMING] $curveName: ${(msDuration.toDouble / 1000.0).formatted("%1.3f")} sec\n") 103 | result 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/test/scala/org/eichelberger/sfc/ComposedCurveTest.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc 2 | 3 | import com.typesafe.scalalogging.slf4j.LazyLogging 4 | import org.eichelberger.sfc.SpaceFillingCurve._ 5 | import org.junit.runner.RunWith 6 | import org.specs2.mutable.Specification 7 | import org.specs2.runner.JUnitRunner 8 | import org.eichelberger.sfc.examples.Geohash 9 | 10 | @RunWith(classOf[JUnitRunner]) 11 | class ComposedCurveTest extends Specification with LazyLogging { 12 | sequential 13 | 14 | "net precisions" should { 15 | "work correctly on a full curve" >> { 16 | val curve = new ComposedCurve( 17 | RowMajorCurve(6, 12, 7), 18 | Seq( 19 | DefaultDimensions.createLongitude(6), 20 | new ComposedCurve( 21 | ZCurve(7, 5), 22 | Seq( 23 | new ComposedCurve( 24 | CompactHilbertCurve(3, 4), 25 | Seq( 26 | DefaultDimensions.createDateTime(3), 27 | DefaultDimensions.createDimension("altitude", 0.0, 50000.0, 4) 28 | ) 29 | ), 30 | DefaultDimensions.createDimension("IBU", 0.0, 100.0, 5) 31 | ) 32 | ), 33 | DefaultDimensions.createLatitude(7) 34 | ) 35 | ) 36 | 37 | curve.precisions must equalTo(OrdinalVector(6, 3, 4, 5, 7)) 38 | } 39 | 40 | /* 41 | */ 42 | "test GeoMesa plan" >> { 43 | //41 28, 45 28, 45 33, 41 33, 41 28 44 | for (bits <- 11 to 41) { 45 | val curve = new Geohash(bits) 46 | val cellQuery = Cell(Seq( 47 | DefaultDimensions.createDimension("x", 41.0, 45.0, 0), 48 | DefaultDimensions.createDimension("y", 28.0, 33.0, 0) 49 | )) 50 | val ranges = curve.getRangesCoveringCell(cellQuery) 51 | println(s"[GEOMESA-905] Number of index-ranges at $bits bits: ${ranges.length}") 52 | } 53 | 1 must equalTo(1) 54 | } 55 | 56 | "GeoMesa test" >> { 57 | val bits = 60 58 | 59 | val pXY = bits * 2 / 3 60 | val pY = pXY >> 1 61 | val pX = pXY - pY 62 | val pT = bits - pXY 63 | 64 | val dimX = DefaultDimensions.createLongitude(pX) 65 | val dimY = DefaultDimensions.createLatitude(pY) 66 | val dimT = DefaultDimensions.createIdentityDimension("t1", pT) 67 | 68 | val curve = new ComposedCurve( 69 | RowMajorCurve(pX, pY, pT), 70 | Seq(dimX, dimY, dimT) 71 | ) 72 | 73 | val (iX0: Long, iX1: Long) = (dimX.index(41.0), dimX.index(45.0)) 74 | val (iY0: Long, iY1: Long) = (dimY.index(28.0), dimY.index(33.0)) 75 | val (iT0: Long, iT1: Long) = (dimT.min, dimT.max) 76 | 77 | val query = Query(Seq( 78 | OrdinalRanges(OrdinalPair(iX0, iX1)), 79 | OrdinalRanges(OrdinalPair(iY0, iY1)), 80 | OrdinalRanges(OrdinalPair(iT0, iT1)) 81 | )) 82 | 83 | val qdimX = DefaultDimensions.createDimension[Double]("x", iX0, iX1, 0) 84 | val qdimY = DefaultDimensions.createDimension[Double]("y", iY0, iY1, 0) 85 | val qdimT = DefaultDimensions.createDimension[Long]("t", iT0, iT1, 0) 86 | 87 | val cellQuery: Cell = Cell(Seq(qdimX, qdimY, qdimT)) 88 | 89 | val ranges = curve.getRangesCoveringCell(cellQuery).toList 90 | 91 | println(s"[GEOMESA-905] bits $bits ($pX X, $pY Y, $pT T), query($iX0-$iX1, $iY0-$iY1, $iT0-$iT1), ranges ${ranges.length}") 92 | println(s" Range 0: ${ranges.head}") 93 | for (range <- ranges; idx <- range.min to Math.min(range.min + 8, range.max)) { 94 | val point = curve.inverseIndex(idx) 95 | println(s" index $idx, point $point") 96 | } 97 | 98 | 1 must equalTo(1) 99 | } 100 | 101 | "work correctly on a partial curve" >> { 102 | val curve = new ComposedCurve( 103 | RowMajorCurve(6, 12, 7), 104 | Seq( 105 | DefaultDimensions.createLongitude(6), 106 | ZCurve(7, 5), 107 | DefaultDimensions.createLatitude(7) 108 | ) 109 | ) 110 | 111 | curve.precisions must equalTo(OrdinalVector(6, 7, 5, 7)) 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/scala/org/eichelberger/sfc/utils/CompositionParser.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.utils 2 | 3 | import com.typesafe.scalalogging.Logging 4 | import org.eichelberger.sfc.SpaceFillingCurve.{Composable, OrdinalNumber, OrdinalVector, SpaceFillingCurve} 5 | import org.eichelberger.sfc._ 6 | import org.joda.time.DateTime 7 | import org.joda.time.format.DateTimeFormat 8 | 9 | import scala.util.parsing.combinator.RegexParsers 10 | 11 | /* 12 | * Examples: 13 | * - R(2,3) 14 | * - R(2,3,4) 15 | * - H(2, Z(7)) 16 | * - Z(R(4,H(2,2)),8) 17 | * 18 | * Examples: 19 | * R(t(15), Z(x(10), y(5))) 20 | * R(Z(x(10), y(5)), t(15)) 21 | */ 22 | object CompositionParser extends RegexParsers { 23 | val dtf = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") 24 | 25 | val LPAREN = """\(""".r 26 | val RPAREN = """\)""".r 27 | val COMMA = "," 28 | 29 | val R_CURVE_NAME = """R""".r 30 | val Z_CURVE_NAME = """Z""".r 31 | val H_CURVE_NAME = """H""".r 32 | 33 | def intLiteral: Parser[Int] = """\d+""".r ^^ { _.toInt } 34 | 35 | def doubleLiteral: Parser[Double] = """[+\-]?[0-9]*\.?[0-9]+""".r ^^ { _.toDouble } 36 | 37 | def dateLiteral: Parser[DateTime] = """\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d\.\d\d\dZ""".r ^^ { s => dtf.parseDateTime(s) } 38 | 39 | def longitude: Parser[Dimension[Double]] = """x""".r ~> LPAREN ~> intLiteral ~ opt(COMMA ~ doubleLiteral ~ COMMA ~ doubleLiteral) <~ RPAREN ^^ { 40 | case p ~ None => DefaultDimensions.createLongitude(p.toInt) 41 | case p ~ Some(_ ~ min ~ _ ~ max) => 42 | Dimension("x", min, isMinIncluded = true, max, isMaxIncluded = true, p.toLong) 43 | } 44 | 45 | def latitude: Parser[Dimension[Double]] = """y""".r ~> LPAREN ~> intLiteral ~ opt(COMMA ~ doubleLiteral ~ COMMA ~ doubleLiteral) <~ RPAREN ^^ { 46 | case p ~ None => DefaultDimensions.createLatitude(p.toInt) 47 | case p ~ Some(_ ~ min ~ _ ~ max) => 48 | Dimension("y", min, isMinIncluded = true, max, isMaxIncluded = true, p.toLong) 49 | } 50 | 51 | case class Bounds(min: String, max: String) 52 | 53 | //Dimension("t", MinDate, isMinIncluded = true, MaxDate, isMaxIncluded = true, 60L) 54 | def dateTime: Parser[Dimension[DateTime]] = """t""".r ~> LPAREN ~> intLiteral ~ opt(COMMA ~ dateLiteral ~ COMMA ~ dateLiteral) <~ RPAREN ^^ { 55 | case p ~ None => DefaultDimensions.createDateTime(p.toInt) 56 | case p ~ Some(_ ~ minDate ~ _ ~ maxDate) => 57 | Dimension("t", minDate, isMinIncluded = true, maxDate, isMaxIncluded = true, p.toLong) 58 | } 59 | 60 | def longDimName: Parser[String] = "u".r | "v".r | "w".r 61 | 62 | def longDim: Parser[Dimension[Long]] = longDimName ~ LPAREN ~ intLiteral ~ opt(COMMA ~ doubleLiteral ~ COMMA ~ doubleLiteral) <~ RPAREN ^^ { 63 | case name ~ _ ~ p ~ None => 64 | Dimension[Long](name, 0L, isMinIncluded=true, (1L << p.toLong) - 1L, isMaxIncluded=true, p.toLong) 65 | case name ~ _ ~ p ~ Some(_ ~ min ~ _ ~ max) => 66 | Dimension[Long](name, min.toLong, isMinIncluded=true, max.toLong, isMaxIncluded=true, p.toLong) 67 | } 68 | 69 | def dimension: Parser[Dimension[_]] = longitude | latitude | dateTime | longDim 70 | 71 | def curveName: Parser[String] = R_CURVE_NAME | Z_CURVE_NAME | H_CURVE_NAME ^^ { _.toString } 72 | 73 | def precision: Parser[Dimension[Long]] = intLiteral ^^ { p => DefaultDimensions.createIdentityDimension(p) } 74 | 75 | def childArg: Parser[Composable] = dimension | precision | curveParser 76 | 77 | def curveParser: Parser[ComposedCurve] = curveName ~ LPAREN ~ repsep(childArg, COMMA) ~ RPAREN ^^ { 78 | case name ~ _ ~ children ~ _ => 79 | val precisions = OrdinalVector(children.map { 80 | case c: SpaceFillingCurve => c.precisions.sum 81 | case d: Dimension[_] => d.precision 82 | }:_*) 83 | val curve = name match { 84 | case s: String if s.matches(R_CURVE_NAME.toString()) => new RowMajorCurve(precisions) 85 | case s: String if s.matches(Z_CURVE_NAME.toString()) => new ZCurve(precisions) 86 | case s: String if s.matches(H_CURVE_NAME.toString()) => new CompactHilbertCurve(precisions) 87 | } 88 | new ComposedCurve(curve, children) 89 | } 90 | 91 | def buildWholeNumberCurve(s: String): ComposedCurve = parse(curveParser, s).get 92 | } 93 | 94 | case class CompositionParserException(msg: String) extends Exception(msg) 95 | -------------------------------------------------------------------------------- /src/main/scala/org/eichelberger/sfc/study/planner/PlannerStudy.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.study.planner 2 | 3 | import org.eichelberger.sfc.SpaceFillingCurve 4 | import org.eichelberger.sfc.SpaceFillingCurve._ 5 | import org.eichelberger.sfc.planners.{SquareQuadTreePlanner, OffSquareQuadTreePlanner} 6 | import org.eichelberger.sfc.utils.Timing 7 | import org.eichelberger.sfc._ 8 | 9 | object PlannerStudy extends App { 10 | trait IndexCounter extends SpaceFillingCurve { 11 | var numIndexed: Long = 0L 12 | 13 | abstract override def index(point: OrdinalVector): OrdinalNumber = { 14 | numIndexed += 1 15 | super.index(point) 16 | } 17 | } 18 | 19 | class NewR(precisions: OrdinalVector) extends RowMajorCurve(precisions) with IndexCounter 20 | 21 | class NewZ(precisions: OrdinalVector) extends ZCurve(precisions) with IndexCounter 22 | 23 | class NewH(precisions: OrdinalVector) extends CompactHilbertCurve(precisions) with IndexCounter 24 | 25 | val precisions = OrdinalVector(20, 30) 26 | 27 | val pointQuery = Query(Seq( 28 | OrdinalRanges(OrdinalPair(3, 3)), 29 | OrdinalRanges(OrdinalPair(19710507, 19710507)) 30 | )) 31 | val smallQuery = Query(Seq( 32 | OrdinalRanges(OrdinalPair(1970, 2001)), 33 | OrdinalRanges(OrdinalPair(423, 828)) 34 | )) 35 | val mediumQuery = Query(Seq( 36 | OrdinalRanges(OrdinalPair(19704, 20018)), 37 | OrdinalRanges(OrdinalPair(4230, 8281)) 38 | )) 39 | val bigQuery = Query(Seq( 40 | OrdinalRanges(OrdinalPair(2, 28), OrdinalPair(101, 159)), 41 | OrdinalRanges(OrdinalPair(19710507, 20010423)) 42 | )) 43 | val query = mediumQuery 44 | 45 | val r = new NewR(precisions) 46 | val z = new NewZ(precisions) 47 | val h = new NewH(precisions) 48 | 49 | //@TODO restore higher counts! 50 | val numWarmup = 1 51 | val numEval = 5 52 | 53 | (1 to numWarmup) foreach { i => 54 | val (rR, msR) = Timing.time(() => r.getRangesCoveringQuery(query)) 55 | println(s"warmup R $i in $msR ms...") 56 | val (rZ, msZ) = Timing.time(() => z.getRangesCoveringQuery(query)) 57 | println(s"warmup Z $i in $msZ ms...") 58 | val (rH, msH) = Timing.time(() => h.getRangesCoveringQuery(query)) 59 | println(s"warmup H $i in $msH ms...") 60 | 61 | if (i == 1) { 62 | val rListR = rR.toList 63 | val rSize = rListR.size 64 | val rCells = rListR.map(_.size).sum 65 | println(s"\n Number of R ranges: $rSize\n cells: $rCells") 66 | 67 | val rListZ = rZ.toList 68 | val zSize = rListZ.size 69 | val zCells = rListZ.map(_.size).sum 70 | println(s"\n Number of Z ranges: $zSize\n cells: $zCells") 71 | 72 | val rListH = rH.toList 73 | val hSize = rListH.size 74 | val hCells = rListH.toList.map(_.size).sum 75 | println(s"\n Number of H ranges: $hSize\n cells: $hCells") 76 | 77 | println(s"\n Number of R evaluations: ${r.numIndexed}") 78 | println(s" Number of Z evaluations: ${z.numIndexed}") 79 | println(s" Number of H evaluations: ${h.numIndexed}") 80 | } 81 | } 82 | 83 | val (msSumR, msSumZ, msSumH) = (1 to numEval).foldLeft((0L, 0L, 0L))((acc, i) => acc match { 84 | case (soFarR, soFarZ, soFarH) => 85 | val (_, msR) = Timing.time(() => r.getRangesCoveringQuery(query)) 86 | println(s"evaluated R $i in $msR ms...") 87 | val (_, msZ) = Timing.time(() => z.getRangesCoveringQuery(query)) 88 | println(s"evaluated Z $i in $msZ ms...") 89 | val (rH1, msH1) = Timing.time(() => h.getRangesCoveringQuery(query)) 90 | println(s"evaluated H1 $i in $msH1 ms...") 91 | 92 | (soFarR + msR, soFarZ + msZ, soFarH + msH1) 93 | }) 94 | val msR = msSumR / numEval.toDouble / 1000.0 95 | val msZ = msSumZ / numEval.toDouble / 1000.0 96 | val msH = msSumH / numEval.toDouble / 1000.0 97 | 98 | println(s"[PLANNER TIMING STUDY]") 99 | println(s"\nNumber of warm-up trials: $numWarmup") 100 | println(s"Number of evaluation trials: $numEval") 101 | println(s"\nR mean planning time: ${msR.formatted("%1.4f")} seconds") 102 | println(s"Z mean planning time: ${msZ.formatted("%1.4f")} seconds") 103 | println(s"H1 mean planning time: ${msH.formatted("%1.4f")} seconds") 104 | } -------------------------------------------------------------------------------- /src/test/scala/org/eichelberger/sfc/DimensionTest.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc 2 | 3 | import com.typesafe.scalalogging.slf4j.LazyLogging 4 | import org.eichelberger.sfc.SpaceFillingCurve._ 5 | import org.eichelberger.sfc.examples.Geohash 6 | import org.joda.time.{DateTimeZone, DateTime} 7 | import org.junit.runner.RunWith 8 | import org.specs2.mutable.Specification 9 | import org.specs2.runner.JUnitRunner 10 | 11 | @RunWith(classOf[JUnitRunner]) 12 | class DimensionTest extends Specification with LazyLogging { 13 | "simple dimensions" should { 14 | "remain consistent round-trip" >> { 15 | val xDim = DefaultDimensions.createLongitude(18) 16 | val idx = xDim.index(-78.5238) 17 | val cell = xDim.inverseIndex(idx) 18 | cell.contains(-78.5238) must beTrue 19 | } 20 | } 21 | 22 | "identity dimensions" should { 23 | "work at both ends" >> { 24 | def last(p: OrdinalNumber): OrdinalNumber = (1L << p) - 1L 25 | 26 | val i1 = DefaultDimensions.createIdentityDimension(1) 27 | i1.index(0) must equalTo(0) 28 | i1.index(1) must equalTo(1) 29 | 30 | val i10 = DefaultDimensions.createIdentityDimension(10) 31 | i10.index(0) must equalTo(0) 32 | i10.index(1023) must equalTo(1023) 33 | 34 | val i20 = DefaultDimensions.createIdentityDimension(20) 35 | i20.index(0) must equalTo(0) 36 | i20.index(last(20)) must equalTo(last(20)) 37 | 38 | val i60 = DefaultDimensions.createIdentityDimension(60) 39 | i60.index(0) must equalTo(0) 40 | i60.index(last(60)) must equalTo(last(60)) 41 | } 42 | } 43 | 44 | "sub dimensions" should { 45 | "work for location" >> { 46 | val gh = new Geohash(35) 47 | val subDim = new SubDimension[Seq[Any]]("gh(3,2)", gh.pointToIndex, gh.M, 15, 10) 48 | 49 | val point = Seq(-78.5238, 38.0097) 50 | 51 | // full-precision index value 52 | val parentIdx = gh.pointToIndex(point) 53 | 54 | // sub-precision index portion 55 | val childIdx = subDim.index(point) 56 | 57 | val expectedChildIdx = (parentIdx >>> 10L) & 31L 58 | 59 | println(s"[SUB-DIM LOCATION] parent $parentIdx, child $childIdx") 60 | 61 | childIdx must equalTo(expectedChildIdx) 62 | } 63 | 64 | "work for time (above a week)" >> { 65 | val dim = DefaultDimensions.dimTime 66 | 67 | val d0 = new DateTime(0L, DateTimeZone.forID("UTC")) 68 | val d1 = d0.plusWeeks(1) 69 | val deltaIdxWeek = dim.index(d1) - dim.index(d0) 70 | val bitsInWeek = Math.ceil(Math.log(deltaIdxWeek.toDouble) / Math.log(2.0)).toLong 71 | 72 | val subDim = new SubDimension[DateTime]("YYYYww", dim.index, dim.precision, 0, dim.precision - bitsInWeek) 73 | 74 | val point = new DateTime(2015, 5, 14, 20, 29, 0, DateTimeZone.forID("UTC")) 75 | 76 | // full-precision index value 77 | val parentIdx = dim.index(point) 78 | 79 | // sub-precision index portion 80 | val childIdx = subDim.index(point) 81 | 82 | val minDT = dim.inverseIndex(childIdx << bitsInWeek).min 83 | val maxDT = dim.inverseIndex(((childIdx + 1L) << bitsInWeek) - 1L).max 84 | 85 | println(s"[SUB-DIM ABOVE-WEEK] minDT $minDT, point $point, maxDT $maxDT") 86 | 87 | point.isAfter(minDT) must beTrue 88 | point.isBefore(maxDT) must beTrue 89 | } 90 | 91 | "work for time (within a week)" >> { 92 | val dim = DefaultDimensions.dimTime 93 | 94 | val d0 = new DateTime(0L, DateTimeZone.forID("UTC")) 95 | val d1 = d0.plusWeeks(1) 96 | val deltaIdxWeek = dim.index(d1) - dim.index(d0) 97 | val bitsInWeek = Math.ceil(Math.log(deltaIdxWeek.toDouble) / Math.log(2.0)).toLong 98 | 99 | val subDim = new SubDimension[DateTime]("YYYYww", dim.index, dim.precision, dim.precision - bitsInWeek, bitsInWeek) 100 | 101 | val point = new DateTime(2015, 5, 14, 20, 29, 0, DateTimeZone.forID("UTC")) 102 | 103 | // full-precision index value 104 | val parentIdx = dim.index(point) 105 | 106 | // sub-precision index portion 107 | val childIdx = subDim.index(point) 108 | 109 | val returnIdx = ((parentIdx >>> bitsInWeek) << bitsInWeek) | childIdx 110 | returnIdx must equalTo(parentIdx) 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/test/scala/org/eichelberger/sfc/SpaceFillingCurveTest.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc 2 | 3 | import com.typesafe.scalalogging.slf4j.LazyLogging 4 | import org.eichelberger.sfc.CompactHilbertCurve.Mask 5 | import org.eichelberger.sfc.SpaceFillingCurve.{OrdinalVector, SpaceFillingCurve, _} 6 | import org.junit.runner.RunWith 7 | import org.specs2.mutable.Specification 8 | import org.specs2.runner.JUnitRunner 9 | 10 | @RunWith(classOf[JUnitRunner]) 11 | class SpaceFillingCurveTest extends Specification with LazyLogging { 12 | sequential 13 | 14 | "static functions in SFC" should { 15 | "iterate over counted combinations correctly" >> { 16 | val bounds = OrdinalVector(1, 2, 3) 17 | val itr = combinationsIterator(bounds) 18 | var n = 0 19 | while (itr.hasNext && n < 10) { 20 | println(s"[combinations count iterator] n $n: ${itr.next()}") 21 | n = n + 1 22 | } 23 | n must equalTo(6) 24 | } 25 | 26 | "iterate over bounded combinations correctly" >> { 27 | val bounds = Seq(OrdinalPair(0, 0), OrdinalPair(1, 2), OrdinalPair(3, 5)) 28 | val itr = combinationsIterator(bounds) 29 | var n = 0 30 | while (itr.hasNext && n < 10) { 31 | println(s"[combinations bounded iterator] n $n: ${itr.next()}") 32 | n = n + 1 33 | } 34 | n must equalTo(6) 35 | } 36 | 37 | "iterate over range-collection combinations correctly" >> { 38 | val ranges = Seq( 39 | OrdinalRanges(OrdinalPair(1, 1)), 40 | OrdinalRanges(OrdinalPair(2, 3)), 41 | OrdinalRanges(OrdinalPair(4, 5), OrdinalPair(7, 7)) 42 | ) 43 | val itr = rangesCombinationsIterator(ranges) 44 | var n = 0 45 | while (itr.hasNext && n < 10) { 46 | println(s"[range-combinations iterator] n $n: ${itr.next()}") 47 | n = n + 1 48 | } 49 | n must equalTo(6) 50 | } 51 | 52 | "consolidated ranges iteratively" >> { 53 | val ranges = Seq( 54 | OrdinalPair(3, 17), 55 | OrdinalPair(19, 20), 56 | OrdinalPair(22, 23), 57 | OrdinalPair(24, 25), 58 | OrdinalPair(31, 39), 59 | OrdinalPair(40, 49), 60 | OrdinalPair(60, 60), 61 | OrdinalPair(61, 61), 62 | OrdinalPair(62, 62) 63 | ) 64 | 65 | val consolidated = consolidatedRangeIterator(ranges.iterator).toList 66 | for (i <- 0 until consolidated.size) { 67 | println(s"[consolidated range iteration] i $i: ${consolidated(i)}") 68 | } 69 | consolidated.size must equalTo(5) 70 | } 71 | } 72 | 73 | "bit coverages" >> { 74 | def testCoverages(range: OrdinalPair, precision: OrdinalNumber, expected: Seq[OrdinalPair]): Boolean = { 75 | val result = bitCoverages(range, precision) 76 | println(s"[bit coverage] range $range, precision $precision...\n${result.mkString(" ","\n ","")}") 77 | result must equalTo(expected) 78 | } 79 | 80 | testCoverages(OrdinalPair(6, 12), 4, Seq(OrdinalPair(6, 2), OrdinalPair(8, 4), OrdinalPair(12, 1))) must beTrue 81 | testCoverages(OrdinalPair(1, 5), 4, Seq(OrdinalPair(1, 1), OrdinalPair(2, 2), OrdinalPair(4, 2))) must beTrue 82 | testCoverages(OrdinalPair(0, 65535), 16, Seq(OrdinalPair(0, 65536))) must beTrue 83 | } 84 | 85 | "cross-curve comparisons" should { 86 | "be possible" >> { 87 | val n = 2 88 | val z = new ZCurve(OrdinalVector(n, n)) 89 | val c = CompactHilbertCurve(OrdinalVector(n, n)) 90 | val r = RowMajorCurve(OrdinalVector(n, n)) 91 | 92 | def d0(sfc: SpaceFillingCurve, name: String): Double = { 93 | val data = for (i <- 0 to sfc.size.toInt - 2) yield { 94 | val j = i + 1 95 | val pi = sfc.inverseIndex(i) 96 | val pix = pi(0) 97 | val piy = pi(1) 98 | val pj = sfc.inverseIndex(j) 99 | val pjx = pj(0) 100 | val pjy = pj(1) 101 | val ds = Math.hypot(pix - pjx, piy - pjy) 102 | val di = Math.abs(i - j) 103 | ds / di 104 | } 105 | val mean = data.sum / data.size.toDouble 106 | println(s"[mean dSpace/dIndex] $name: ${mean.formatted("%1.3f")}") 107 | mean 108 | } 109 | 110 | val idr = d0(r, "R") 111 | val idz = d0(z, "Z") 112 | val idc = d0(c, "C") 113 | 114 | idc must beLessThan(idz) 115 | idz must beLessThan(idr) 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/scala/org/eichelberger/sfc/utils/LocalityEstimator.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.utils 2 | 3 | import org.eichelberger.sfc.SpaceFillingCurve._ 4 | 5 | case class LocalityResult( 6 | locality: Double, 7 | normalizedLocality: Double, 8 | localityInverse: Double, 9 | normalizedLocalityInverse: Double, 10 | sampleSize: Int, 11 | coverage: Double) 12 | 13 | object LocalityEstimator { 14 | val maxEvaluations = 1L << 14L 15 | 16 | case class SampleItem(dUser: Double, dIndex: Double) 17 | 18 | type Sample = Seq[SampleItem] 19 | } 20 | 21 | case class LocalityEstimator(curve: SpaceFillingCurve) { 22 | import LocalityEstimator._ 23 | 24 | lazy val deltas = (0 until curve.n).toList 25 | 26 | lazy val fullSampleSize = curve.n * curve.cardinalities.product - 27 | curve.cardinalities.sum 28 | 29 | lazy val maxUserDistance = Math.sqrt(curve.cardinalities.map(c => c * c).sum) 30 | 31 | // items are known to be 1 unit apart in user space 32 | def sampleItem(a: OrdinalVector, b: OrdinalVector): SampleItem = { 33 | val idxA: OrdinalNumber = curve.index(a) 34 | val idxB: OrdinalNumber = curve.index(b) 35 | SampleItem(1.0, Math.abs(idxA - idxB)) 36 | } 37 | 38 | // items are known to be 1 unit apart in index space 39 | def sampleItem(a: OrdinalNumber, b: OrdinalNumber): SampleItem = { 40 | val ptA: OrdinalVector = curve.inverseIndex(a) 41 | val ptB: OrdinalVector = curve.inverseIndex(b) 42 | val distUser = Math.sqrt(ptA.zipWith(ptB).map { case (coordA, coordB) => 43 | (coordA - coordB) * (coordA - coordB) }.sum) 44 | SampleItem(distUser, 1.0) 45 | } 46 | 47 | def randomPoint: OrdinalVector = 48 | OrdinalVector(deltas.map(i => 49 | Math.floor(Math.random() * (curve.cardinalities(i).toDouble - 1.0)).toLong 50 | ):_*) 51 | 52 | def randomPointAdjacent(a: OrdinalVector): OrdinalVector = { 53 | while (true) { 54 | val dim = Math.floor(Math.random() * (curve.n.toDouble - 1.0)).toInt 55 | val dir = if (Math.random() < 0.5) 1L else -1L 56 | val newCoord = a(dim) + dir 57 | if (newCoord >= 0 && newCoord < curve.cardinalities(dim)) 58 | return a.set(dim, newCoord) 59 | } 60 | 61 | // dummy; this code is never reached 62 | OrdinalVector() 63 | } 64 | 65 | def randomSample: Sample = { 66 | var sample: Sample = Seq[SampleItem]() 67 | 68 | var i = 0 69 | while (i < maxEvaluations) { 70 | val a = randomPoint 71 | val b = randomPointAdjacent(a) 72 | sample = sample :+ sampleItem(a, b) 73 | i = i + 1 74 | } 75 | 76 | sample 77 | } 78 | 79 | def randomSampleInverse: Sample = { 80 | val sample = collection.mutable.ListBuffer[SampleItem]() 81 | 82 | var i = 0 83 | while (i < maxEvaluations) { 84 | val idx = Math.floor(Math.random() * (curve.n.toDouble - 2.0)).toLong 85 | sample += sampleItem(idx, idx + 1L) 86 | i = i + 1 87 | } 88 | 89 | sample 90 | } 91 | 92 | def fullSample: Sample = { 93 | // iterate over all cells in the index space 94 | val cellIdxItr = combinationsIterator(OrdinalVector(curve.cardinalities:_*)) 95 | 96 | (for ( 97 | cellIdx <- cellIdxItr; 98 | deltaDim <- deltas if cellIdx(deltaDim) + 1L < curve.cardinalities(deltaDim); 99 | adjCellIdx = cellIdx.set(deltaDim, cellIdx(deltaDim) + 1L) 100 | ) yield sampleItem(cellIdx, adjCellIdx)).toSeq 101 | } 102 | 103 | def fullSampleInverse: Sample = { 104 | val sample = collection.mutable.ListBuffer[SampleItem]() 105 | 106 | var idx: OrdinalNumber = 0 107 | while ((idx + 1L) < curve.size) { 108 | sample += sampleItem(idx, idx+1) 109 | idx = idx + 1L 110 | } 111 | 112 | sample 113 | } 114 | 115 | def locality: LocalityResult = { 116 | // pull a sample, constrained by a maximum size 117 | val sample: Sample = 118 | if (fullSampleSize > maxEvaluations) randomSample 119 | else fullSample 120 | 121 | val absLocality = sample.map(_.dIndex).sum / sample.size.toDouble 122 | val relLocality = absLocality / curve.size.toDouble 123 | 124 | // pull an inverse sample, constrained by a maximum size 125 | val sampleInverse: Sample = 126 | if (fullSampleSize > maxEvaluations) randomSampleInverse 127 | else fullSampleInverse 128 | 129 | val absLocalityInverse = sampleInverse.map(_.dUser).sum / sample.size.toDouble 130 | val relLocalityInverse = absLocalityInverse / maxUserDistance 131 | 132 | LocalityResult( 133 | absLocality, 134 | relLocality, 135 | absLocalityInverse, 136 | relLocalityInverse, 137 | sample.size, 138 | sample.size.toDouble / fullSampleSize 139 | ) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/main/scala/org/eichelberger/sfc/study/OutputFormats.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.study 2 | 3 | import java.io.{FileWriter, BufferedWriter, PrintWriter} 4 | 5 | case class ColumnSpec(name: String, isQuoted: Boolean) 6 | 7 | case class OutputMetadata(columnSpecs: Seq[ColumnSpec]) 8 | 9 | trait OutputDestination { 10 | def print(s: String): Unit 11 | def println(s: String): Unit 12 | def println(record: Seq[Any]): Unit 13 | def close(): Unit 14 | } 15 | 16 | trait FileOutputDestination extends OutputDestination { 17 | def fileName: String 18 | 19 | // create a PrintWriter that will automatically flush 20 | lazy val pw = new PrintWriter(new BufferedWriter(new FileWriter(fileName)), true) 21 | 22 | def print(s: String): Unit = pw.print(s) 23 | 24 | def println(s: String): Unit = pw.println(s) 25 | 26 | def close(): Unit = { 27 | pw.flush() 28 | pw.close() 29 | } 30 | } 31 | 32 | trait ScreenOutputDestination extends OutputDestination { 33 | lazy val pw = System.out 34 | 35 | def print(s: String): Unit = pw.print(s) 36 | 37 | def println(s: String): Unit = pw.println(s) 38 | 39 | def close(): Unit = { 40 | pw.flush() 41 | pw.close() 42 | } 43 | } 44 | 45 | trait OutputFormat { 46 | this: OutputDestination => 47 | 48 | def prepareToClose(): Unit = {} 49 | } 50 | 51 | class DelimitedTextFile(fileName: String, metadata: OutputMetadata, writeHeader: Boolean, fieldSeparator: String, encloser: String) extends OutputFormat { 52 | this: OutputDestination => 53 | 54 | // number of fields 55 | val n = metadata.columnSpecs.size 56 | 57 | if (writeHeader) { 58 | // write header line 59 | println(metadata.columnSpecs.map(spec => encloser + spec.name + encloser).mkString(fieldSeparator)) 60 | } 61 | 62 | def println(record: Seq[Any]): Unit = { 63 | require(record.size <= n, s"Too many fields to write out") 64 | val values: Seq[String] = record.padTo(n, null).zip(metadata.columnSpecs).map { 65 | case (field, spec) => 66 | val fieldStr = 67 | if (field == null) "" 68 | else field.toString 69 | val outStr = 70 | if (spec.isQuoted) encloser + fieldStr + encloser 71 | else fieldStr 72 | outStr 73 | } 74 | println(values.mkString(fieldSeparator)) 75 | } 76 | } 77 | 78 | abstract class CSV(fileName: String, metadata: OutputMetadata, writeHeader: Boolean) 79 | extends DelimitedTextFile(fileName, metadata, writeHeader, ",", "\"") { 80 | this: OutputDestination => 81 | 82 | } 83 | 84 | abstract class TSV(fileName: String, metadata: OutputMetadata, writeHeader: Boolean) 85 | extends DelimitedTextFile(fileName, metadata, writeHeader, "\t", "") { 86 | this: OutputDestination => 87 | 88 | } 89 | 90 | abstract class JSON(fileName: String, metadata: OutputMetadata) { 91 | this: OutputDestination => 92 | 93 | print("var data = [") 94 | 95 | val quote = "\"" 96 | 97 | var needsComma = false 98 | 99 | def println(values: Seq[Any]): Unit = { 100 | if (needsComma) print(",") 101 | println("\n\t{") 102 | values.zip(metadata.columnSpecs).zipWithIndex.foreach { 103 | case ((field, spec), i) => 104 | val fieldStr = 105 | if (field == null) "" 106 | else field.toString 107 | val outStr = 108 | if (spec.isQuoted) quote + fieldStr + quote 109 | else fieldStr 110 | if (i > 0) println(",") 111 | print("\t\t" + quote + spec.name + quote + ": " + outStr) 112 | } 113 | print("\n\t}") 114 | 115 | needsComma = true 116 | } 117 | 118 | def prepareToClose(): Unit = { 119 | println("\n];") 120 | close() 121 | } 122 | } 123 | 124 | class MultipleOutput(children: Seq[OutputDestination]) extends OutputDestination { 125 | def print(s: String): Unit = children.foreach(_.print(s)) 126 | def println(s: String): Unit = children.foreach(_.println(s)) 127 | def println(values: Seq[Any]): Unit = children.foreach(_.println(values)) 128 | override def close(): Unit = children.foreach(_.close()) 129 | } 130 | 131 | class MirroredCSV(val fn: String, metadata: OutputMetadata, writeHeader: Boolean) 132 | extends MultipleOutput(Seq( 133 | new CSV(fn, metadata, writeHeader) with FileOutputDestination { def fileName = fn }, 134 | new CSV(fn, metadata, writeHeader) with ScreenOutputDestination 135 | )) 136 | 137 | class MirroredTSV(val fn: String, metadata: OutputMetadata, writeHeader: Boolean) 138 | extends MultipleOutput(Seq( 139 | new TSV(fn, metadata, writeHeader) with FileOutputDestination { def fileName = fn }, 140 | new TSV(fn, metadata, writeHeader) with ScreenOutputDestination 141 | )) 142 | 143 | class MirroredJSON(val fn: String, metadata: OutputMetadata, writeHeader: Boolean) 144 | extends MultipleOutput(Seq( 145 | new JSON(fn, metadata) with FileOutputDestination { def fileName = fn }, 146 | new JSON(fn, metadata) with ScreenOutputDestination 147 | )) 148 | -------------------------------------------------------------------------------- /src/main/scala/org/eichelberger/sfc/planners/SquareQuadTreePlanner.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.planners 2 | 3 | import org.eichelberger.sfc.SpaceFillingCurve._ 4 | 5 | import scala.collection.mutable 6 | 7 | // this works well for planning Hilbert curves on square 8 | // spaces 9 | 10 | trait SquareQuadTreePlanner { 11 | this: SpaceFillingCurve => 12 | 13 | def getBitsRemainingPerDim(prefix: OrdinalNumber, precision: Int): Seq[OrdinalNumber] = { 14 | var full = precisions 15 | var bitPos = 0 16 | var i = 0 17 | while (i < precision) { 18 | // decrement this counter 19 | full = full.set(bitPos, full(bitPos) - 1) 20 | 21 | i = i + 1 22 | 23 | // update bitPos 24 | bitPos = (bitPos + 1) % n 25 | while (i < precision && full(bitPos) < 1) { 26 | bitPos = (bitPos + 1) % n 27 | } 28 | } 29 | 30 | full.toSeq 31 | } 32 | 33 | def getRange(prefix: OrdinalNumber, precision: Int): OrdinalPair = 34 | OrdinalPair( 35 | prefix << (M - precision), 36 | ((prefix + 1L) << (M - precision)) - 1L 37 | ) 38 | 39 | def getRanges(prefix: OrdinalNumber, precision: Int): Seq[OrdinalPair] = 40 | Seq(getRange(prefix, precision)) 41 | 42 | def getExtents(prefix: OrdinalNumber, precision: Int, cacheOpt: Option[mutable.HashMap[OrdinalNumber, OrdinalVector]] = None): Seq[OrdinalPair] = { 43 | // quad-trees use interval-halving per bit per dimension 44 | 45 | // initialize the full range 46 | val dimRanges = new collection.mutable.ArrayBuffer[OrdinalPair]() 47 | cardinalities.toSeq.foreach { c => dimRanges.append(OrdinalPair(0, c - 1L)) } 48 | 49 | // divide the ranges according to the bits already in the prefix 50 | var numBitsRemaining = precisions 51 | var bitPos = 0 52 | var i = 0 53 | while (i < precision) { 54 | // adjust this range 55 | val oldRange = dimRanges(bitPos) 56 | val bit = (prefix >> (precision - i - 1)) & 1L 57 | if (bit == 1L) { 58 | // upper half of the remaining range 59 | dimRanges(bitPos) = OrdinalPair( 60 | 1L + ((oldRange.max + oldRange.min) >> 1L), 61 | oldRange.max 62 | ) 63 | } else { 64 | // lower half of the remaining range 65 | dimRanges(bitPos) = OrdinalPair( 66 | oldRange.min, 67 | (oldRange.max + oldRange.min) >> 1L 68 | ) 69 | } 70 | 71 | // decrement this counter 72 | numBitsRemaining = numBitsRemaining.set(bitPos, numBitsRemaining(bitPos) - 1) 73 | 74 | i = i + 1 75 | 76 | // update bitPos 77 | bitPos = (bitPos + 1) % n 78 | while (i < precision && numBitsRemaining(bitPos) < 1) { 79 | bitPos = (bitPos + 1) % n 80 | } 81 | } 82 | 83 | dimRanges 84 | } 85 | 86 | def isPairSupersetOfPair(qPair: OrdinalPair, iPair: OrdinalPair): Boolean = 87 | iPair.min >= qPair.min && iPair.max <= qPair.max 88 | 89 | def pairsAreDisjoint(qPair: OrdinalPair, iPair: OrdinalPair): Boolean = 90 | qPair.max < iPair.min || qPair.min > iPair.max 91 | 92 | def queryCovers(query: Query, extents: Seq[OrdinalPair]): Boolean = { 93 | query.toSeq.zip(extents).foreach { 94 | case (dimRanges, ordPair) => 95 | if (!dimRanges.toSeq.exists(qPair => isPairSupersetOfPair(qPair, ordPair))) 96 | return false 97 | } 98 | true 99 | } 100 | 101 | def queryIsDisjoint(query: Query, extents: Seq[OrdinalPair]): Boolean = { 102 | query.toSeq.zip(extents).foreach { 103 | case (dimRanges, ordPair) => 104 | if (dimRanges.toSeq.forall(qPair => pairsAreDisjoint(qPair, ordPair))) 105 | return true 106 | } 107 | false 108 | } 109 | 110 | // this method should work for the Z-curve, but is too generic to 111 | // be very efficient 112 | def getRangesCoveringQueryOnSquare(query: Query): Iterator[OrdinalPair] = { 113 | // quick check for "everything" 114 | if (isEverything(query)) 115 | return Seq(OrdinalPair(0, size - 1L)).iterator 116 | 117 | // "prefix" is right-justified 118 | 119 | val cache = collection.mutable.HashMap[OrdinalNumber, OrdinalVector]() 120 | 121 | def recursiveSearch(prefix: OrdinalNumber, precision: Int): Seq[OrdinalPair] = { 122 | val extents = getExtents(prefix, precision, Option(cache)) 123 | 124 | // easy case: this prefix does not overlap at all with the query 125 | if (queryIsDisjoint(query, extents)) 126 | return Seq() 127 | 128 | // easy case: this prefix is entirely contained within the query 129 | if (queryCovers(query, extents)) 130 | return getRanges(prefix, precision) 131 | 132 | // the prefix does overlap, but only partially, with the query 133 | 134 | // check for precision exhaustion 135 | if (precision == M) 136 | return getRanges(prefix, precision) 137 | require(precision < M, s"Precision overflow: $precision >= $M") 138 | 139 | // recurse 140 | Seq( 141 | recursiveSearch(prefix << 1L, precision + 1), 142 | recursiveSearch((prefix << 1L) + 1L, precision + 1) 143 | ).flatten 144 | } 145 | 146 | val ranges = recursiveSearch(0L, 0).iterator 147 | 148 | consolidatedRangeIterator(ranges) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/main/scala/org/eichelberger/sfc/planners/ZCurvePlanner.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.planners 2 | 3 | import org.eichelberger.sfc.SpaceFillingCurve._ 4 | 5 | import scala.collection.mutable 6 | 7 | // @TODO this contains too many inverse-index operations for 8 | // a regular Z-order curve 9 | 10 | trait ZCurvePlanner { 11 | this: SpaceFillingCurve => 12 | 13 | def dimExtentLT(a: (Int, OrdinalPair), b: (Int, OrdinalPair)): Boolean = 14 | a._1 < b._1 15 | 16 | def getRange(prefix: OrdinalNumber, precision: Int): OrdinalPair = 17 | OrdinalPair( 18 | prefix << (M - precision), 19 | ((prefix + 1L) << (M - precision)) - 1L 20 | ) 21 | 22 | def getBitsRemainingPerDim(prefix: OrdinalNumber, precision: Int): Seq[OrdinalNumber] = { 23 | var full = precisions 24 | var bitPos = 0 25 | var i = 0 26 | while (i < precision) { 27 | // decrement this counter 28 | full = full.set(bitPos, full(bitPos) - 1) 29 | 30 | i = i + 1 31 | 32 | // update bitPos 33 | bitPos = (bitPos + 1) % n 34 | while (i < precision && full(bitPos) < 1) { 35 | bitPos = (bitPos + 1) % n 36 | } 37 | } 38 | 39 | full.toSeq 40 | } 41 | 42 | val dims = (0 until n).toList 43 | 44 | def getExtents(prefix: OrdinalNumber, precision: Int, cacheOpt: Option[mutable.HashMap[OrdinalNumber, OrdinalVector]] = None): Seq[OrdinalPair] = { 45 | val remaining = OrdinalVector(getBitsRemainingPerDim(prefix, precision):_*) 46 | 47 | // the prefix, when padded to the full curve precision, represents 48 | // one corner of the cube 49 | 50 | // the other corners are reachable by adding bits to the prefix 51 | 52 | val toggles = combinationsIterator(OrdinalVector(List.fill(n)(2L):_*)).toList 53 | val cornerIdxs: Seq[OrdinalNumber] = toggles.map( 54 | toggleVec => { 55 | // build up the index value for this corner 56 | toggleVec.zipWith(remaining).foldLeft(prefix)((idxSoFar, tuple) => tuple match { 57 | case (toggle, remainder) if toggle > 0 => 58 | ((idxSoFar + 1L) << remainder) - 1L 59 | case (toggle, remainder) => 60 | idxSoFar << remainder 61 | }) 62 | }) 63 | 64 | // compute the user-space coordinates for each of the corner index values 65 | val points = cornerIdxs.map(idx => { 66 | cacheOpt.map { cache => 67 | cache.getOrElse(idx, { 68 | val point = inverseIndex(idx) 69 | cache.put(idx, point) 70 | point 71 | }) 72 | }.getOrElse(inverseIndex(idx)) 73 | }) 74 | 75 | // extract coordinates per dimension 76 | val coordsPerDim = points.flatMap(point => point.toSeq.zipWithIndex).groupBy(_._2) 77 | 78 | // extract extrema per dimension, and report them in order 79 | dims.map(dim => { 80 | val coords = coordsPerDim(dim).map(_._1) 81 | OrdinalPair(coords.min, coords.max) 82 | }) 83 | } 84 | 85 | def isPairSupersetOfPair(qPair: OrdinalPair, iPair: OrdinalPair): Boolean = 86 | iPair.min >= qPair.min && iPair.max <= qPair.max 87 | 88 | def pairsAreDisjoint(qPair: OrdinalPair, iPair: OrdinalPair): Boolean = 89 | qPair.max < iPair.min || qPair.min > iPair.max 90 | 91 | def queryCovers(query: Query, extents: Seq[OrdinalPair]): Boolean = { 92 | query.toSeq.zip(extents).foreach { 93 | case (dimRanges, ordPair) => 94 | if (!dimRanges.toSeq.exists(qPair => isPairSupersetOfPair(qPair, ordPair))) 95 | return false 96 | } 97 | true 98 | } 99 | 100 | def queryIsDisjoint(query: Query, extents: Seq[OrdinalPair]): Boolean = { 101 | query.toSeq.zip(extents).foreach { 102 | case (dimRanges, ordPair) => 103 | if (dimRanges.toSeq.forall(qPair => pairsAreDisjoint(qPair, ordPair))) 104 | return true 105 | } 106 | false 107 | } 108 | 109 | // this method should work for the Z-curve, but is too generic to 110 | // be very efficient 111 | override def getRangesCoveringQuery(query: Query): Iterator[OrdinalPair] = { 112 | // quick check for "everything" 113 | if (isEverything(query)) 114 | return Seq(OrdinalPair(0, size - 1L)).iterator 115 | 116 | // "prefix" is right-justified 117 | 118 | val cache = collection.mutable.HashMap[OrdinalNumber, OrdinalVector]() 119 | 120 | def recursiveSearch(prefix: OrdinalNumber, precision: Int): Seq[OrdinalPair] = { 121 | val extents = getExtents(prefix, precision, Option(cache)) 122 | 123 | // easy case: this prefix does not overlap at all with the query 124 | if (queryIsDisjoint(query, extents)) 125 | return Seq() 126 | 127 | // easy case: this prefix is entirely contained within the query 128 | if (queryCovers(query, extents)) 129 | return Seq(getRange(prefix, precision)) 130 | 131 | // the prefix does overlap, but only partially, with the query 132 | 133 | // check for precision exhaustion 134 | if (precision == M) 135 | return Seq(getRange(prefix, precision)) 136 | require(precision < M, s"Precision overflow: $precision >= $M") 137 | 138 | // recurse 139 | val zeroBit = recursiveSearch(prefix << 1L, precision + 1) 140 | val oneBit = recursiveSearch((prefix << 1L) + 1L, precision + 1) 141 | Seq(zeroBit, oneBit).flatten 142 | } 143 | 144 | val ranges = recursiveSearch(0L, 0).iterator 145 | 146 | consolidatedRangeIterator(ranges) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/main/scala/org/eichelberger/sfc/study/composition/CompositionSampleData.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.study.composition 2 | 3 | import org.eichelberger.sfc.SpaceFillingCurve._ 4 | import org.eichelberger.sfc.{Dimension, DefaultDimensions} 5 | import org.joda.time.{DateTimeZone, DateTime} 6 | 7 | import scala.util.Random 8 | 9 | case class XYZTPoint(x: Double, y: Double, z: Double, t: DateTime) 10 | 11 | case class MinMax[T](min: T, max: T) 12 | 13 | object CompositionSampleData { 14 | object TestLevels extends Enumeration { 15 | type TestLevel = Value 16 | val Debug, Small, Medium, Large = Value 17 | } 18 | import TestLevels._ 19 | 20 | val bboxCville = (-78.5238, 38.0097, -78.4464, 38.0705) 21 | 22 | // ensure that all of this discretizing-dimensions share the SAME precision 23 | val dimLong = DefaultDimensions.createLongitude(10L) 24 | val dimLat = DefaultDimensions.createLatitude(10L) 25 | val dimDate = DefaultDimensions.createNearDateTime(10L) 26 | val dimAlt = DefaultDimensions.createDimension("z", 0.0, 50000.0, 10L) 27 | 28 | def discretize[T](value: T, dim: Dimension[T]): (T, T) = { 29 | val idx = dim.index(value) 30 | val cell = dim.inverseIndex(idx) 31 | (cell.min, cell.max) 32 | } 33 | 34 | def getAllMissingVariants(point: XYZTPoint, cellX: Dimension[Double], cellY: Dimension[Double], cellZ: Dimension[Double], cellT: Dimension[DateTime]): Seq[(XYZTPoint, Cell, String)] = { 35 | combinationsIterator(OrdinalVector(2, 2, 2, 2)).map(combination => { 36 | ( 37 | point, 38 | Cell(Seq( 39 | if (combination(0) == 0L) dimLong else cellX, 40 | if (combination(1) == 0L) dimLat else cellY, 41 | if (combination(2) == 0L) dimAlt else cellZ, 42 | if (combination(3) == 0L) dimDate else cellT 43 | )), 44 | (if (combination(0) == 0L) "-" else "X") + 45 | (if (combination(1) == 0L) "-" else "Y") + 46 | (if (combination(2) == 0L) "-" else "Z") + 47 | (if (combination(3) == 0L) "-" else "T") 48 | ) 49 | }).toSeq 50 | } 51 | 52 | def getOneMissingVariants(point: XYZTPoint, cellX: Dimension[Double], cellY: Dimension[Double], cellZ: Dimension[Double], cellT: Dimension[DateTime]): Seq[(XYZTPoint, Cell, String)] = { 53 | Seq( 54 | (point, Cell(Seq(cellX, cellY, cellZ, cellT)), "XYZT"), 55 | (point, Cell(Seq(cellX, cellY, cellZ, dimDate)), "XYZ-"), 56 | (point, Cell(Seq(cellX, cellY, dimAlt, cellT)), "XY-T"), 57 | (point, Cell(Seq(cellX, dimLat, cellZ, cellT)), "X-ZT"), 58 | (point, Cell(Seq(dimLong, cellY, cellZ, cellT)), "-YZT"), 59 | (point, Cell(Seq(dimLong, dimLat, dimAlt, dimDate)), "----") 60 | ) 61 | } 62 | 63 | def getAllOrNothingVariants(point: XYZTPoint, cellX: Dimension[Double], cellY: Dimension[Double], cellZ: Dimension[Double], cellT: Dimension[DateTime]): Seq[(XYZTPoint, Cell, String)] = { 64 | Seq( 65 | (point, Cell(Seq(cellX, cellY, cellZ, cellT)), "XYZT"), 66 | (point, Cell(Seq(dimLong, dimLat, dimAlt, dimDate)), "----") 67 | ) 68 | } 69 | 70 | def getN(testLevel: TestLevels.Value) = testLevel match { 71 | case Debug => 1 72 | case Small => 10 73 | case Medium => 100 74 | case Large => 1000 75 | } 76 | 77 | def getPointQueryPairs(testLevel: TestLevels.Value): Seq[(XYZTPoint, Cell, String)] = { 78 | val prng = new Random(5771L) 79 | val MinDate = new DateTime(2010, 1, 1, 0, 0, 0, DateTimeZone.forID("UTC")) 80 | val MaxDate = new DateTime(2014, 12, 31, 23, 59, 59, DateTimeZone.forID("UTC")) 81 | (1 to getN(testLevel)).flatMap(i => { 82 | // construct the point 83 | val x = Math.min(180.0, Math.max(-180.0, -180.0 + 360.0 * prng.nextDouble())) 84 | val y = Math.min(90.0, Math.max(-90.0, -90.0 + 180.0 * prng.nextDouble())) 85 | val z = Math.min(50000.0, Math.max(0.0, 50000.0 * prng.nextDouble())) 86 | val ms = Math.min(MaxDate.getMillis, Math.max(MinDate.getMillis, MinDate.getMillis + (MaxDate.getMillis - MinDate.getMillis) * prng.nextDouble())).toLong 87 | val t = new DateTime(ms, DateTimeZone.forID("UTC")) 88 | val point = XYZTPoint(x, y, z, t) 89 | // construct a query that contains this point 90 | // (discretized to equivalent precision) 91 | val (x0: Double, x1: Double) = discretize(x, dimLong) 92 | val (y0: Double, y1: Double) = discretize(y, dimLat) 93 | val (z0: Double, z1: Double) = discretize(z, dimAlt) 94 | val (t0: DateTime, t1: DateTime) = discretize(t, dimDate) 95 | val cellX = DefaultDimensions.createDimension("x", x0, x1, 0L) 96 | val cellY = DefaultDimensions.createDimension("y", y0, y1, 0L) 97 | val cellZ = DefaultDimensions.createDimension("z", z0, z1, 0L) 98 | val cellT = DefaultDimensions.createNearDateTime(t0, t1, 0L) 99 | testLevel match { 100 | case Medium | Small => 101 | // only report the combinations in which exactly one dimension is missing 102 | getOneMissingVariants(point, cellX, cellY, cellZ, cellT) 103 | case Large => 104 | // return the combinations with (and without) queries per dimension 105 | val allEntries = getAllMissingVariants(point, cellX, cellY, cellZ, cellT) 106 | if (i == 1) allEntries else allEntries.tail 107 | case _ => 108 | // all or nothing 109 | getAllOrNothingVariants(point, cellX, cellY, cellZ, cellT) 110 | } 111 | }) 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/test/scala/org/eichelberger/sfc/examples/composition/contrast/StackingVariantsTest.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.examples.composition.contrast 2 | 3 | import com.typesafe.scalalogging.slf4j.LazyLogging 4 | import org.eichelberger.sfc.ComposedCurve 5 | import org.eichelberger.sfc.SpaceFillingCurve._ 6 | import org.eichelberger.sfc.study.composition.CompositionSampleData._ 7 | import org.eichelberger.sfc.study.composition.XYZTPoint 8 | import org.eichelberger.sfc.utils.Timing._ 9 | import org.joda.time.DateTime 10 | import org.junit.runner.RunWith 11 | import org.specs2.mutable.Specification 12 | import org.specs2.runner.JUnitRunner 13 | 14 | @RunWith(classOf[JUnitRunner]) 15 | class StackingVariantsTest extends Specification with LazyLogging { 16 | sequential 17 | 18 | import TestLevels._ 19 | val testLevel = Small 20 | 21 | // standard test suite of points and queries 22 | val n = getN(testLevel) 23 | val pointQueryPairs = getPointQueryPairs(testLevel) 24 | val points: Seq[XYZTPoint] = pointQueryPairs.map(_._1) 25 | val cells: Seq[Cell] = pointQueryPairs.map(_._2) 26 | val labels: Seq[String] = pointQueryPairs.map(_._3) 27 | 28 | val uniqueLabels = labels.toSet.toSeq 29 | 30 | def verifyRoundTrip(curve: ComposedCurve): Boolean = { 31 | val (_, msElapsed) = time(() => { 32 | var i = 0 33 | while (i < n) { 34 | val XYZTPoint(x, y, z, t) = points(i) 35 | val pt = 36 | if (curve.numLeafNodes == 4) Seq(x, y, z, t) 37 | else Seq(x, y, t) 38 | val hash = curve.pointToHash(pt) 39 | val cell = curve.hashToCell(hash) 40 | cell.contains(pt) must beTrue 41 | i = i + 1 42 | } 43 | }) 44 | 45 | println(s"${curve.name},round-trip,${msElapsed/1000.0},${curve.numLeafNodes}") 46 | 47 | true 48 | } 49 | 50 | def verifyQueryRanges(curve: ComposedCurve): Boolean = { 51 | // conduct all queries against this curve 52 | val results: List[(String, Seq[OrdinalPair], Long)] = pointQueryPairs.map{ 53 | case (point, rawCell, label) => 54 | val cell = 55 | if (curve.numLeafNodes == 4) rawCell 56 | else Cell(rawCell.dimensions.take(2) ++ rawCell.dimensions.takeRight(1)) 57 | 58 | val (ranges, msElapsed) = time(() => { 59 | val itr = curve.getRangesCoveringCell(cell) 60 | val list = itr.toList 61 | list 62 | }) 63 | 64 | // compute a net label (only needed for 3D curves) 65 | val netLabel = curve.numLeafNodes match { 66 | case 3 => label.take(2) + label.takeRight(1) 67 | case 4 => label 68 | case _ => 69 | throw new Exception(s"Something went wrong: ${curve.numLeafNodes} dimensions found") 70 | } 71 | 72 | (netLabel, ranges, msElapsed) 73 | }.toList 74 | 75 | // aggregate by label 76 | val aggregates = results.groupBy(_._1) 77 | aggregates.foreach { 78 | case (aggLabel, group) => 79 | var totalCells = 0L 80 | var totalRanges = 0L 81 | var totalMs = 0L 82 | 83 | group.foreach { 84 | case (_, ranges, ms) => 85 | totalRanges = totalRanges + ranges.size 86 | totalCells = totalCells + ranges.map(_.size).sum 87 | totalMs = totalMs + ms 88 | } 89 | 90 | val m = group.size.toDouble 91 | val avgRanges = totalRanges.toDouble / m 92 | val avgCells = totalCells.toDouble / m 93 | val seconds = totalMs.toDouble / 1000.0 94 | val avgCellsPerSecond = totalCells / seconds 95 | val avgRangesPerSecond = totalRanges / seconds 96 | val avgCellsPerRange = totalRanges / seconds 97 | val avgSecondsPerCell = seconds / totalCells 98 | val avgSecondsPerRange = seconds / totalRanges 99 | val avgScore = avgCellsPerSecond * avgCellsPerRange 100 | val avgAdjScore = avgCellsPerSecond * Math.log(1.0 + avgCellsPerRange) 101 | 102 | val data = Seq( 103 | DateTime.now().toString, 104 | curve.name, 105 | "ranges", 106 | aggLabel, 107 | curve.M, 108 | n, 109 | curve.numLeafNodes, 110 | curve.plys, 111 | avgRanges, 112 | avgCells, 113 | avgCellsPerSecond, 114 | avgCellsPerRange, 115 | avgSecondsPerCell, 116 | avgSecondsPerRange, 117 | avgScore, 118 | avgAdjScore, 119 | seconds 120 | ) 121 | println(data.mkString(",")) 122 | } 123 | 124 | true 125 | } 126 | 127 | def perCurveTestSuite(curve: ComposedCurve): Boolean = 128 | verifyRoundTrip(curve) && verifyQueryRanges(curve) 129 | 130 | "the various compositions" should { 131 | "print scaling results" >> { 132 | val totalPrecision = 24 133 | 134 | // 4D, horizontal 135 | FactoryXYZT(totalPrecision, 1).getCurves.map(curve => perCurveTestSuite(curve)) 136 | 137 | // 4D, mixed (2, 2) 138 | FactoryXYZT(totalPrecision, 2).getCurves.map(curve => perCurveTestSuite(curve)) 139 | 140 | // 4D, mixed (3, 1) 141 | FactoryXYZT(totalPrecision, -2).getCurves.map(curve => perCurveTestSuite(curve)) 142 | 143 | // 4D, vertical 144 | FactoryXYZT(totalPrecision, 3).getCurves.map(curve => perCurveTestSuite(curve)) 145 | 146 | // 3D, horizontal 147 | FactoryXYT(totalPrecision, 1).getCurves.map(curve => perCurveTestSuite(curve)) 148 | 149 | // 3D, mixed 150 | FactoryXYT(totalPrecision, 2).getCurves.map(curve => perCurveTestSuite(curve)) 151 | 152 | 1 must equalTo(1) 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/main/scala/org/eichelberger/sfc/study/locality/LocalityAnalysisStudy.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.study.locality 2 | 3 | import org.eichelberger.sfc.SpaceFillingCurve.{OrdinalVector, SpaceFillingCurve} 4 | import org.eichelberger.sfc.utils.Timing 5 | import org.eichelberger.sfc.{CompactHilbertCurve, ZCurve} 6 | 7 | import scala.collection.immutable.HashMap 8 | import scala.collection.mutable 9 | 10 | object LocalityAnalysisStudy extends App { 11 | val padding = 10 12 | 13 | trait InverseLocalityPredictor { 14 | this: SpaceFillingCurve => 15 | def predictInverseLocalitySquareMap: Map[Long, Long] 16 | def predictInverseLocality: Double 17 | } 18 | 19 | trait ZInverseLocalityPredictor extends InverseLocalityPredictor { 20 | this: ZCurve => 21 | 22 | def predictInverseLocalitySquareMap: Map[Long, Long] = { 23 | val p = mutable.HashMap[Long, Long]() 24 | 25 | for (bitpos <- M - 1 to 0 by -1) { 26 | val count = 1L << (M - bitpos - 1) 27 | 28 | val idx: Long = 1L << bitpos 29 | val prevIdx: Long = idx - 1L 30 | 31 | val coords = inverseIndex(idx) 32 | val prevCoords = inverseIndex(prevIdx) 33 | 34 | val dist = coords.zipWith(prevCoords).map { 35 | case (coord, prevCoord) => (coord - prevCoord) * (coord - prevCoord) 36 | }.sum 37 | 38 | p.put(dist, p.getOrElse(dist, 0L) + count) 39 | } 40 | 41 | p.toMap 42 | } 43 | 44 | def predictInverseLocality: Double = { 45 | val squaredMap = predictInverseLocalitySquareMap 46 | 47 | val (totalDist, totalCount) = squaredMap.foldLeft((0.0, 0.0))((acc, entry) => entry match { 48 | case (distSquared, count) => 49 | (acc._1 + Math.sqrt(distSquared) * count, acc._2 + count) 50 | }) 51 | 52 | totalDist / totalCount 53 | } 54 | 55 | 56 | } 57 | 58 | def exhaustCounts(curve: SpaceFillingCurve): Map[Long, Long] = { 59 | val counts = new mutable.HashMap[Long, Long]() 60 | 61 | for (idx <- 0 until curve.size.toInt - 1) { 62 | val a = curve.inverseIndex(idx) 63 | val b = curve.inverseIndex(idx + 1L) 64 | 65 | val dSquared: Long = a.zipWith(b).map { 66 | case (coordA, coordB) => (coordA - coordB) * (coordA - coordB) 67 | }.sum 68 | 69 | counts.put(dSquared, 1L + counts.getOrElse(dSquared, 0L)) 70 | } 71 | 72 | counts.toMap 73 | } 74 | 75 | def oneCurve(curve: SpaceFillingCurve with InverseLocalityPredictor): Unit = { 76 | 77 | val (counts, msCount) = Timing.time { () => exhaustCounts(curve) } 78 | 79 | val (prediction, msPrediction) = Timing.time { () => curve.predictInverseLocalitySquareMap } 80 | 81 | println(s"\n\n==========[ $curve ]==========\n") 82 | println( 83 | "D^2".formatted(s"%${padding}s") + " " + 84 | "COUNT".formatted(s"%${padding}s") + " " + 85 | "PRED".formatted(s"%${padding}s") + " " + 86 | "ERR".formatted(s"%${padding}s")) 87 | println( 88 | "-"*padding + " " + 89 | "-"*padding + " " + 90 | "-"*padding + " " + 91 | "-"*padding + " ") 92 | 93 | val keys = (counts.keySet ++ prediction.keySet).toList.sorted 94 | for (key <- keys) { 95 | val c = counts.getOrElse(key, 0L) 96 | val p = prediction.getOrElse(key, 0L) 97 | val e = Math.abs(c - p) 98 | val cs = if (c == 0) "-" else c.toString 99 | val ps = if (p == 0) "-" else p.toString 100 | val es = if (e == 0) "-" else e.toString 101 | 102 | println( 103 | key.formatted(s"%${padding}d") + " " + 104 | cs.formatted(s"%${padding}s") + " " + 105 | ps.formatted(s"%${padding}s") + " " + 106 | es.formatted(s"%${padding}s") 107 | ) 108 | 109 | require(e == 0, "Non-zero error for prediction on $curve") 110 | } 111 | 112 | println(s"\nInverse locality: ${curve.predictInverseLocality.formatted("%1.4f")}") 113 | 114 | println(s"\nTimes:\n count: ${msCount.formatted("%6d")} ms\n prediction: ${msPrediction.formatted("%6d")} ms") 115 | } 116 | 117 | // compare strategies for various curves (for correctness) 118 | for (i <- 1 to 6) { 119 | oneCurve(new ZCurve(OrdinalVector(i, i, i)) with ZInverseLocalityPredictor) //@TODO broken 120 | oneCurve(new ZCurve(OrdinalVector(i, i)) with ZInverseLocalityPredictor) 121 | oneCurve(new ZCurve(OrdinalVector(i, 1)) with ZInverseLocalityPredictor) 122 | oneCurve(new ZCurve(OrdinalVector(1, i)) with ZInverseLocalityPredictor) 123 | oneCurve(new ZCurve(OrdinalVector(i, i, 1)) with ZInverseLocalityPredictor) //@TODO broken 124 | oneCurve(new ZCurve(OrdinalVector(i, 1, i)) with ZInverseLocalityPredictor) //@TODO broken 125 | oneCurve(new ZCurve(OrdinalVector(1, i, i)) with ZInverseLocalityPredictor) //@TODO broken 126 | } 127 | 128 | // compare strategies for various curves (for timing) 129 | println() 130 | for (i <- 1 to 10) { 131 | val curve = new ZCurve(OrdinalVector(i, i)) with ZInverseLocalityPredictor 132 | val (counts, msCount) = Timing.time { () => exhaustCounts(curve) } 133 | val (prediction, msPrediction) = Timing.time { () => curve.predictInverseLocality } 134 | println(s"SMALL-SCALE COMPARATIVE TIMING, $curve, $msCount, $msPrediction, ${prediction.formatted("%1.4f")}") 135 | } 136 | 137 | // drive prediction harder, and see how long it takes 138 | println() 139 | for (i <- 13 to 15; j <- 13 to 15; k <- 13 to 15; m <- 13 to 15) { 140 | val curve = new ZCurve(OrdinalVector(i, j, k, m)) with ZInverseLocalityPredictor 141 | val (prediction, msPrediction) = Timing.time { () => curve.predictInverseLocality } 142 | println(s"AT-SCALE PREDICTION TIMING, $curve, $msPrediction, ${prediction.formatted("%1.4f")}") 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/test/scala/org/eichelberger/sfc/utils/CompositionParserTest.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.utils 2 | 3 | import com.typesafe.scalalogging.slf4j.LazyLogging 4 | import org.eichelberger.sfc.DefaultDimensions.IdentityDimension 5 | import org.eichelberger.sfc.SpaceFillingCurve.{OrdinalVector, SpaceFillingCurve} 6 | import org.eichelberger.sfc._ 7 | import org.joda.time.DateTime 8 | import org.junit.runner.RunWith 9 | import org.specs2.matcher.Matchers.beNull 10 | import org.specs2.mutable.Specification 11 | import org.specs2.runner.JUnitRunner 12 | 13 | @RunWith(classOf[JUnitRunner]) 14 | class CompositionParserTest extends Specification { 15 | sequential 16 | 17 | def parsableCurve(curve: SpaceFillingCurve): String = curve match { 18 | case c: ComposedCurve => 19 | c.delegate.name.charAt(0).toString + c.children.map { 20 | case d: IdentityDimension => d.precision 21 | case d: Dimension[DateTime] if d.min.isInstanceOf[DateTime] => s"t(${d.precision})" 22 | case d: Dimension[Double] if d.doubleMin < -100 => s"x(${d.precision})" 23 | case d: Dimension[Double] if d.doubleMin > -100 => s"y(${d.precision})" 24 | case s: SubDimension[_] => s.precision 25 | case c: SpaceFillingCurve => parsableCurve(c) 26 | }.mkString("(", ", ", ")") 27 | case s => 28 | s.name.charAt(0).toString + s.precisions.toSeq.map(_.toString).mkString("(", ", ", ")") 29 | } 30 | 31 | def eval(curve: ComposedCurve): Boolean = { 32 | val toParse: String = parsableCurve(curve) 33 | val parsed: ComposedCurve = CompositionParser.buildWholeNumberCurve(toParse) 34 | val fromParse: String = parsableCurve(parsed) 35 | println(s"[CURVE PARSER FORWARD]\n Input: $toParse\n Output: '$fromParse''") 36 | toParse == fromParse 37 | } 38 | 39 | def eval(toParse: String): Boolean = { 40 | val parsed: ComposedCurve = CompositionParser.buildWholeNumberCurve(toParse) 41 | val fromParse: String = parsableCurve(parsed) 42 | println(s"[CURVE PARSER BACKWARD]\n Input: '$toParse'\n Output: $fromParse") 43 | toParse == fromParse 44 | } 45 | 46 | "simple expressions" should { 47 | val R23 = new ComposedCurve( 48 | RowMajorCurve(2, 3), 49 | Seq( 50 | DefaultDimensions.createIdentityDimension(2), 51 | DefaultDimensions.createIdentityDimension(3) 52 | ) 53 | ) 54 | val H_2_R23 = new ComposedCurve( 55 | CompactHilbertCurve(2), 56 | Seq( 57 | DefaultDimensions.createIdentityDimension(2), 58 | R23 59 | ) 60 | ) 61 | val Z_R23_2 = new ComposedCurve( 62 | ZCurve(2), 63 | Seq( 64 | R23, 65 | DefaultDimensions.createIdentityDimension(2) 66 | ) 67 | ) 68 | 69 | "parse correctly" >> { 70 | eval(R23) must beTrue 71 | eval(H_2_R23) must beTrue 72 | eval(Z_R23_2) must beTrue 73 | 74 | 1 must_== 1 75 | } 76 | } 77 | 78 | "curves with explicit dimensions" should { 79 | 80 | "parse without explicit bounds correctly" >> { 81 | eval("Z(x(10))") must beTrue 82 | eval("Z(x(10), y(5))") must beTrue 83 | eval("Z(y(5), x(10))") must beTrue 84 | eval("Z(t(15), H(y(5), x(10)))") must beTrue 85 | eval("Z(H(y(5), x(10)), t(15))") must beTrue 86 | 87 | 1 must_== 1 88 | } 89 | 90 | "parse with explicit TIME bounds correctly" >> { 91 | val cADefault = CompositionParser.buildWholeNumberCurve("Z(t(10))") 92 | val cTDefault: Dimension[DateTime] = cADefault.children.head.asInstanceOf[Dimension[DateTime]] 93 | cTDefault.min must_== DefaultDimensions.MinDate 94 | cTDefault.max must_== DefaultDimensions.MaxDate 95 | 96 | val dtMin = "1990-01-01T00:00:00.000Z" 97 | val dtMax = "2050-12-31T23:59:59.999Z" 98 | val cACustom = CompositionParser.buildWholeNumberCurve(s"Z(t(10, $dtMin, $dtMax))") 99 | val cTCustom: Dimension[DateTime] = cACustom.children.head.asInstanceOf[Dimension[DateTime]] 100 | cTCustom.min must_== CompositionParser.dtf.parseDateTime(dtMin) 101 | cTCustom.max must_== CompositionParser.dtf.parseDateTime(dtMax) 102 | 103 | 1 must_== 1 104 | } 105 | 106 | "parse with explicit LONGITUDE bounds correctly" >> { 107 | val cADefault = CompositionParser.buildWholeNumberCurve("Z(x(10))") 108 | val cXDefault: Dimension[Double] = cADefault.children.head.asInstanceOf[Dimension[Double]] 109 | cXDefault.min must_== DefaultDimensions.dimLongitude.min 110 | cXDefault.max must_== DefaultDimensions.dimLongitude.max 111 | 112 | val dxMin = "-39.876" 113 | val dxMax = "41.234" 114 | val cACustom = CompositionParser.buildWholeNumberCurve(s"Z(x(10, $dxMin, $dxMax))") 115 | val cXCustom: Dimension[Double] = cACustom.children.head.asInstanceOf[Dimension[Double]] 116 | cXCustom.min must_== dxMin.toDouble 117 | cXCustom.max must_== dxMax.toDouble 118 | 119 | 1 must_== 1 120 | } 121 | 122 | "parse with all variables with explicit bounds" >> { 123 | val curve = CompositionParser.buildWholeNumberCurve("Z(x(2,-180.0,180.0),y(2,-90.0,90.0),t(2,1900-01-01T00:00:00.000Z,2029-12-31T23:59:59.999Z))") 124 | curve must not beNull; 125 | 1 must_== 1 126 | } 127 | 128 | "parse with explicit secondary indexes (U, V, W) set correctly" >> { 129 | // ... when using implicit per-dimension bounds 130 | val cImplicit = CompositionParser.buildWholeNumberCurve("Z(u(10), v(10))") 131 | cImplicit.cardinalities must_== List(1024, 1024) 132 | cImplicit.children.head.asInstanceOf[Dimension[Long]].min must_== 0 133 | cImplicit.children.head.asInstanceOf[Dimension[Long]].max must_== 1023 134 | cImplicit.children.last.asInstanceOf[Dimension[Long]].min must_== 0 135 | cImplicit.children.last.asInstanceOf[Dimension[Long]].max must_== 1023 136 | 137 | // ... when using explicit per-dimension bounds 138 | val cExplicit = CompositionParser.buildWholeNumberCurve("Z(u(10, 4, 12), v(10, 1, 18))") 139 | cExplicit.cardinalities must_== List(1024, 1024) 140 | cExplicit.children.head.asInstanceOf[Dimension[Long]].min must_== 4 141 | cExplicit.children.head.asInstanceOf[Dimension[Long]].max must_== 12 142 | cExplicit.children.last.asInstanceOf[Dimension[Long]].min must_== 1 143 | cExplicit.children.last.asInstanceOf[Dimension[Long]].max must_== 18 144 | 145 | 1 must_== 1 146 | } 147 | 148 | "index points correctly" >> { 149 | val dtMin = "1990-01-01T00:00:00.000Z" 150 | val dtMax = "2050-12-31T23:59:59.999Z" 151 | val curve = CompositionParser.buildWholeNumberCurve(s"Z(t(10, $dtMin, $dtMax), H(x(8), y(7)))") 152 | 153 | println(s"curve: ${curve.name}") 154 | println(s"top precisions: ${curve.precisions.toSeq.map(_.toString).mkString("[", ",", "]")}") 155 | 156 | val index = curve.pointToIndex(Seq[Any]( 157 | CompositionParser.dtf.parseDateTime("2020-07-10T12:23:31.000Z"), 158 | -79.9, 159 | 38.2 160 | )) 161 | println(s"index: $index") 162 | 163 | val cell = curve.indexToCell(index) 164 | println(s"cell: $cell") 165 | 166 | (index >= 0L) must beTrue 167 | (index < (1L << 25)) must beTrue 168 | 169 | 1 must_== 1 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/main/scala/org/eichelberger/sfc/ComposedCurve.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc 2 | 3 | import org.eichelberger.sfc.SpaceFillingCurve._ 4 | import org.eichelberger.sfc.utils.Lexicographics.Lexicographic 5 | 6 | object ComposedCurve { 7 | val EmptyQuery = Query(Seq[OrdinalRanges]()) 8 | 9 | case class CoveringReturn(query: Query, cell: Cell) { 10 | def this(cell: Cell) = this(EmptyQuery, cell) 11 | } 12 | } 13 | 14 | import ComposedCurve._ 15 | 16 | // Composables can be either SFCs or Partitioners; 17 | // leaf nodes must all be Partitioners 18 | class ComposedCurve(val delegate: SpaceFillingCurve, val children: Seq[Composable]) 19 | extends SpaceFillingCurve with Lexicographic { 20 | 21 | lazy val precisions: OrdinalVector = new OrdinalVector( 22 | children.zip(delegate.precisions.x).flatMap { 23 | case (c: ComposedCurve, _) => c.precisions.x 24 | case (c: SpaceFillingCurve, _) => c.precisions.x 25 | case (d: Dimension[_], prec: OrdinalNumber) => Seq(prec) 26 | }:_* 27 | ) 28 | 29 | lazy val numLeafNodes: Int = children.map { 30 | case c: ComposedCurve => c.numLeafNodes 31 | case c: SpaceFillingCurve => c.n 32 | case d: Dimension[_] => 1 33 | case d: SubDimension[_] => 1 34 | }.sum 35 | 36 | override lazy val plys: Int = 1 + children.map(_.plys).max 37 | 38 | lazy val name: String = delegate.name + 39 | children.map(_.name).mkString("(", ",", ")") 40 | 41 | private def _getRangesCoveringCell(cell: Cell): CoveringReturn = { 42 | // dimension ranges must be picked off in-order 43 | val covretFromChildren = children.foldLeft(new CoveringReturn(cell))((covret, child) => child match { 44 | case c: ComposedCurve => 45 | val CoveringReturn(subQuery: Query, subCell: Cell) = c._getRangesCoveringCell(covret.cell) 46 | CoveringReturn(covret.query + subQuery, subCell) 47 | case d: Dimension[_] => 48 | val dimRange = covret.cell.dimensions.head 49 | val idxRange = OrdinalPair(d.indexAny(dimRange.min), d.indexAny(dimRange.max)) 50 | val subQuery = Query(Seq(OrdinalRanges(idxRange))) 51 | CoveringReturn(covret.query + subQuery, Cell(covret.cell.dimensions.drop(1))) 52 | case d: SubDimension[_] => 53 | val dimRange = covret.cell.dimensions.head 54 | val idxRange = OrdinalPair(d.indexAny(dimRange.min), d.indexAny(dimRange.max)) 55 | val subQuery = Query(Seq(OrdinalRanges(idxRange))) 56 | CoveringReturn(covret.query + subQuery, Cell(covret.cell.dimensions.drop(1))) 57 | }) 58 | 59 | // having all of the dimension ranges you need, use them 60 | val ranges = delegate.getRangesCoveringQuery(covretFromChildren.query) 61 | val rangesToQuery = Query(Seq(OrdinalRanges(ranges.toList:_*))) 62 | CoveringReturn(rangesToQuery, covretFromChildren.cell) 63 | } 64 | 65 | def getRangesCoveringCell(cell: Cell): Iterator[OrdinalPair] = { 66 | val CoveringReturn(queryResult: Query, emptyCell: Cell) = _getRangesCoveringCell(cell) 67 | require(emptyCell.size == 0, "Expected the cell to be depleted; found ${cell.size} entries remaining") 68 | require(queryResult.numDimensions == 1, "Expected a single return dimension; found ${queryResult.numDimensions} instead") 69 | 70 | // coerce types 71 | queryResult.rangesPerDim.head.iterator 72 | } 73 | 74 | // it doesn't really make sense to call this routine, does it? 75 | def getRangesCoveringQuery(query: Query): Iterator[OrdinalPair] = 76 | throw new UnsupportedOperationException( 77 | "This routine is not sensible for a composed curve. Try getRangesCoveringCell(Cell) instead.") 78 | 79 | // def index(point: OrdinalVector): OrdinalNumber = 80 | // delegate.index(point) 81 | // 82 | // def inverseIndex(index: OrdinalNumber): OrdinalVector = 83 | // delegate.inverseIndex(index) 84 | 85 | def index(point: OrdinalVector): OrdinalNumber = { 86 | require(point.size == numLeafNodes, s"Number of point-dimensions (${point.size}) must equal number of leaf-nodes ($numLeafNodes)") 87 | 88 | val values = point.x 89 | 90 | // pick off these ordinal values in turn 91 | val (ordinalVector: OrdinalVector, _) = children.foldLeft((OrdinalVector(), values))((acc, child) => acc match { 92 | case (ordsSoFar, valuesLeft) => 93 | val (ord: OrdinalNumber, numUsed: Int) = child match { 94 | case c: ComposedCurve => 95 | val thisChildsValues = valuesLeft.take(c.numLeafNodes) 96 | (c.index(new OrdinalVector(thisChildsValues:_*)), c.numLeafNodes) 97 | case c: SpaceFillingCurve => 98 | val thisChildsValues = valuesLeft.take(c.n) 99 | (c.index(new OrdinalVector(thisChildsValues:_*)), c.n) 100 | case d: Dimension[_] => // identity map 101 | val thisChildsValues = valuesLeft.take(1) 102 | (thisChildsValues.head, 1) 103 | case _ => throw new Exception("Unrecognized child type") 104 | } 105 | (ordsSoFar ++ ord, valuesLeft.drop(numUsed)) 106 | }) 107 | 108 | // ask the delegate to do the final join 109 | delegate.index(ordinalVector) 110 | } 111 | 112 | def inverseIndex(index: OrdinalNumber): OrdinalVector = { 113 | // decompose this single index into coordinates 114 | val ordinalsVector = delegate.inverseIndex(index) 115 | 116 | // farm out these coordinates among the children 117 | val point: Seq[OrdinalNumber] = children.zip(ordinalsVector.toSeq).flatMap { 118 | case (child, ordinal) => 119 | child match { 120 | case c: ComposedCurve => c.inverseIndex(ordinal).x 121 | case c: SpaceFillingCurve => c.inverseIndex(ordinal).x 122 | case d: Dimension[_] => Seq(ordinal) // identity map 123 | } 124 | } 125 | 126 | new OrdinalVector(point:_*) 127 | } 128 | 129 | def pointToIndex(values: Seq[Any]): OrdinalNumber = { 130 | require(values.size == numLeafNodes, s"Number of values (${values.size}) must equal number of leaf-nodes ($numLeafNodes)") 131 | 132 | // convert these values to ordinal numbers 133 | val (ordinalVector: OrdinalVector, _) = children.foldLeft((OrdinalVector(), values))((acc, child) => acc match { 134 | case (ordsSoFar, valuesLeft) => 135 | val (ord: OrdinalNumber, numRemaining: Int) = child match { 136 | case c: ComposedCurve => 137 | val thisChildsValues = valuesLeft.take(c.numLeafNodes) 138 | (c.pointToIndex(thisChildsValues), c.numLeafNodes) 139 | case d: Dimension[_] => 140 | val thisChildsValues = valuesLeft.take(1) 141 | (d.indexAny(thisChildsValues.head), 1) 142 | case _ => throw new Exception("Unrecognized child type") 143 | } 144 | (ordsSoFar ++ ord, valuesLeft.drop(numRemaining)) 145 | }) 146 | 147 | // ask the delegate to do the final join 148 | delegate.index(ordinalVector) 149 | } 150 | 151 | def pointToHash(point: Seq[_]): String = 152 | lexEncodeIndex(pointToIndex(point)) 153 | 154 | def indexToCell(index: OrdinalNumber): Cell = { 155 | // decompose this single index into coordinates 156 | val ordinalsVector = delegate.inverseIndex(index) 157 | 158 | // farm out these coordinates among the children 159 | val dims: Seq[Dimension[_]] = children.zip(ordinalsVector.toSeq).flatMap { 160 | case (child, ordinal) => 161 | child match { 162 | case c: ComposedCurve => c.indexToCell(ordinal).dimensions 163 | case d: Dimension[_] => Seq(d.inverseIndex(ordinal)) 164 | } 165 | } 166 | 167 | Cell(dims) 168 | } 169 | 170 | def hashToCell(hash: String): Cell = 171 | indexToCell(lexDecodeIndex(hash)) 172 | } 173 | -------------------------------------------------------------------------------- /src/main/scala/org/eichelberger/sfc/study/composition/SchemaStudy.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.study.composition 2 | 3 | import org.eichelberger.sfc.examples.Geohash 4 | import org.eichelberger.sfc.study.composition.CompositionSampleData._ 5 | import org.eichelberger.sfc.study.composition.SchemaStudy.TestCase 6 | import org.eichelberger.sfc.utils.Timing 7 | import org.eichelberger.sfc._ 8 | import org.eichelberger.sfc.SpaceFillingCurve._ 9 | import org.eichelberger.sfc.study._ 10 | import org.joda.time.{DateTimeZone, DateTime} 11 | 12 | /* 13 | 14 | Need the ability to force the planner to bail out early, 15 | because we've declared in advance that we don't want more 16 | than RRR ranges returned. 17 | 18 | Need a new metric: 19 | Something to represent the percentage of (expected) false-positives 20 | that result from the current range (approximation). 21 | 22 | */ 23 | 24 | object SchemaStudy extends App { 25 | 26 | val output: OutputDestination = { 27 | val columns = OutputMetadata(Seq( 28 | ColumnSpec("when", isQuoted = true), 29 | ColumnSpec("curve", isQuoted = true), 30 | ColumnSpec("test.type", isQuoted = true), 31 | ColumnSpec("label", isQuoted = true), 32 | ColumnSpec("precision", isQuoted = false), 33 | ColumnSpec("replications", isQuoted = false), 34 | ColumnSpec("dimensions", isQuoted = false), 35 | ColumnSpec("plys", isQuoted = false), 36 | ColumnSpec("avg.ranges", isQuoted = false), 37 | ColumnSpec("avg.cells", isQuoted = false), 38 | ColumnSpec("cells.per.second", isQuoted = false), 39 | ColumnSpec("cells.per.range", isQuoted = false), 40 | ColumnSpec("seconds.per.cell", isQuoted = false), 41 | ColumnSpec("seconds.per.range", isQuoted = false), 42 | ColumnSpec("score", isQuoted = false), 43 | ColumnSpec("adj.score", isQuoted = false), 44 | ColumnSpec("seconds", isQuoted = false) 45 | )) 46 | val baseFile = "schema" 47 | new MultipleOutput(Seq( 48 | new MirroredTSV(s"/tmp/$baseFile.tsv", columns, writeHeader = true), 49 | new JSON(s"/tmp/$baseFile.js", columns) 50 | with FileOutputDestination { def fileName = s"/tmp/$baseFile.js" } 51 | )) 52 | } 53 | 54 | 55 | import TestLevels._ 56 | val testLevel = Large 57 | 58 | // standard test suite of points and queries 59 | val n = getN(testLevel) 60 | val pointQueryPairs = getPointQueryPairs(testLevel) 61 | val points: Seq[XYZTPoint] = pointQueryPairs.map(_._1) 62 | val cells: Seq[Cell] = pointQueryPairs.map(_._2) 63 | val labels: Seq[String] = pointQueryPairs.map(_._3) 64 | 65 | val uniqueLabels = labels.toSet.toSeq 66 | 67 | case class TestCase(curve: ComposedCurve, columnOrder: String) 68 | val TXY = "T ~ X ~ Y" 69 | val T0XYT1 = "T0 ~ X ~ Y ~ T1" 70 | val XYT = "X ~ Y ~ T" 71 | 72 | def testSchema(testCase: TestCase): Unit = { 73 | val TestCase(curve, columnOrder) = testCase 74 | 75 | println(s"[SCHEMA ${curve.name}]") 76 | // conduct all queries against this curve 77 | val results: List[(String, Seq[OrdinalPair], Long)] = pointQueryPairs.map{ 78 | case (point, rawCell, label) => 79 | val x = rawCell.dimensions.head 80 | val y = rawCell.dimensions(1) 81 | val t = rawCell.dimensions(3) 82 | 83 | val cell = columnOrder match { 84 | case s if s == TXY => 85 | Cell(Seq(t, x, y)) 86 | case s if s == T0XYT1 => 87 | Cell(Seq(t, x, y, t)) 88 | case s if s == XYT => 89 | Cell(Seq(x, y, t)) 90 | case _ => 91 | throw new Exception(s"Unhandled column order: $columnOrder") 92 | } 93 | 94 | //@TODO debug! 95 | //System.out.println(s"TEST: cell $cell") 96 | 97 | curve.clearCache() 98 | 99 | val (ranges, msElapsed) = Timing.time(() => { 100 | val itr = curve.getRangesCoveringCell(cell) 101 | val list = itr.toList 102 | list 103 | }) 104 | 105 | // compute a net label (only needed for 3D curves) 106 | val netLabel = label.take(2) + label.takeRight(1) 107 | 108 | (netLabel, ranges, msElapsed) 109 | }.toList 110 | 111 | // aggregate by label 112 | val aggregates = results.groupBy(_._1) 113 | aggregates.foreach { 114 | case (aggLabel, group) => 115 | var totalCells = 0L 116 | var totalRanges = 0L 117 | var totalMs = 0L 118 | 119 | group.foreach { 120 | case (_, ranges, ms) => 121 | totalRanges = totalRanges + ranges.size 122 | totalCells = totalCells + ranges.map(_.size).sum 123 | totalMs = totalMs + ms 124 | } 125 | 126 | val m = group.size.toDouble 127 | val avgRanges = totalRanges.toDouble / m 128 | val avgCells = totalCells.toDouble / m 129 | val seconds = totalMs.toDouble / 1000.0 130 | val avgCellsPerSecond = totalCells / seconds 131 | val avgRangesPerSecond = totalRanges / seconds 132 | val avgCellsPerRange = totalRanges / seconds 133 | val avgSecondsPerCell = seconds / totalCells 134 | val avgSecondsPerRange = seconds / totalRanges 135 | val avgScore = avgCellsPerSecond * avgCellsPerRange 136 | val avgAdjScore = avgCellsPerSecond * Math.log(1.0 + avgCellsPerRange) 137 | 138 | val data = Seq( 139 | DateTime.now().toString, 140 | curve.name, 141 | "ranges", 142 | aggLabel, 143 | curve.M, 144 | n, 145 | curve.numLeafNodes, 146 | curve.plys, 147 | avgRanges, 148 | avgCells, 149 | avgCellsPerSecond, 150 | avgCellsPerRange, 151 | avgSecondsPerCell, 152 | avgSecondsPerRange, 153 | avgScore, 154 | avgAdjScore, 155 | seconds 156 | ) 157 | output.println(data) 158 | } 159 | } 160 | 161 | val BasePrecision = 9 162 | 163 | // Geohash dimensions 164 | val gh = new Geohash(BasePrecision << 1L) 165 | val subGH0 = new SubDimension[Seq[Any]]("gh0", gh.pointToIndex, gh.M, 0, BasePrecision * 2 / 3) 166 | val subGH1 = new SubDimension[Seq[Any]]("gh1", gh.pointToIndex, gh.M, BasePrecision * 2 / 3, BasePrecision * 2 / 3) 167 | val subGH2 = new SubDimension[Seq[Any]]("gh2", gh.pointToIndex, gh.M, BasePrecision * 4 / 3, (BasePrecision << 1L) - BasePrecision * 4 / 3) 168 | 169 | // geographic dimensions 170 | val dimX = DefaultDimensions.createLongitude(BasePrecision) 171 | val dimY = DefaultDimensions.createLongitude(BasePrecision) 172 | 173 | // time dimensions 174 | val dimTime = DefaultDimensions.createNearDateTime(BasePrecision) 175 | val dimT0 = new SubDimension[DateTime]("t0", dimTime.index, dimTime.precision, 0, dimTime.precision >> 1) 176 | val dimT1 = new SubDimension[DateTime]("t1", dimTime.index, dimTime.precision, dimTime.precision >> 1, dimTime.precision - (dimTime.precision >> 1)) 177 | 178 | def getCurves: Seq[TestCase] = Seq( 179 | // R(t, gh) 180 | TestCase(new ComposedCurve(RowMajorCurve(dimTime.precision, gh.M), Seq(dimTime, gh)), TXY), 181 | 182 | // R(gh, t) 183 | TestCase(new ComposedCurve(RowMajorCurve(gh.M, dimTime.precision), Seq(gh, dimTime)), XYT), 184 | 185 | // R(t0, gh, t1) 186 | TestCase(new ComposedCurve(RowMajorCurve(dimT0.precision, gh.M, dimT1.precision), Seq(dimT0, gh, dimT1)), T0XYT1), 187 | 188 | // R(t0, Z(x, y, t1)) 189 | TestCase( 190 | new ComposedCurve( 191 | RowMajorCurve(dimT0.precision, dimX.precision + dimY.precision + dimT1.precision), 192 | Seq(dimT0, new ComposedCurve( 193 | ZCurve(dimX.precision, dimY.precision, dimT1.precision), 194 | Seq(dimX, dimY, dimT1)))), 195 | T0XYT1) 196 | ) 197 | 198 | getCurves.foreach(testSchema) 199 | 200 | output.close() 201 | } 202 | -------------------------------------------------------------------------------- /src/test/scala/org/eichelberger/sfc/ZCurveTest.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc 2 | 3 | import com.typesafe.scalalogging.slf4j.LazyLogging 4 | import org.eichelberger.sfc.CompactHilbertCurve.Mask 5 | import org.eichelberger.sfc.SpaceFillingCurve.{OrdinalVector, SpaceFillingCurve, _} 6 | import org.eichelberger.sfc.utils.Timing 7 | import org.junit.runner.RunWith 8 | import org.specs2.mutable.Specification 9 | import org.specs2.runner.JUnitRunner 10 | 11 | @RunWith(classOf[JUnitRunner]) 12 | class ZCurveTest extends Specification with GenericCurveValidation with LazyLogging { 13 | sequential 14 | 15 | def curveName = "ZCurve" 16 | 17 | def createCurve(precisions: OrdinalNumber*): SpaceFillingCurve = 18 | new ZCurve(precisions.toOrdinalVector) 19 | 20 | def verifyQuery(curve: ZCurve, query: Query, expected: Seq[OrdinalPair]): Boolean = { 21 | val ranges = curve.getRangesCoveringQuery(query).toList 22 | 23 | ranges.zipWithIndex.foreach { 24 | case (range, i) => 25 | println(s"[z-curve query ranges $query] $i: $range -> ${expected(i)}") 26 | range must equalTo(expected(i)) 27 | } 28 | 29 | ranges.size must equalTo(expected.size) 30 | 31 | true 32 | } 33 | 34 | "planning methods" should { 35 | val precisions = OrdinalVector(1, 3, 7) 36 | val z = new ZCurve(precisions) 37 | 38 | "compute ranges from prefix and precision" >> { 39 | val range_0_0 = OrdinalPair(0, 2047) 40 | z.getRange(0, 0) must equalTo(range_0_0) 41 | 42 | val range_3_5 = OrdinalPair(192, 255) 43 | z.getRange(3, 5) must equalTo(range_3_5) 44 | } 45 | 46 | "identify supersets" >> { 47 | val qPair = OrdinalPair(10, 20) 48 | val iPair_sub = OrdinalPair(10, 15) 49 | val iPair_ovr_L = OrdinalPair(5, 15) 50 | val iPair_ovr_R = OrdinalPair(15, 25) 51 | val iPair_sup = OrdinalPair(5, 25) 52 | val iPair_disjoint_L = OrdinalPair(0, 5) 53 | val iPair_disjoint_R = OrdinalPair(25, 30) 54 | 55 | z.isPairSupersetOfPair(qPair, iPair_sub) must beTrue 56 | z.isPairSupersetOfPair(qPair, iPair_ovr_L) must beFalse 57 | z.isPairSupersetOfPair(qPair, iPair_ovr_R) must beFalse 58 | z.isPairSupersetOfPair(qPair, iPair_sup) must beFalse 59 | z.isPairSupersetOfPair(qPair, iPair_disjoint_L) must beFalse 60 | z.isPairSupersetOfPair(qPair, iPair_disjoint_R) must beFalse 61 | } 62 | 63 | "identify disjointedness" >> { 64 | val qPair = OrdinalPair(10, 20) 65 | val iPair_sub = OrdinalPair(10, 15) 66 | val iPair_ovr_L = OrdinalPair(5, 15) 67 | val iPair_ovr_R = OrdinalPair(15, 25) 68 | val iPair_sup = OrdinalPair(5, 25) 69 | val iPair_disjoint_L = OrdinalPair(0, 5) 70 | val iPair_disjoint_R = OrdinalPair(25, 30) 71 | 72 | z.pairsAreDisjoint(qPair, iPair_sub) must beFalse 73 | z.pairsAreDisjoint(qPair, iPair_ovr_L) must beFalse 74 | z.pairsAreDisjoint(qPair, iPair_ovr_R) must beFalse 75 | z.pairsAreDisjoint(qPair, iPair_sup) must beFalse 76 | z.pairsAreDisjoint(qPair, iPair_disjoint_L) must beTrue 77 | z.pairsAreDisjoint(qPair, iPair_disjoint_R) must beTrue 78 | } 79 | 80 | "identify query coverage and disjointedness" >> { 81 | val query = Query(Seq( 82 | OrdinalRanges(OrdinalPair(1, 5)), 83 | OrdinalRanges(OrdinalPair(6, 12)))) 84 | 85 | val xAllLow = OrdinalPair(0, 0) 86 | val xOverlapLow = OrdinalPair(0, 1) 87 | val xSpan = OrdinalPair(0, 6) 88 | val xIdent = OrdinalPair(1, 5) 89 | val xOverlapHigh = OrdinalPair(5, 10) 90 | val xAllHigh = OrdinalPair(8, 10) 91 | 92 | val yAllLow = OrdinalPair(0, 0) 93 | val yOverlapLow = OrdinalPair(0, 6) 94 | val ySpan = OrdinalPair(5, 13) 95 | val yIdent = OrdinalPair(6, 12) 96 | val yOverlapHigh = OrdinalPair(12, 15) 97 | val yAllHigh = OrdinalPair(13, 15) 98 | 99 | // coverage 100 | (for ( 101 | xr <- Seq(xAllLow, xOverlapLow, xSpan, xIdent, xOverlapHigh, xAllHigh); 102 | yr <- Seq(yAllLow, yOverlapLow, ySpan, yIdent, yOverlapHigh, yAllHigh) if xr != xIdent || yr != yIdent; 103 | test = z.queryCovers(query, Seq(xr, yr)) 104 | ) yield test).forall(!_) must beTrue 105 | 106 | z.queryCovers(query, Seq(xIdent, yIdent)) must beTrue 107 | 108 | // disjointedness 109 | (for ( 110 | xr <- Seq(xAllLow, xAllHigh); 111 | yr <- Seq(yAllLow, yOverlapLow, ySpan, yIdent, yOverlapHigh, yAllHigh); 112 | test = z.queryIsDisjoint(query, Seq(xr, yr)) 113 | ) yield { 114 | println(s"[Z x TEST DISJOINT] query $query, x-range $xr, y-range $yr, is disjoint? $test") 115 | test 116 | }).forall(identity) must beTrue 117 | 118 | (for ( 119 | xr <- Seq(xAllLow, xOverlapLow, xSpan, xIdent, xOverlapHigh, xAllHigh); 120 | yr <- Seq(yAllLow, yAllHigh); 121 | test = z.queryIsDisjoint(query, Seq(xr, yr)) 122 | ) yield { 123 | println(s"[Z y TEST DISJOINT] query $query, x-range $xr, y-range $yr, is disjoint? $test") 124 | test 125 | }).forall(identity) must beTrue 126 | 127 | val querySmall = Query(Seq( 128 | OrdinalRanges(OrdinalPair(1, 3)), 129 | OrdinalRanges(OrdinalPair(2, 3)))) 130 | z.queryIsDisjoint(query, Seq(OrdinalPair(0, 1), OrdinalPair(0, 1))) must beTrue 131 | } 132 | 133 | "compute extents" >> { 134 | def testExtent(prefix: OrdinalNumber, precision: Int, expected: Seq[OrdinalPair]) = { 135 | val extent = z.getExtents(prefix, precision) 136 | println(s"[EXTENT ${z.name}] ($prefix, $precision) -> $extent") 137 | extent must equalTo(expected) 138 | } 139 | 140 | testExtent(0, 0, z.cardinalities.toSeq.map(c => OrdinalPair(0, c - 1L))) 141 | testExtent(3, 2, Seq(OrdinalPair(1, 1), OrdinalPair(4, 7), OrdinalPair(0, 127))) 142 | testExtent(1023, 9, Seq(OrdinalPair(1, 1), OrdinalPair(7, 7), OrdinalPair(124, 127))) 143 | } 144 | 145 | "identify ranges from coordinate queries on small, 2D, square-ish spaces" >> { 146 | verifyQuery( 147 | ZCurve(OrdinalVector(2, 2)), 148 | Query(Seq( 149 | OrdinalRanges(OrdinalPair(2, 3)), 150 | OrdinalRanges(OrdinalPair(1, 3)) 151 | )), 152 | Seq( 153 | OrdinalPair(9, 9), 154 | OrdinalPair(11, 15) 155 | ) 156 | ) must beTrue 157 | } 158 | 159 | "identify ranges from coordinate queries on square-ish spaces" >> { 160 | verifyQuery( 161 | ZCurve(OrdinalVector(4, 4)), 162 | Query(Seq( 163 | OrdinalRanges(OrdinalPair(1, 5)), 164 | OrdinalRanges(OrdinalPair(6, 12)) 165 | )), 166 | Seq( 167 | OrdinalPair(22, 23), 168 | OrdinalPair(28, 31), 169 | OrdinalPair(52, 55), 170 | OrdinalPair(66, 67), 171 | OrdinalPair(70, 79), 172 | OrdinalPair(82, 82), 173 | OrdinalPair(88, 88), 174 | OrdinalPair(90, 90), 175 | OrdinalPair(96, 103), 176 | OrdinalPair(112, 112), 177 | OrdinalPair(114, 114) 178 | ) 179 | ) must beTrue 180 | } 181 | 182 | "identify ranges from coordinate queries on non-square-ish space #1" >> { 183 | verifyQuery( 184 | ZCurve(OrdinalVector(2, 4)), 185 | Query(Seq( 186 | OrdinalRanges(OrdinalPair(1, 2)), 187 | OrdinalRanges(OrdinalPair(6, 12)) 188 | )), 189 | Seq( 190 | OrdinalPair(14, 15), 191 | OrdinalPair(24, 28), 192 | OrdinalPair(38, 39), 193 | OrdinalPair(48, 52) 194 | ) 195 | ) must beTrue 196 | 197 | verifyQuery( 198 | ZCurve(OrdinalVector(2, 4)), 199 | Query(Seq( 200 | OrdinalRanges(OrdinalPair(2, 3)), 201 | OrdinalRanges(OrdinalPair(6, 12)) 202 | )), 203 | Seq( 204 | OrdinalPair(38, 39), 205 | OrdinalPair(46, 52), 206 | OrdinalPair(56, 60) 207 | ) 208 | ) must beTrue 209 | 210 | verifyQuery( 211 | ZCurve(OrdinalVector(2, 4)), 212 | Query(Seq( 213 | OrdinalRanges(OrdinalPair(0, 3)), 214 | OrdinalRanges(OrdinalPair(4, 4)) 215 | )), 216 | Seq( 217 | OrdinalPair(4, 4), 218 | OrdinalPair(12, 12), 219 | OrdinalPair(36, 36), 220 | OrdinalPair(44, 44) 221 | ) 222 | ) must beTrue 223 | } 224 | } 225 | 226 | "Z-order space-filling curves" should { 227 | "satisfy the ordering constraints" >> { 228 | timeTestOrderings() must beTrue 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.eichelberger 8 | sfseize 9 | 1.0-SNAPSHOT 10 | jar 11 | SFSeize 12 | 13 | 14 | 2.11 15 | 2.11.8 16 | 1.0.4 17 | 18 | 1.7.10 19 | 2.1.2 20 | 1.2.17 21 | 22 | -Xms4g -Xmx8g -XX:-UseGCOverheadLimit 23 | once 24 | 25 | UTF-8 26 | 27 | 28 | 29 | 30 | org.scala-lang 31 | scala-library 32 | ${scala.version} 33 | 34 | 35 | org.scala-lang.modules 36 | scala-parser-combinators_2.11 37 | ${scala.parser.version} 38 | 39 | 40 | org.scala-lang 41 | scala-reflect 42 | ${scala.version} 43 | 44 | 45 | com.typesafe.scala-logging 46 | scala-logging-slf4j_${scala.series} 47 | ${scalalogging.version} 48 | 49 | 50 | org.slf4j 51 | slf4j-api 52 | ${slf4j.version} 53 | 54 | 55 | joda-time 56 | joda-time 57 | 2.7 58 | 59 | 60 | 61 | 62 | org.slf4j 63 | slf4j-log4j12 64 | ${slf4j.version} 65 | test 66 | 67 | 68 | log4j 69 | log4j 70 | ${log4j.version} 71 | 72 | 73 | org.specs2 74 | specs2-core_${scala.series} 75 | 3.5 76 | test 77 | 78 | 79 | org.specs2 80 | specs2-matcher_${scala.series} 81 | 3.5 82 | test 83 | 84 | 85 | org.specs2 86 | specs2-junit_${scala.series} 87 | 3.5 88 | test 89 | 90 | 91 | org.scalatest 92 | scalatest_${scala.series} 93 | 2.2.1 94 | test 95 | 96 | 97 | 98 | 99 | 100 | 101 | src/test/resources 102 | false 103 | 104 | 105 | 106 | 107 | net.alchim31.maven 108 | scala-maven-plugin 109 | 3.2.0 110 | 111 | 112 | scala-compile-first 113 | process-resources 114 | 115 | add-source 116 | compile 117 | 118 | 119 | 120 | scala-test-compile 121 | process-test-resources 122 | 123 | testCompile 124 | 125 | 126 | 127 | 128 | 129 | -Xms1024m 130 | -Xmx4096m 131 | 132 | 133 | -nowarn 134 | -unchecked 135 | -deprecation 136 | 137 | ${scala.version} 138 | 139 | 140 | 141 | org.apache.maven.plugins 142 | maven-surefire-plugin 143 | 2.18.1 144 | 158 | 159 | 160 | org.apache.maven.plugins 161 | maven-dependency-plugin 162 | 2.10 163 | 164 | 165 | copy-dependencies 166 | package 167 | 168 | copy-dependencies 169 | 170 | 171 | ${project.build.directory}/dependencies 172 | false 173 | false 174 | true 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | central 185 | default 186 | http://repo1.maven.org/maven2 187 | 188 | true 189 | 190 | 191 | 192 | bintray 193 | http://dl.bintray.com/scalaz/releases 194 | 195 | 196 | geotools 197 | http://download.osgeo.org/webdav/geotools 198 | 199 | 200 | boundlessgeo 201 | http://repo.boundlessgeo.com/main 202 | 203 | 204 | conjars.org 205 | http://conjars.org/repo 206 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /src/main/scala/org/eichelberger/sfc/examples/composition/contrast/StackingVariants.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.examples.composition.contrast 2 | 3 | import org.eichelberger.sfc._ 4 | import org.eichelberger.sfc.SpaceFillingCurve._ 5 | import org.eichelberger.sfc.utils.Lexicographics.Lexicographic 6 | import org.joda.time.DateTime 7 | 8 | object BaseCurves { 9 | val RowMajor = 0 10 | val ZOrder = 1 11 | val Hilbert = 2 12 | 13 | def decomposePrecision(combination: OrdinalVector, precision: Int): (Int, Int) = { 14 | val b = precision / (combination.size + 1) 15 | val a = precision - b 16 | (a, b) 17 | } 18 | 19 | def rawNWayCurve(combination: OrdinalVector, precisions: OrdinalNumber*): SpaceFillingCurve = 20 | combination.toSeq.last match { 21 | case 0 => RowMajorCurve(precisions:_*) 22 | case 1 => ZCurve(precisions:_*) 23 | case 2 => CompactHilbertCurve(precisions:_*) 24 | } 25 | } 26 | import BaseCurves._ 27 | 28 | // four-dimensionals... 29 | 30 | case class FactoryXYZT(precision: Int, plys: Int) { 31 | /* 32 | * SFC 33 | * / \ 34 | * SFC t 35 | * / \ 36 | * SFC z 37 | * / \ 38 | * x y 39 | */ 40 | def buildVerticalCurve(combination: OrdinalVector, precision: Int): ComposedCurve = { 41 | val (leftPrec, rightPrec) = decomposePrecision(combination, precision) 42 | 43 | val rightChild: Composable = combination.size match { 44 | case 3 => DefaultDimensions.createNearDateTime(rightPrec) 45 | case 2 => DefaultDimensions.createDimension[Double]("z", 0.0, 50000.0, rightPrec) 46 | case 1 => DefaultDimensions.createLatitude(rightPrec) 47 | case _ => throw new Exception("Invalid right child specification") 48 | } 49 | val leftChild: Composable = combination.size match { 50 | case 3 | 2 => buildVerticalCurve(OrdinalVector(combination.toSeq.dropRight(1):_*), leftPrec) 51 | case 1 => DefaultDimensions.createLongitude(leftPrec) 52 | case _ => throw new Exception("Invalid left child specification") 53 | } 54 | 55 | val rawCurve = rawNWayCurve(combination, leftPrec, rightPrec) 56 | 57 | new ComposedCurve(rawCurve, Seq(leftChild, rightChild)) 58 | } 59 | 60 | /* 61 | * SFC 62 | * / \ 63 | * SFC SFC 64 | * / \ / \ 65 | * x y z t 66 | */ 67 | def buildMixedCurve22(combination: OrdinalVector, precision: Int, side: Int = 0): ComposedCurve = { 68 | val (leftPrec, rightPrec) = decomposePrecision(combination, precision) 69 | 70 | val leftChild: Composable = side match { 71 | case 0 => buildMixedCurve22(OrdinalVector(combination(0)), leftPrec, 1) 72 | case 1 => DefaultDimensions.createLongitude(leftPrec) 73 | case 2 => DefaultDimensions.createDimension[Double]("z", 0.0, 50000.0, leftPrec) 74 | } 75 | val rightChild: Composable = side match { 76 | case 0 => buildMixedCurve22(OrdinalVector(combination(1)), rightPrec, 2) 77 | case 1 => DefaultDimensions.createLatitude(rightPrec) 78 | case 2 => DefaultDimensions.createNearDateTime(rightPrec) 79 | } 80 | 81 | val rawCurve = rawNWayCurve(combination, leftPrec, rightPrec) 82 | 83 | new ComposedCurve(rawCurve, Seq(leftChild, rightChild)) 84 | } 85 | 86 | /* 87 | * SFC 88 | * / \ 89 | * SFC t 90 | * / | \ 91 | * x y z 92 | */ 93 | def buildMixedCurve31(combination: OrdinalVector, precision: Int, side: Int = 0): ComposedCurve = { 94 | val topDimPrecision = precision >> 2 95 | val bottomCurvePrecision = precision - topDimPrecision 96 | val leafPrecisionRight = bottomCurvePrecision / 3 97 | val leafPrecisionCenter = (bottomCurvePrecision - leafPrecisionRight) >> 1 98 | val leafPrecisionLeft = bottomCurvePrecision - leafPrecisionRight - leafPrecisionCenter 99 | 100 | val rawCurveBottom = rawNWayCurve(OrdinalVector(combination(0)), leafPrecisionLeft, leafPrecisionCenter, leafPrecisionRight) 101 | 102 | val curveBottom = new ComposedCurve( 103 | rawCurveBottom, 104 | Seq( 105 | DefaultDimensions.createLongitude(leafPrecisionLeft), 106 | DefaultDimensions.createLatitude(leafPrecisionCenter), 107 | DefaultDimensions.createDimension[Double]("z", 0.0, 50000.0, leafPrecisionRight) 108 | ) 109 | ) 110 | 111 | val rawCurveTop = rawNWayCurve(OrdinalVector(combination(1)), bottomCurvePrecision, topDimPrecision) 112 | 113 | new ComposedCurve( 114 | rawCurveTop, 115 | Seq( 116 | curveBottom, 117 | DefaultDimensions.createNearDateTime(topDimPrecision) 118 | ) 119 | ) 120 | } 121 | 122 | /* 123 | * SFC 124 | * / | | \ 125 | * x y z t 126 | */ 127 | def buildHorizontalCurve(combination: OrdinalVector, precision: Int): ComposedCurve = { 128 | val tPrecision = precision / 4 129 | val zPrecision = (precision - tPrecision) / 3 130 | val yPrecision = (precision - tPrecision - zPrecision) >> 1 131 | val xPrecision = precision - tPrecision - zPrecision - yPrecision 132 | 133 | val rawCurve = rawNWayCurve(combination, xPrecision, yPrecision, zPrecision, tPrecision) 134 | 135 | new ComposedCurve( 136 | rawCurve, 137 | Seq( 138 | DefaultDimensions.createLongitude(xPrecision), 139 | DefaultDimensions.createLatitude(yPrecision), 140 | DefaultDimensions.createDimension[Double]("z", 0.0, 50000.0, zPrecision), 141 | DefaultDimensions.createNearDateTime(tPrecision) 142 | ) 143 | ) 144 | } 145 | 146 | def getCurves: Seq[ComposedCurve] = { 147 | plys match { 148 | case 3 => // vertical 149 | combinationsIterator(OrdinalVector(3, 3, 3)).toList.map(combination => buildVerticalCurve(combination, precision)) 150 | case 2 => // mixed 151 | combinationsIterator(OrdinalVector(3, 3, 3)).toList.map(combination => buildMixedCurve22(combination, precision)) 152 | case -2 => // mixed 153 | combinationsIterator(OrdinalVector(3, 3)).toList.map(combination => buildMixedCurve31(combination, precision)) 154 | case 1 => // horizontal 155 | combinationsIterator(OrdinalVector(3)).toList.map(combination => buildHorizontalCurve(combination, precision)) 156 | } 157 | } 158 | } 159 | 160 | // three-dimensionals... 161 | 162 | case class FactoryXYT(precision: Int, plys: Int) { 163 | def buildVerticalCurve(combination: OrdinalVector, precision: Int): ComposedCurve = { 164 | val (leftPrec, rightPrec) = decomposePrecision(combination, precision) 165 | 166 | val rightChild: Composable = combination.size match { 167 | case 2 => DefaultDimensions.createNearDateTime(rightPrec) 168 | case 1 => DefaultDimensions.createLatitude(rightPrec) 169 | case _ => throw new Exception("Invalid right child specification") 170 | } 171 | val leftChild: Composable = combination.size match { 172 | case 2 => buildVerticalCurve(OrdinalVector(combination.toSeq.dropRight(1):_*), leftPrec) 173 | case 1 => DefaultDimensions.createLongitude(leftPrec) 174 | case _ => throw new Exception("Invalid left child specification") 175 | } 176 | 177 | val rawCurve = rawNWayCurve(combination, leftPrec, rightPrec) 178 | 179 | new ComposedCurve(rawCurve, Seq(leftChild, rightChild)) 180 | } 181 | 182 | def buildHorizontalCurve(combination: OrdinalVector, precision: Int): ComposedCurve = { 183 | val tPrecision = precision / 3 184 | val yPrecision = (precision - tPrecision) >> 1 185 | val xPrecision = precision - tPrecision - yPrecision 186 | 187 | val rawCurve = rawNWayCurve(combination, xPrecision, yPrecision, tPrecision) 188 | 189 | new ComposedCurve( 190 | rawCurve, 191 | Seq( 192 | DefaultDimensions.createLongitude(xPrecision), 193 | DefaultDimensions.createLatitude(yPrecision), 194 | DefaultDimensions.createNearDateTime(tPrecision) 195 | ) 196 | ) 197 | } 198 | 199 | def getCurves: Seq[ComposedCurve] = { 200 | plys match { 201 | case 2 => // mixed 202 | combinationsIterator(OrdinalVector(3, 3)).toList.map(combination => buildVerticalCurve(combination, precision)) 203 | case 1 => // horizontal 204 | combinationsIterator(OrdinalVector(3)).toList.map(combination => buildHorizontalCurve(combination, precision)) 205 | } 206 | } 207 | } 208 | 209 | // two-dimensionals... 210 | 211 | case class FactoryXY(precision: Int) { 212 | def buildCurve(combination: OrdinalVector, precision: Int): ComposedCurve = { 213 | val (leftPrec, rightPrec) = decomposePrecision(combination, precision) 214 | 215 | val rightChild: Composable = DefaultDimensions.createLatitude(rightPrec) 216 | val leftChild: Composable = DefaultDimensions.createLongitude(leftPrec) 217 | 218 | val rawCurve = rawNWayCurve(combination, leftPrec, rightPrec) 219 | 220 | new ComposedCurve(rawCurve, Seq(leftChild, rightChild)) 221 | } 222 | 223 | def getCurves: Seq[ComposedCurve] = 224 | combinationsIterator(OrdinalVector(3)).toList.map(combination => buildCurve(combination, precision)) 225 | } -------------------------------------------------------------------------------- /src/main/scala/org/eichelberger/sfc/Dimensions.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc 2 | 3 | import org.eichelberger.sfc.SpaceFillingCurve.{Composable, OrdinalNumber} 4 | import org.joda.time.{DateTimeZone, DateTime} 5 | import scala.reflect._ 6 | 7 | object Dimensions { 8 | trait DimensionLike[T] { 9 | def eq(a: T, b: T): Boolean 10 | def lt(a: T, b: T): Boolean 11 | def lteq(a: T, b: T): Boolean = !gt(a, b) 12 | def gt(a: T, b: T): Boolean 13 | def gteq(a: T, b: T): Boolean = !lt(a, b) 14 | def compare(a: T, b: T): Int = 15 | if (lt(a, b)) -1 16 | else { 17 | if (gt(a, b)) 1 18 | else 0 19 | } 20 | def add(a: T, b: T): T 21 | def subtract(a: T, b: T): T 22 | def multiply(a: T, b: T): T 23 | def divide(a: T, b: T): T 24 | def toDouble(a: T): Double 25 | def min(a: T, b: T): T = 26 | if (gt(a, b)) b 27 | else a 28 | def floor(a: Double): T 29 | def fromDouble(a: Double): T 30 | } 31 | 32 | implicit object DimensionLikeDouble extends DimensionLike[Double] { 33 | val maxTolerance = 1e-10 34 | def eq(a: Double, b: Double) = Math.abs(a - b) <= maxTolerance 35 | def lt(a: Double, b: Double) = (b - a) > maxTolerance 36 | def gt(a: Double, b: Double) = (a - b) > maxTolerance 37 | def add(a: Double, b: Double) = a + b 38 | def subtract(a: Double, b: Double) = a - b 39 | def multiply(a: Double, b: Double) = a * b 40 | def divide(a: Double, b: Double) = a / b 41 | def toDouble(a: Double) = a 42 | def floor(a: Double) = Math.floor(a) 43 | def fromDouble(a: Double) = a 44 | } 45 | 46 | implicit object DimensionLikeLong extends DimensionLike[Long] { 47 | def eq(a: Long, b: Long) = a == b 48 | def lt(a: Long, b: Long) = a < b 49 | def gt(a: Long, b: Long) = a > b 50 | def add(a: Long, b: Long) = a + b 51 | def subtract(a: Long, b: Long) = a - b 52 | def multiply(a: Long, b: Long) = a * b 53 | def divide(a: Long, b: Long) = a / b 54 | def toDouble(a: Long) = a.toDouble 55 | def floor(a: Double) = Math.floor(a).toLong 56 | def fromDouble(a: Double) = Math.round(a).toLong 57 | } 58 | 59 | implicit object DimensionLikeDate extends DimensionLike[DateTime] { 60 | def eq(a: DateTime, b: DateTime) = a == b 61 | def lt(a: DateTime, b: DateTime) = a.getMillis < b.getMillis 62 | def gt(a: DateTime, b: DateTime) = a.getMillis > b.getMillis 63 | def add(a: DateTime, b: DateTime) = new DateTime(a.getMillis + b.getMillis, DateTimeZone.forID("UTC")) 64 | def subtract(a: DateTime, b: DateTime) = new DateTime(a.getMillis - b.getMillis, DateTimeZone.forID("UTC")) 65 | def multiply(a: DateTime, b: DateTime) = new DateTime(a.getMillis * b.getMillis, DateTimeZone.forID("UTC")) 66 | def divide(a: DateTime, b: DateTime) = new DateTime(a.getMillis / b.getMillis, DateTimeZone.forID("UTC")) 67 | def toDouble(a: DateTime) = a.getMillis.toDouble 68 | def floor(a: Double) = new DateTime(Math.floor(a).toLong, DateTimeZone.forID("UTC")) 69 | def fromDouble(a: Double) = new DateTime(a.toLong, DateTimeZone.forID("UTC")) 70 | } 71 | } 72 | 73 | import Dimensions._ 74 | 75 | trait BareDimension[T] { 76 | def index(value: T): OrdinalNumber 77 | def indexAny(value: Any): OrdinalNumber 78 | def inverseIndex(ordinal: OrdinalNumber): Dimension[T] 79 | } 80 | 81 | case class HalfDimension[T](extreme: T, isExtremeIncluded: Boolean) 82 | 83 | // for now, assume all dimensions are bounded 84 | // (reasonable, unless you're really going to use BigInt or 85 | // some other arbitrary-precision class as the basis) 86 | case class Dimension[T : DimensionLike](name: String, min: T, isMinIncluded: Boolean, max: T, isMaxIncluded: Boolean, precision: OrdinalNumber)(implicit classTag: ClassTag[T]) 87 | extends Composable with BareDimension[T] { 88 | 89 | val basis = implicitly[DimensionLike[T]] 90 | 91 | val span = basis.subtract(max, min) 92 | val numBins = 1L << precision 93 | val penultimateBin = numBins - 1L 94 | 95 | val doubleSpan = basis.toDouble(span) 96 | val doubleNumBins = numBins.toDouble 97 | val doubleMin = basis.toDouble(min) 98 | val doubleMax = basis.toDouble(max) 99 | 100 | val doubleMid = 0.5 * (doubleMin + doubleMax) 101 | 102 | def containsAny(value: Any): Boolean = { 103 | val coercedValue: T = value.asInstanceOf[T] 104 | contains(coercedValue) 105 | } 106 | 107 | def contains(value: T): Boolean = 108 | (basis.gt(value, min) ||(basis.eq(value, min) && isMinIncluded)) && 109 | (basis.lt(value, max) ||(basis.eq(value, max) && isMaxIncluded)) 110 | 111 | def indexAny(value: Any): OrdinalNumber = { 112 | val coercedValue: T = value.asInstanceOf[T] 113 | index(coercedValue) 114 | } 115 | 116 | def index(value: T): OrdinalNumber = { 117 | val doubleValue = basis.toDouble(value) 118 | Math.min( 119 | Math.floor(doubleNumBins * (doubleValue - doubleMin) / doubleSpan), 120 | penultimateBin 121 | ).toLong 122 | } 123 | 124 | def getLowerBound(ordinal: OrdinalNumber): HalfDimension[T] = { 125 | /* 126 | val minimum = numeric.toDouble(dim.min) + numeric.toDouble(dim.span) * ordinal.toDouble / size.toDouble 127 | val incMin = dim.isMinIncluded || (ordinal == 0L) 128 | */ 129 | val x = basis.fromDouble(doubleMin + doubleSpan * ordinal.toDouble / doubleNumBins) 130 | val included = isMinIncluded || (ordinal == 0L) 131 | HalfDimension[T](x, included) 132 | } 133 | 134 | def getUpperBound(ordinal: OrdinalNumber): HalfDimension[T] = { 135 | /* 136 | val max = numeric.toDouble(dim.min) + numeric.toDouble(dim.span) * (1L + ordinal.toDouble) / size.toDouble 137 | val incMax = dim.isMaxIncluded && (ordinal == (size - 1L)) 138 | */ 139 | val x = basis.fromDouble(doubleMin + doubleSpan * (1L + ordinal.toDouble) / doubleNumBins) 140 | val included = isMinIncluded || (ordinal == 0L) 141 | HalfDimension[T](x, included) 142 | } 143 | 144 | def inverseIndex(ordinal: OrdinalNumber): Dimension[T] = { 145 | val lowerBound = getLowerBound(ordinal) 146 | val upperBound = getUpperBound(ordinal) 147 | new Dimension[T](name, lowerBound.extreme, lowerBound.isExtremeIncluded, upperBound.extreme, upperBound.isExtremeIncluded, 1L) 148 | } 149 | 150 | override def toString: String = 151 | (if (isMinIncluded) "[" else "(") + 152 | min.toString + ", " + max.toString + 153 | (if (isMaxIncluded) "]" else ")") + 154 | "@" + precision.toString 155 | } 156 | 157 | // meant to represent a contiguous subset of bits from a 158 | // larger, parent dimension 159 | class SubDimension[T](val name: String, encoder: T => OrdinalNumber, parentPrecision: OrdinalNumber, bitsToSkip: OrdinalNumber, val precision: OrdinalNumber) 160 | extends Composable with BareDimension[T] { 161 | 162 | val numBitsRightToLose = parentPrecision - bitsToSkip - precision 163 | 164 | val mask = (1L << precision) - 1L 165 | 166 | def index(value: T): OrdinalNumber = 167 | (encoder(value) >>> numBitsRightToLose) & mask 168 | 169 | def indexAny(value: Any): OrdinalNumber = { 170 | val coercedValue: T = value.asInstanceOf[T] 171 | index(coercedValue) 172 | } 173 | 174 | def inverseIndex(ordinal: OrdinalNumber): Dimension[T] = 175 | throw new UnsupportedOperationException("The 'inverseIndex' method is ill defined for a SubDimension.") 176 | } 177 | 178 | object DefaultDimensions { 179 | import Dimensions._ 180 | 181 | val dimLongitude = Dimension[Double]("x", -180.0, isMinIncluded = true, +180.0, isMaxIncluded = true, 18L) 182 | val dimLatitude = Dimension[Double]("y", -90.0, isMinIncluded = true, +90.0, isMaxIncluded = true, 17L) 183 | 184 | val MinNearDate = new DateTime(1900, 1, 1, 0, 0, 0, DateTimeZone.forID("UTC")) 185 | val MaxNearDate = new DateTime(2100, 12, 31, 23, 59, 59, DateTimeZone.forID("UTC")) 186 | val dimNearTime = Dimension("t", MinNearDate, isMinIncluded = true, MaxNearDate, isMaxIncluded = true, 15L) 187 | 188 | val MinYYYYDate = new DateTime( 0, 1, 1, 0, 0, 0, DateTimeZone.forID("UTC")) 189 | val MaxYYYYDate = new DateTime(9999, 12, 31, 23, 59, 59, DateTimeZone.forID("UTC")) 190 | val dimYYYYTime = Dimension("t", MinYYYYDate, isMinIncluded = true, MaxYYYYDate, isMaxIncluded = true, 20L) 191 | 192 | val MinDate = new DateTime(Long.MinValue >> 2L, DateTimeZone.forID("UTC")) 193 | val MaxDate = new DateTime(Long.MaxValue >> 2L, DateTimeZone.forID("UTC")) 194 | val dimTime = Dimension("t", MinDate, isMinIncluded = true, MaxDate, isMaxIncluded = true, 60L) 195 | 196 | def createLongitude(atPrecision: OrdinalNumber): Dimension[Double] = 197 | dimLongitude.copy(precision = atPrecision) 198 | 199 | def createLatitude(atPrecision: OrdinalNumber): Dimension[Double] = 200 | dimLatitude.copy(precision = atPrecision) 201 | 202 | def createDateTime(atPrecision: OrdinalNumber): Dimension[DateTime] = 203 | dimTime.copy(precision = atPrecision) 204 | 205 | def createYYYYDateTime(atPrecision: OrdinalNumber): Dimension[DateTime] = 206 | dimYYYYTime.copy(precision = atPrecision) 207 | 208 | def createNearDateTime(atPrecision: OrdinalNumber): Dimension[DateTime] = 209 | dimNearTime.copy(precision = atPrecision) 210 | 211 | def createNearDateTime(minDate: DateTime, maxDate: DateTime, atPrecision: OrdinalNumber): Dimension[DateTime] = 212 | dimNearTime.copy(min = minDate, max = maxDate, precision = atPrecision) 213 | 214 | def createDimension[T](name: String, minimum: T, maximum: T, precision: OrdinalNumber)(implicit dimLike: DimensionLike[T], ctag: ClassTag[T]): Dimension[T] = 215 | Dimension[T](name, minimum, isMinIncluded = true, maximum, isMaxIncluded = true, precision) 216 | 217 | class IdentityDimension(name: String, precision: OrdinalNumber) 218 | extends Dimension[Long](name, 0L, isMinIncluded=true, (1L << precision) - 1L, isMaxIncluded=true, precision) { 219 | 220 | override def index(value: OrdinalNumber): OrdinalNumber = value 221 | 222 | override def indexAny(value: Any): OrdinalNumber = value.asInstanceOf[OrdinalNumber] 223 | 224 | override def inverseIndex(ordinal: OrdinalNumber): Dimension[Long] = 225 | Dimension[Long]("dummy", ordinal, isMinIncluded=true, ordinal, isMaxIncluded=true, 0) 226 | } 227 | 228 | def createIdentityDimension(name: String, precision: OrdinalNumber): Dimension[Long] = 229 | new IdentityDimension(name, precision) 230 | 231 | def createIdentityDimension(precision: OrdinalNumber): Dimension[Long] = 232 | createIdentityDimension(s"Identity_$precision", precision) 233 | } 234 | -------------------------------------------------------------------------------- /src/test/scala/org/eichelberger/sfc/CompactHilbertCurveTest.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc 2 | 3 | import com.typesafe.scalalogging.slf4j.LazyLogging 4 | import org.eichelberger.sfc.CompactHilbertCurve.Mask 5 | import org.eichelberger.sfc.SpaceFillingCurve._ 6 | import org.eichelberger.sfc.planners.{OffSquareQuadTreePlanner, ZCurvePlanner} 7 | import org.eichelberger.sfc.utils.Timing 8 | import org.junit.runner.RunWith 9 | import org.specs2.mutable.Specification 10 | import org.specs2.runner.JUnitRunner 11 | 12 | @RunWith(classOf[JUnitRunner]) 13 | class CompactHilbertCurveTest extends Specification with GenericCurveValidation with LazyLogging { 14 | sequential 15 | 16 | "static functions" should { 17 | "extract bits" >> { 18 | val bits = Seq(1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1) 19 | val bitString = bits.reverse.map(_.toString).mkString("") 20 | val num = java.lang.Integer.parseInt(bitString, 2) 21 | for (bit <- 0 until bits.size) { 22 | println(s"[extract bits] num $num bit $bit known ${bits(bit)} computed ${bitAt(num, bit)}") 23 | bitAt(num, bit) must equalTo(bits(bit)) 24 | } 25 | 26 | // degenerate test 27 | 1 must equalTo(1) 28 | } 29 | 30 | "set individual bits" >> { 31 | // 000...00011010 32 | (0 to 29).foreach { 33 | case 1 => 34 | setBitAt(26L, 1, 1) must equalTo(26L) 35 | setBitAt(26L, 1, 0) must equalTo(24L) 36 | case 3 => 37 | setBitAt(26L, 3, 1) must equalTo(26L) 38 | setBitAt(26L, 3, 0) must equalTo(18L) 39 | case 4 => 40 | setBitAt(26L, 4, 1) must equalTo(26L) 41 | setBitAt(26L, 4, 0) must equalTo(10L) 42 | case i => 43 | (26L ^ setBitAt(26L, i, 1)) must equalTo(1L << i) 44 | setBitAt(26L, i, 0) must equalTo(26L) 45 | } 46 | 47 | // degenerate test 48 | 1 must equalTo(1) 49 | } 50 | 51 | "onBitsIn must work" >> { 52 | (1 to 63).foreach { i => { 53 | onBitsIn((1L << i) - 1L) must equalTo(i) 54 | }} 55 | 56 | onBitsIn(0L) must equalTo(0) 57 | onBitsIn(1L) must equalTo(1) 58 | onBitsIn(256L) must equalTo(1) 59 | onBitsIn(65536L) must equalTo(1) 60 | onBitsIn(3L) must equalTo(2) 61 | onBitsIn(5L) must equalTo(2) 62 | onBitsIn(65537L) must equalTo(2) 63 | } 64 | 65 | "instantiate from variable-length lists" >> { 66 | CompactHilbertCurve(2) must not beNull; 67 | CompactHilbertCurve(2, 3) must not beNull; 68 | CompactHilbertCurve(2, 3, 4) must not beNull 69 | } 70 | 71 | "round-trip Gray (en|de)code" >> { 72 | val ch = CompactHilbertCurve(3, 2) 73 | 74 | for(w <- 31 to 0 by -1) { 75 | val t = ch.grayCode(w) 76 | val w2 = ch.inverseGrayCode(t) 77 | println(s"[Gray (en|de)coding] w $w t $t w2 $w2") 78 | w must equalTo(w2) 79 | } 80 | 81 | // degenerate test 82 | 1 must equalTo(1) 83 | } 84 | 85 | "barrel-shift correctly" >> { 86 | val ch = CompactHilbertCurve(1, 1, 1, 1, 1, 1) 87 | 88 | // to the left... 89 | ch.barrelShiftLeft(13L, 0L) must equalTo(13L) 90 | ch.barrelShiftLeft(13L, 1L) must equalTo(26L) 91 | ch.barrelShiftLeft(13L, 2L) must equalTo(52L) 92 | ch.barrelShiftLeft(13L, 3L) must equalTo(41L) 93 | ch.barrelShiftLeft(13L, 4L) must equalTo(19L) 94 | ch.barrelShiftLeft(13L, 5L) must equalTo(38L) 95 | ch.barrelShiftLeft(13L, 6L) must equalTo(13L) 96 | 97 | // to the right... 98 | ch.barrelShiftRight(13L, 0L) must equalTo(13L) 99 | ch.barrelShiftRight(13L, 1L) must equalTo(38L) 100 | ch.barrelShiftRight(13L, 2L) must equalTo(19L) 101 | ch.barrelShiftRight(13L, 3L) must equalTo(41L) 102 | ch.barrelShiftRight(13L, 4L) must equalTo(52L) 103 | ch.barrelShiftRight(13L, 5L) must equalTo(26L) 104 | ch.barrelShiftRight(13L, 6L) must equalTo(13L) 105 | } 106 | 107 | "Gray-code rank" >> { 108 | def gTest(ch: CompactHilbertCurve, mu: Mask, gc: Long, iExp: Long, gcrExp: Long): Boolean = { 109 | val i = ch.inverseGrayCode(gc) 110 | val gcr = ch.grayCodeRank(mu, i) 111 | println(s"[Gray test] i $i ($iExp), gc $gc, gcr $gcr ($gcrExp)") 112 | 113 | i must equalTo(iExp) 114 | gcr must equalTo(gcrExp) 115 | 116 | // if you get this far, everything is fine 117 | true 118 | } 119 | 120 | "example #1" >> { 121 | // examples pulled from the CS-2006-07 paper 122 | // in which n=6, mu=010110b 123 | 124 | // (NB: One correction is that the paper suggests that the gc-1(20) = 16, 125 | // when it appears to mean gc-1(24) = 16.) 126 | 127 | val ch = CompactHilbertCurve(1, 1, 1, 1, 1, 1) 128 | val mu = Mask(22L, 3) 129 | 130 | gTest(ch, mu, 8, 15, 3) 131 | gTest(ch, mu, 10, 12, 2) 132 | gTest(ch, mu, 12, 8, 0) 133 | gTest(ch, mu, 14, 11, 1) 134 | gTest(ch, mu, 24, 16, 4) 135 | gTest(ch, mu, 26, 19, 5) 136 | gTest(ch, mu, 28, 23, 7) 137 | gTest(ch, mu, 30, 20, 6) 138 | 139 | 1 must equalTo(1) 140 | } 141 | 142 | "example #2" >> { 143 | // examples pulled from https://web.cs.dal.ca/~chamilto/hilbert/ipl.pdf 144 | // n=4, 2 free bits 145 | val ch = CompactHilbertCurve(1, 1, 1, 1) 146 | val mu = Mask(9L, 2) 147 | 148 | gTest(ch, mu, 4, 7, 1) 149 | gTest(ch, mu, 5, 6, 0) 150 | gTest(ch, mu, 12, 8, 2) 151 | gTest(ch, mu, 13, 9, 3) 152 | } 153 | } 154 | 155 | "T and T-1 should be inverses" >> { 156 | val ch = CompactHilbertCurve(2, 2) 157 | 158 | for (e <- 0 to 1; d <- 0 to 1; b <- 0 to 3) { 159 | val Tb = ch.T(e, d)(b) 160 | val b2 = ch.invT(e, d)(Tb) 161 | println(s"[T/T-1] T($e, $d)($b) = $Tb -> T-1($e, $d)($Tb) = $b2") 162 | b2 must equalTo(b) 163 | } 164 | 165 | 1 must equalTo(1) 166 | } 167 | } 168 | 169 | def curveName = "CompactHilbertCurve" 170 | 171 | def createCurve(precisions: OrdinalNumber*): SpaceFillingCurve = 172 | CompactHilbertCurve(precisions.toOrdinalVector) 173 | 174 | "Hilbert range planner" should { 175 | "identify ranges from coordinate queries on square spaces" >> { 176 | val sfc = CompactHilbertCurve(OrdinalVector(4, 4)) 177 | val query = Query(Seq( 178 | OrdinalRanges(OrdinalPair(1, 5)), 179 | OrdinalRanges(OrdinalPair(6, 12)) 180 | )) 181 | val ranges = sfc.getRangesCoveringQuery(query).toList 182 | ranges.zipWithIndex.foreach { 183 | case (range, i) => 184 | println(s"[h-curve square query ranges $query] $i: $range") 185 | } 186 | 187 | ranges must equalTo(Seq( 188 | OrdinalPair(22, 27), 189 | OrdinalPair(36, 39), 190 | OrdinalPair(202, 203), 191 | OrdinalPair(216, 233), 192 | OrdinalPair(237, 238), 193 | OrdinalPair(243, 245) 194 | )) 195 | } 196 | 197 | "identify ranges from coordinate queries on non-square spaces" >> { 198 | val sfc = CompactHilbertCurve(OrdinalVector(3, 5)) 199 | val query = Query(Seq( 200 | OrdinalRanges(OrdinalPair(5, 7)), 201 | OrdinalRanges(OrdinalPair(7, 10)) 202 | )) 203 | val ranges = sfc.getRangesCoveringQuery(query).toList 204 | ranges.zipWithIndex.foreach { 205 | case (range, i) => 206 | println(s"[h-curve off-square query ranges $query] $i: $range") 207 | } 208 | 209 | ranges must equalTo(Seq( 210 | OrdinalPair(42, 44), 211 | OrdinalPair(113, 114), 212 | OrdinalPair(119, 120), 213 | OrdinalPair(123, 127) 214 | )) 215 | } 216 | } 217 | 218 | "direction-helper functions" should { 219 | "ensure g(i) is symmetric" >> { 220 | val h = CompactHilbertCurve(2, 2, 2) 221 | for (i <- 0 to 31) { 222 | val g0 = h.g(i) 223 | val g1 = h.g(62 - i) 224 | println(s"[g(i) VALIDATION] g($i) = $g0, g(${62-i}) = $g1") 225 | g0 must equalTo(g1) 226 | } 227 | 228 | 1 must equalTo(1) 229 | } 230 | 231 | "ensure d(i) is symmetric" >> { 232 | val h = CompactHilbertCurve(2, 2, 2) 233 | for (i <- 0 to 31) { 234 | val d0 = h.nextDim(i) 235 | val d1 = h.nextDim(63 - i) 236 | println(s"[d(w) VALIDATION] d($i) = $d0, d(${63-i}) = $d1") 237 | d0 must equalTo(d1) 238 | } 239 | 240 | 1 must equalTo(1) 241 | } 242 | } 243 | 244 | "3D Hilbert curves" should { 245 | def testConsecutive(h: CompactHilbertCurve, failFast: Boolean = true): Boolean = { 246 | val testName = h.name + h.precisions.toSeq.mkString("(", ",", ")") 247 | println(s"Evaluating consecutive indexes for $testName...") 248 | 249 | var lastCell = h.inverseIndex(0) 250 | var idx = 1L 251 | var numFailures = 0 252 | while (idx < h.size) { 253 | val cell = h.inverseIndex(idx) 254 | val dists = cell.zipWith(lastCell).map { 255 | case (a, b) => Math.abs(a - b) 256 | } 257 | val dist = dists.sum 258 | if (dist > 1) 259 | println(Seq( 260 | testName, 261 | idx - 1, 262 | "[" + lastCell + "]", 263 | idx, 264 | "[" + cell + "]", 265 | dist, 266 | dists.mkString("[", ", ", "]") 267 | ).mkString("\t")) 268 | if (failFast) 269 | require(dist == 1, s"Step between ${idx-1} and $idx yielded distance $dist <- $dists") 270 | if (dist != 1) numFailures += 1 271 | idx = idx + 1L 272 | lastCell = cell 273 | } 274 | numFailures == 0 275 | } 276 | 277 | "never step more than 1 unit between successive cells in square cubes" >> { 278 | for (i <- 1 to 4) { 279 | testConsecutive(CompactHilbertCurve(i, i), failFast = false) must beTrue 280 | testConsecutive(CompactHilbertCurve(i, i, i), failFast = false) must beTrue 281 | testConsecutive(CompactHilbertCurve(i, i, i, i), failFast = false) must beTrue 282 | testConsecutive(CompactHilbertCurve(i, i, i, i, i), failFast = false) must beTrue 283 | } 284 | 285 | 1 must equalTo(1) 286 | }//.pendingUntilFixed("This is very strange.") //@TODO EXTREMELY HIGH PRIORITY! 287 | } 288 | 289 | //@TODO restore! 290 | // "compact Hilbert space-filling curves" should { 291 | // "satisfy the ordering constraints" >> { 292 | // timeTestOrderings() must beTrue 293 | // } 294 | // } 295 | } 296 | -------------------------------------------------------------------------------- /src/main/scala/org/eichelberger/sfc/utils/CompositionWKT.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.utils 2 | 3 | import org.eichelberger.sfc.ComposedCurve 4 | import org.eichelberger.sfc.SpaceFillingCurve._ 5 | 6 | import scala.collection.immutable.HashMap 7 | import scala.collection.mutable 8 | 9 | object CompositionWKT { 10 | case class ContiguousPolygon(curve: ComposedCurve, sparse: HashMap[OrdinalVector,OrdinalNumber]) { 11 | def isAdjacent(coords: OrdinalVector, idx: OrdinalNumber): Boolean = { 12 | sparse.contains(OrdinalVector(coords(0) - 1, coords(1))) || 13 | sparse.contains(OrdinalVector(coords(0) + 1, coords(1))) || 14 | sparse.contains(OrdinalVector(coords(0), coords(1) - 1)) || 15 | sparse.contains(OrdinalVector(coords(0), coords(1) + 1)) 16 | } 17 | 18 | def +(item: (OrdinalVector, OrdinalNumber)): ContiguousPolygon = 19 | ContiguousPolygon(curve, sparse + (item._1 -> item._2)) 20 | 21 | def minY = sparse.map { case (k,v) => k(1) }.min 22 | def maxY = sparse.map { case (k,v) => k(1) }.max 23 | def minX = sparse.map { case (k,v) => k(0) }.min 24 | def maxX = sparse.map { case (k,v) => k(0) }.max 25 | 26 | def upperLeft: OrdinalVector = { 27 | require(sparse.size > 0, "The ContiguousPolygon must contain at least one cell") 28 | var x = minX 29 | var y = maxY 30 | while (!sparse.contains(OrdinalVector(x, y))) { 31 | x = x + 1 32 | if (x > maxX) { 33 | x = minX 34 | y = y - 1 35 | } 36 | } 37 | OrdinalVector(x, y) 38 | } 39 | 40 | def wkt: String = { 41 | // find the left-most cell of the first row with data 42 | var UL = upperLeft 43 | var current = OrdinalVector(UL(0), UL(1), 0) 44 | var seen = mutable.HashSet[OrdinalVector]() 45 | val sb = new StringBuilder() 46 | val points = mutable.ArrayBuffer[String]() 47 | 48 | // initialize the starting point 49 | val cell: Cell = curve.indexToCell(sparse(OrdinalVector(current(0), current(1)))) 50 | points += cell.dimensions(0).min + " " + cell.dimensions(1).max 51 | 52 | // Theseus and the minotaur ensues 53 | while (!seen.contains(current)) { 54 | val cell: Cell = curve.indexToCell(sparse(OrdinalVector(current(0), current(1)))) 55 | current(2) match { 56 | case 0 | 5 => // top L->R 57 | points += cell.dimensions(0).max + " " + cell.dimensions(1).max 58 | case 1 | 6 => // right T->B 59 | points += cell.dimensions(0).max + " " + cell.dimensions(1).min 60 | case 2 | 7 => // bottom L->R 61 | points += cell.dimensions(0).min + " " + cell.dimensions(1).min 62 | case 3 | 4 => // left T->B 63 | points += cell.dimensions(0).min + " " + cell.dimensions(1).max 64 | } 65 | seen.add(OrdinalVector(current(0), current(1), current(2))) 66 | current(2) match { 67 | case 0 => // moving right along the top edge 68 | if (sparse.contains(OrdinalVector(current(0) + 1, current(1) + 1))) { 69 | // move up 70 | current = OrdinalVector(current(0) + 1, current(1) + 1, 3) 71 | } else if (sparse.contains(OrdinalVector(current(0) + 1, current(1)))) { 72 | // move right 73 | current = OrdinalVector(current(0) + 1, current(1), 0) 74 | } else { 75 | // move down 76 | current = OrdinalVector(current(0), current(1), 1) 77 | } 78 | case 1 => // moving down the right edge 79 | if (sparse.contains(OrdinalVector(current(0) + 1, current(1) - 1))) { 80 | // move right 81 | current = OrdinalVector(current(0) + 1, current(1) - 1, 0) 82 | } else if (sparse.contains(OrdinalVector(current(0), current(1) - 1))) { 83 | // move down 84 | current = OrdinalVector(current(0), current(1) - 1, 1) 85 | } else { 86 | // move left 87 | current = OrdinalVector(current(0), current(1), 2) 88 | } 89 | case 2 => // moving left along the bottom edge 90 | if (sparse.contains(OrdinalVector(current(0) - 1, current(1) - 1))) { 91 | // move down 92 | current = OrdinalVector(current(0) - 1, current(1) - 1, 1) 93 | } else if (sparse.contains(OrdinalVector(current(0) - 1, current(1)))) { 94 | // move left 95 | current = OrdinalVector(current(0) - 1, current(1), 2) 96 | } else { 97 | // move up 98 | current = OrdinalVector(current(0), current(1), 3) 99 | } 100 | case 3 => // moving up the left edge 101 | if (sparse.contains(OrdinalVector(current(0) - 1, current(1) + 1))) { 102 | // move left 103 | current = OrdinalVector(current(0) - 1, current(1) + 1, 2) 104 | } else if (sparse.contains(OrdinalVector(current(0), current(1) + 1))) { 105 | // move up 106 | current = OrdinalVector(current(0), current(1) + 1, 3) 107 | } else { 108 | // move left 109 | current = OrdinalVector(current(0), current(1), 0) 110 | } 111 | case 4 => // moving left along the top edge 112 | if (sparse.contains(OrdinalVector(current(0) - 1, current(1) + 1))) { 113 | // move up 114 | current = OrdinalVector(current(0) - 1, current(1) + 1, 5) 115 | } else if (sparse.contains(OrdinalVector(current(0) - 1, current(1)))) { 116 | // move left 117 | current = OrdinalVector(current(0) - 1, current(1), 4) 118 | } else { 119 | // move up 120 | current = OrdinalVector(current(0), current(1), 7) 121 | } 122 | case 5 => // moving up the right edge 123 | if (sparse.contains(OrdinalVector(current(0) + 1, current(1) + 1))) { 124 | // move right 125 | current = OrdinalVector(current(0) + 1, current(1) + 1, 6) 126 | } else if (sparse.contains(OrdinalVector(current(0), current(1) + 1))) { 127 | // move up 128 | current = OrdinalVector(current(0), current(1) + 1, 5) 129 | } else { 130 | // move left 131 | current = OrdinalVector(current(0), current(1), 4) 132 | } 133 | case 6 => // moving right along the bottom edge 134 | if (sparse.contains(OrdinalVector(current(0) + 1, current(1) - 1))) { 135 | // move down 136 | current = OrdinalVector(current(0) + 1, current(1) - 1, 1) 137 | } else if (sparse.contains(OrdinalVector(current(0) + 1, current(1)))) { 138 | // move right 139 | current = OrdinalVector(current(0) + 1, current(1), 6) 140 | } else { 141 | // move up 142 | current = OrdinalVector(current(0), current(1), 5) 143 | } 144 | case 7 => // moving down the left edge 145 | if (sparse.contains(OrdinalVector(current(0) - 1, current(1) - 1))) { 146 | // move left 147 | current = OrdinalVector(current(0) - 1, current(1) - 1, 4) 148 | } else if (sparse.contains(OrdinalVector(current(0), current(1) - 1))) { 149 | // move down 150 | current = OrdinalVector(current(0), current(1) - 1, 7) 151 | } else { 152 | // move right 153 | current = OrdinalVector(current(0), current(1), 6) 154 | } 155 | case _ => throw new Exception("Fix this; bad direction.") 156 | } 157 | } 158 | 159 | points.mkString("((", ", ", "))") 160 | } 161 | } 162 | 163 | case class BBox(x0: Double, y0: Double, x1: Double, y1: Double) { 164 | def expandToInclude(bbox: BBox): BBox = BBox( 165 | Math.min(x0, bbox.x0), 166 | Math.min(y0, bbox.y0), 167 | Math.max(x1, bbox.x1), 168 | Math.max(y1, bbox.y1) 169 | ) 170 | 171 | def areaDegrees = Math.abs((x1 - x0) * (y1 - y0)) 172 | 173 | def wkt: String = 174 | s"POLYGON(($x0 $y0, $x0 $y1, $x1 $y1, $x1 $y0, $x0 $y0))" 175 | } 176 | 177 | case class RichRange(order: OrdinalNumber, range: OrdinalPair, wkt: String) 178 | 179 | implicit class CellWKT(cell: Cell) { 180 | require(cell.size == 2, s"CellWKT is only valid for 2D curves") 181 | 182 | // assumes (X, Y) order 183 | def bbox: BBox = BBox( 184 | cell.dimensions(0).doubleMin, 185 | cell.dimensions(1).doubleMin, 186 | cell.dimensions(0).doubleMax, 187 | cell.dimensions(1).doubleMax 188 | ) 189 | 190 | def wkt: String = bbox.wkt 191 | } 192 | 193 | implicit class ComposedCurveWKT(composedCurve: ComposedCurve) { 194 | require(composedCurve.n == 2, s"ComposedCurveWKT is only valid for 2D curves") 195 | 196 | def getMultipolygonFromRange(range: OrdinalPair): String = { 197 | val polys = collection.mutable.ArrayBuffer[ContiguousPolygon]() 198 | 199 | // first point 200 | val coord: OrdinalVector = composedCurve.inverseIndex(range.min) 201 | polys += ContiguousPolygon(composedCurve, HashMap[OrdinalVector,OrdinalNumber](coord -> range.min)) 202 | 203 | // accumulate subsequent points 204 | for (idx <- (range.min + 1L) to range.max) { 205 | val coord = composedCurve.inverseIndex(idx) 206 | var foundSlot = -1 207 | for (slot <- 0 until polys.size) { 208 | if (polys(slot).isAdjacent(coord, idx)) foundSlot = slot 209 | } 210 | if (foundSlot > -1) polys(foundSlot) = polys(foundSlot) + (coord, idx) 211 | else polys += ContiguousPolygon(composedCurve, HashMap[OrdinalVector,OrdinalNumber](coord -> idx)) 212 | } 213 | 214 | polys.size match { 215 | case 0 => throw new Exception("Must have encountered at least one polygon") 216 | case 1 => "POLYGON" + polys.head.wkt 217 | case _ => "MULTIPOLYGON(" + polys.map(_.wkt).mkString(", ") + ")" 218 | } 219 | } 220 | 221 | def bboxFromRange(range: OrdinalPair): BBox = { 222 | // lazy: iterate over all cells 223 | var bbox = composedCurve.indexToCell(range.min).bbox 224 | var idx = range.min + 1L 225 | while (idx <= range.max) { 226 | bbox = bbox.expandToInclude(composedCurve.indexToCell(idx).bbox) 227 | idx = idx + 1L 228 | } 229 | bbox 230 | } 231 | 232 | def getQueryResultRichRanges(cell: Cell): Iterator[RichRange] = { 233 | val itr = composedCurve.getRangesCoveringCell(cell) 234 | itr.zipWithIndex.map { 235 | case (range, i) => 236 | RichRange(i, range, getMultipolygonFromRange(range)) 237 | } 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/test/scala/org/eichelberger/sfc/utils/RenderSourceTest.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.utils 2 | 3 | import java.io.{BufferedOutputStream, FileOutputStream, PrintStream} 4 | 5 | import com.typesafe.scalalogging.slf4j.LazyLogging 6 | import org.eichelberger.sfc.SpaceFillingCurve.{OrdinalVector, _} 7 | import org.eichelberger.sfc._ 8 | import org.eichelberger.sfc.examples.composition.contrast.BaseCurves 9 | import org.junit.runner.RunWith 10 | import org.specs2.mutable.Specification 11 | import org.specs2.runner.JUnitRunner 12 | 13 | @RunWith(classOf[JUnitRunner]) 14 | class RenderSourceTest extends Specification with LazyLogging { 15 | sequential 16 | 17 | "to-screen renderers" should { 18 | "be able to dump a 2D Compact-Hilbert-curve to screen" >> { 19 | val sfc = new CompactHilbertCurve(OrdinalVector(4, 4)) with RenderSource { 20 | def getCurveName = "Compact-Hilbert" 21 | } 22 | val screenTarget = new ScreenRenderTarget 23 | sfc.render(screenTarget) 24 | 25 | 1 must equalTo(1) 26 | } 27 | 28 | "be able to dump a 3D Compact-Hilbert-curve to screen" >> { 29 | val sfc = new CompactHilbertCurve(OrdinalVector(3, 3, 3)) with RenderSource { 30 | def getCurveName = "Compact-Hilbert" 31 | } 32 | val screenTarget = new ScreenRenderTarget 33 | sfc.render(screenTarget) 34 | 35 | 1 must equalTo(1) 36 | } 37 | 38 | "be able to dump a 2D Z-curve to screen" >> { 39 | val sfc = new ZCurve(OrdinalVector(4, 4)) with RenderSource { 40 | def getCurveName = "Z" 41 | } 42 | val screenTarget = new ScreenRenderTarget 43 | sfc.render(screenTarget) 44 | 45 | 1 must equalTo(1) 46 | } 47 | 48 | "be able to dump a 3D Z-curve to screen" >> { 49 | val sfc = new ZCurve(OrdinalVector(3, 3, 3)) with RenderSource { 50 | def getCurveName = "Z" 51 | } 52 | val screenTarget = new ScreenRenderTarget 53 | sfc.render(screenTarget) 54 | 55 | 1 must equalTo(1) 56 | } 57 | 58 | "be able to dump a 2D rowmajor curve to screen" >> { 59 | val sfc = new RowMajorCurve(OrdinalVector(4, 4)) with RenderSource { 60 | def getCurveName = "Row-major" 61 | } 62 | val screenTarget = new ScreenRenderTarget 63 | sfc.render(screenTarget) 64 | 65 | 1 must equalTo(1) 66 | } 67 | 68 | "be able to dump a 3D Z-curve to screen" >> { 69 | val sfc = new RowMajorCurve(OrdinalVector(3, 3, 3)) with RenderSource { 70 | def getCurveName = "Row-major" 71 | } 72 | val screenTarget = new ScreenRenderTarget 73 | sfc.render(screenTarget) 74 | 75 | 1 must equalTo(1) 76 | } 77 | 78 | "be able to render small curves for Graphviz" >> { 79 | def dotTarget(fileName: String, precision: OrdinalNumber) = new GraphvizRenderTarget() { 80 | val hue: Float = (39.0 / 255.0).toFloat 81 | override val pw: PrintStream = 82 | new java.io.PrintStream(new BufferedOutputStream(new FileOutputStream(s"/tmp/$fileName.dot"))) 83 | override val drawNumbers = true 84 | override val drawArrows = true 85 | override val cellShadingRamp = Option(ShadeRamp( 86 | new ShadeRampEndpoint(0L, hue, 0.0f, 0.1f), 87 | new ShadeRampEndpoint(1L << precision, hue, 0.0f, 0.8f) 88 | )) 89 | override def afterRendering(sfc: RenderSource): Unit = { 90 | super.afterRendering(sfc) 91 | pw.close() 92 | } 93 | } 94 | 95 | new CompactHilbertCurve(OrdinalVector(2, 2, 2)) with RenderSource { def getCurveName = "Compact Hilbert" }.render(dotTarget("h(2,2,2)", 8)) 96 | 97 | new ZCurve(OrdinalVector(4, 4)) with RenderSource { def getCurveName = "Z" }.render(dotTarget("z(4,4)", 8)) 98 | new CompactHilbertCurve(OrdinalVector(4, 4)) with RenderSource { def getCurveName = "Compact Hilbert" }.render(dotTarget("h(4,4)", 8)) 99 | new RowMajorCurve(OrdinalVector(4, 4)) with RenderSource { def getCurveName = "Row-major" }.render(dotTarget("r(4,4)", 8)) 100 | 101 | new ZCurve(OrdinalVector(3, 5)) with RenderSource { def getCurveName = "Z" }.render(dotTarget("z(3,5)", 8)) 102 | new CompactHilbertCurve(OrdinalVector(3, 5)) with RenderSource { def getCurveName = "Compact Hilbert" }.render(dotTarget("h(3,5)", 8)) 103 | new RowMajorCurve(OrdinalVector(3, 5)) with RenderSource { def getCurveName = "Row-major" }.render(dotTarget("r(3,5)", 8)) 104 | 105 | 1 must equalTo(1) 106 | } 107 | 108 | "be able to render small curves for POV-Ray" >> { 109 | def povTarget(fileName: String) = new PovrayRenderTarget() { 110 | override val pw: PrintStream = 111 | new java.io.PrintStream(new BufferedOutputStream(new FileOutputStream(s"/tmp/$fileName.inc"))) 112 | override def afterRendering(sfc: RenderSource): Unit = { 113 | super.afterRendering(sfc) 114 | pw.close() 115 | } 116 | } 117 | 118 | // square cubes 119 | new RowMajorCurve(OrdinalVector(4, 4, 4)) with RenderSource { override val useSlices = false; def getCurveName = "R444" }.render(povTarget("r(4,4,4)")) 120 | new ZCurve(OrdinalVector(4, 4, 4)) with RenderSource { override val useSlices = false; def getCurveName = "Z444" }.render(povTarget("z(4,4,4)")) 121 | new CompactHilbertCurve(OrdinalVector(4, 4, 4)) with RenderSource { override val useSlices = false; def getCurveName = "H444" }.render(povTarget("h(4,4,4)")) 122 | 123 | // oblong cubes 124 | new RowMajorCurve(OrdinalVector(4, 4, 5)) with RenderSource { override val useSlices = false; def getCurveName = "R445" }.render(povTarget("r(4,4,5)")) 125 | new ZCurve(OrdinalVector(4, 4, 5)) with RenderSource { override val useSlices = false; def getCurveName = "Z445" }.render(povTarget("z(4,4,5)")) 126 | new CompactHilbertCurve(OrdinalVector(4, 4, 5)) with RenderSource { override val useSlices = false; def getCurveName = "H445" }.render(povTarget("h(4,4,5)")) 127 | 128 | 1 must equalTo(1) 129 | } 130 | 131 | "be able to render small curves to CSV" >> { 132 | def csvTarget(fileName: String) = new CSVRenderTarget() { 133 | override val pw: PrintStream = 134 | new java.io.PrintStream(new BufferedOutputStream(new FileOutputStream(s"/tmp/$fileName.csv"))) 135 | override def afterRendering(sfc: RenderSource): Unit = { 136 | super.afterRendering(sfc) 137 | pw.close() 138 | } 139 | } 140 | 141 | // square cubes 142 | new RowMajorCurve(OrdinalVector(4, 4, 4)) with RenderSource { override val useSlices = false; def getCurveName = "R444" }.render(csvTarget("r(4,4,4)")) 143 | new ZCurve(OrdinalVector(4, 4, 4)) with RenderSource { override val useSlices = false; def getCurveName = "Z444" }.render(csvTarget("z(4,4,4)")) 144 | new CompactHilbertCurve(OrdinalVector(4, 4, 4)) with RenderSource { override val useSlices = false; def getCurveName = "H444" }.render(csvTarget("h(4,4,4)")) 145 | 146 | // oblong cubes 147 | new RowMajorCurve(OrdinalVector(4, 4, 5)) with RenderSource { override val useSlices = false; def getCurveName = "R445" }.render(csvTarget("r(4,4,5)")) 148 | new ZCurve(OrdinalVector(4, 4, 5)) with RenderSource { override val useSlices = false; def getCurveName = "Z445" }.render(csvTarget("z(4,4,5)")) 149 | new CompactHilbertCurve(OrdinalVector(4, 4, 5)) with RenderSource { override val useSlices = false; def getCurveName = "H445" }.render(csvTarget("h(4,4,5)")) 150 | 151 | 1 must equalTo(1) 152 | } 153 | } 154 | 155 | "be able to render small curves to JSON" >> { 156 | def jsonTarget(fileName: String) = new JSONRenderTarget() { 157 | override val pw: PrintStream = 158 | new java.io.PrintStream(new BufferedOutputStream(new FileOutputStream(s"/tmp/$fileName.js"))) 159 | override def afterRendering(sfc: RenderSource): Unit = { 160 | super.afterRendering(sfc) 161 | pw.close() 162 | } 163 | } 164 | 165 | // square, 1-ply curves 166 | new RowMajorCurve(OrdinalVector(2, 2, 2)) with RenderSource { override val useSlices = false; def getCurveName = "R222" }.render(jsonTarget("r(2,2,2)")) 167 | new ZCurve(OrdinalVector(2, 2, 2)) with RenderSource { override val useSlices = false; def getCurveName = "Z222" }.render(jsonTarget("z(2,2,2)")) 168 | new CompactHilbertCurve(OrdinalVector(2, 2, 2)) with RenderSource { override val useSlices = false; def getCurveName = "H222" }.render(jsonTarget("h(2,2,2)")) 169 | new RowMajorCurve(OrdinalVector(4, 4, 4)) with RenderSource { override val useSlices = false; def getCurveName = "R444" }.render(jsonTarget("r(4,4,4)")) 170 | new ZCurve(OrdinalVector(4, 4, 4)) with RenderSource { override val useSlices = false; def getCurveName = "Z444" }.render(jsonTarget("z(4,4,4)")) 171 | new CompactHilbertCurve(OrdinalVector(4, 4, 4)) with RenderSource { override val useSlices = false; def getCurveName = "H444" }.render(jsonTarget("h(4,4,4)")) 172 | 173 | // oblong, 1-ply curves 174 | new RowMajorCurve(OrdinalVector(2, 2, 4)) with RenderSource { override val useSlices = false; def getCurveName = "R224" }.render(jsonTarget("r(2,2,4)")) 175 | new ZCurve(OrdinalVector(2, 2, 4)) with RenderSource { override val useSlices = false; def getCurveName = "Z224" }.render(jsonTarget("z(2,2,4)")) 176 | new CompactHilbertCurve(OrdinalVector(2, 2, 4)) with RenderSource { override val useSlices = false; def getCurveName = "H224" }.render(jsonTarget("h(2,2,4)")) 177 | new RowMajorCurve(OrdinalVector(4, 4, 5)) with RenderSource { override val useSlices = false; def getCurveName = "R445" }.render(jsonTarget("r(4,4,5)")) 178 | new ZCurve(OrdinalVector(4, 4, 5)) with RenderSource { override val useSlices = false; def getCurveName = "Z445" }.render(jsonTarget("z(4,4,5)")) 179 | new CompactHilbertCurve(OrdinalVector(4, 4, 5)) with RenderSource { override val useSlices = false; def getCurveName = "H445" }.render(jsonTarget("h(4,4,5)")) 180 | 181 | // square, 2-ply (composed) curves 182 | import BaseCurves._ 183 | def getCurveType(curveType: Int, precisions: OrdinalVector): SpaceFillingCurve = curveType match { 184 | case RowMajor => RowMajorCurve(precisions) 185 | case ZOrder => ZCurve(precisions) 186 | case Hilbert => CompactHilbertCurve(precisions) 187 | } 188 | def getCurveLetter(curveType: Int): String = curveType match { 189 | case RowMajor => "R" 190 | case ZOrder => "Z" 191 | case Hilbert => "H" 192 | } 193 | def mk3curve(topCurve: Int, topLiteralFirst: Boolean, bottomCurve: Int): Unit = { 194 | val (curveName, children, delegate) = if (topLiteralFirst) ( 195 | getCurveLetter(topCurve) + "2" + getCurveLetter(bottomCurve) + "22", 196 | Seq(DefaultDimensions.createIdentityDimension(2), getCurveType(bottomCurve, OrdinalVector(2,2))), 197 | getCurveType(topCurve, OrdinalVector(2, 4)) 198 | ) 199 | else ( 200 | getCurveLetter(topCurve) + getCurveLetter(bottomCurve) + "222", 201 | Seq(getCurveType(bottomCurve, OrdinalVector(2,2)), DefaultDimensions.createIdentityDimension(2)), 202 | getCurveType(topCurve, OrdinalVector(4, 2)) 203 | ) 204 | new ComposedCurve(delegate, children) with RenderSource { 205 | override val useSlices = false 206 | def getCurveName = curveName 207 | }.render(jsonTarget(curveName)) 208 | } 209 | for (top <- Seq(RowMajor, ZOrder, Hilbert); first <- Seq(true, false); bottom <- Seq(RowMajor, ZOrder, Hilbert)) { 210 | mk3curve(top, topLiteralFirst = first, bottom) 211 | } 212 | 213 | 1 must equalTo(1) 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/main/scala/org/eichelberger/sfc/study/composition/StackingVariantsStudy.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.study.composition 2 | 3 | import java.io.{FileWriter, BufferedWriter, PrintWriter} 4 | 5 | import org.eichelberger.sfc.SpaceFillingCurve._ 6 | import org.eichelberger.sfc._ 7 | import org.eichelberger.sfc.study._ 8 | import org.eichelberger.sfc.study.composition.CompositionSampleData._ 9 | import org.eichelberger.sfc.examples.composition.contrast.{FactoryXY, FactoryXYT, FactoryXYZT} 10 | import org.eichelberger.sfc.utils.Timing 11 | import org.joda.time.{DateTimeZone, DateTime} 12 | import org.eichelberger.sfc.utils.CompositionWKT._ 13 | 14 | object StackingVariantsStudy extends App { 15 | 16 | import TestLevels._ 17 | val testLevel = Medium 18 | 19 | // standard test suite of points and queries 20 | val n = getN(testLevel) 21 | val pointQueryPairs = getPointQueryPairs(testLevel) 22 | val points: Seq[XYZTPoint] = pointQueryPairs.map(_._1) 23 | val cells: Seq[Cell] = pointQueryPairs.map(_._2) 24 | val labels: Seq[String] = pointQueryPairs.map(_._3) 25 | 26 | val uniqueLabels = labels.toSet.toSeq 27 | 28 | def goRoundTrip(curve: ComposedCurve, output: OutputDestination): Boolean = { 29 | val (_, msElapsed) = Timing.time(() => { 30 | var i = 0 31 | while (i < n) { 32 | val XYZTPoint(x, y, z, t) = points(i) 33 | val pt = 34 | if (curve.numLeafNodes == 4) Seq(x, y, z, t) 35 | else Seq(x, y, t) 36 | val hash = curve.pointToHash(pt) 37 | val cell = curve.hashToCell(hash) 38 | i = i + 1 39 | } 40 | }) 41 | 42 | output.println(Seq( 43 | curve.name, 44 | msElapsed / 1000.0, 45 | curve.numLeafNodes 46 | )) 47 | 48 | true 49 | } 50 | 51 | def computeQueryRanges(curve: ComposedCurve, output: OutputDestination): Boolean = { 52 | // conduct all queries against this curve 53 | val results: List[(String, Seq[OrdinalPair], Long)] = pointQueryPairs.map{ 54 | case (point, rawCell, label) => 55 | val cell = 56 | if (curve.numLeafNodes == 4) rawCell 57 | else Cell(rawCell.dimensions.take(2) ++ rawCell.dimensions.takeRight(1)) 58 | 59 | curve.clearCache() 60 | 61 | val (ranges, msElapsed) = Timing.time(() => { 62 | val itr = curve.getRangesCoveringCell(cell) 63 | val list = itr.toList 64 | list 65 | }) 66 | 67 | // compute a net label (only needed for 3D curves) 68 | val netLabel = curve.numLeafNodes match { 69 | case 3 => label.take(2) + label.takeRight(1) 70 | case 4 => label 71 | case _ => 72 | throw new Exception(s"Something went wrong: ${curve.numLeafNodes} dimensions found") 73 | } 74 | 75 | (netLabel, ranges, msElapsed) 76 | }.toList 77 | 78 | // aggregate by label 79 | val aggregates = results.groupBy(_._1) 80 | aggregates.foreach { 81 | case (aggLabel, group) => 82 | var totalCells = 0L 83 | var totalRanges = 0L 84 | var totalMs = 0L 85 | 86 | group.foreach { 87 | case (_, ranges, ms) => 88 | totalRanges = totalRanges + ranges.size 89 | totalCells = totalCells + ranges.map(_.size).sum 90 | totalMs = totalMs + ms 91 | } 92 | 93 | val m = group.size.toDouble 94 | val avgRanges = totalRanges.toDouble / m 95 | val avgCells = totalCells.toDouble / m 96 | val seconds = totalMs.toDouble / 1000.0 97 | val avgCellsPerSecond = totalCells / seconds 98 | val avgRangesPerSecond = totalRanges / seconds 99 | val avgCellsPerRange = totalRanges / seconds 100 | val avgSecondsPerCell = seconds / totalCells 101 | val avgSecondsPerRange = seconds / totalRanges 102 | val avgScore = avgCellsPerSecond * avgCellsPerRange 103 | val avgAdjScore = avgCellsPerSecond * Math.log(1.0 + avgCellsPerRange) 104 | 105 | val data = Seq( 106 | DateTime.now().toString, 107 | curve.name, 108 | "ranges", 109 | aggLabel, 110 | curve.M, 111 | n, 112 | curve.numLeafNodes, 113 | curve.plys, 114 | avgRanges, 115 | avgCells, 116 | avgCellsPerSecond, 117 | avgCellsPerRange, 118 | avgSecondsPerCell, 119 | avgSecondsPerRange, 120 | avgScore, 121 | avgAdjScore, 122 | seconds 123 | ) 124 | output.println(data) 125 | } 126 | 127 | true 128 | } 129 | 130 | def writeCharlottesvilleRanges(curve: ComposedCurve, precision: Int): Boolean = { 131 | val pw = new PrintWriter(new BufferedWriter(new FileWriter(s"/tmp/Charlottesville-${precision.formatted("%02d")}-${curve.name}.tsv"))) 132 | val pw2 = new PrintWriter(new BufferedWriter(new FileWriter(s"/tmp/Charlottesville-${precision.formatted("%02d")}-${curve.name}.txt"))) 133 | val pw3 = new PrintWriter(new BufferedWriter(new FileWriter(s"/tmp/Charlottesville-${precision.formatted("%02d")}-${curve.name}-curve.wkt"))) 134 | pw.println("order\tidx_min\tidx_max\tnum_cells\twkt") 135 | 136 | val queryCell: Cell = Cell(Seq( 137 | DefaultDimensions.createDimension("x", bboxCville._1, bboxCville._3, 0L), 138 | DefaultDimensions.createDimension("y", bboxCville._2, bboxCville._4, 0L) 139 | )) 140 | val sparse = collection.mutable.HashMap[OrdinalVector, OrdinalNumber]() 141 | var maxIdx = 0L 142 | var idxY0 = Long.MaxValue 143 | var idxY1 = Long.MinValue 144 | var idxX0 = Long.MaxValue 145 | var idxX1 = Long.MinValue 146 | val ranges = curve.getQueryResultRichRanges(queryCell).toList 147 | ranges.foreach { rrange => { 148 | pw.println(s"${rrange.order}\t${rrange.range.min}\t${rrange.range.max}\t${rrange.range.size}\t${rrange.wkt}") 149 | for (idx <- rrange.range.min to rrange.range.max) { 150 | val coords: OrdinalVector = curve.inverseIndex(idx) 151 | sparse.put(coords, idx) 152 | idxY0 = Math.min(idxY0, coords(1)) 153 | idxX0 = Math.min(idxX0, coords(0)) 154 | idxY1 = Math.max(idxY1, coords(1)) 155 | idxX1 = Math.max(idxX1, coords(0)) 156 | } 157 | }} 158 | maxIdx = Math.max(idxX1, idxY1) 159 | 160 | // dump the grid of indexes 161 | val len = maxIdx.toString.length 162 | def fmt(ord: OrdinalNumber): String = ord.formatted("%" + len + "d") 163 | for (y <- idxY1 to idxY0 by -1) { 164 | for (x <- idxX0 to idxX1) { 165 | val coords = OrdinalVector(x, y) 166 | sparse.get(coords).foreach(idx => { 167 | if (x > idxX0) pw2.print("\t") 168 | pw2.print(fmt(idx)) 169 | }) 170 | } 171 | pw2.println() 172 | } 173 | 174 | // dump the curve as WKT segments 175 | pw3.println(Seq("i", "idxY", "y", "idxX", "x", "idx", "lastx", "lasty", "wkt").mkString("\t")) 176 | val longitude = DefaultDimensions.createLongitude(curve.precisions(0)) 177 | val latitude = DefaultDimensions.createLatitude(curve.precisions(1)) 178 | def getLatLon(index: OrdinalNumber): (Double, Double) = { 179 | val OrdinalVector(ix, iy) = curve.inverseIndex(index) 180 | ( 181 | longitude.inverseIndex(ix).doubleMid, 182 | latitude.inverseIndex(iy).doubleMid 183 | ) 184 | } 185 | var i = 0L 186 | for (idxY <- idxY0 - 1 to idxY1 + 1) { 187 | val y = latitude.inverseIndex(idxY).doubleMid 188 | for (idxX <- idxX0 - 1 to idxX1 + 1) { 189 | val x = longitude.inverseIndex(idxX).doubleMid 190 | val idx = curve.index(OrdinalVector(idxX, idxY)) 191 | val (lastx, lasty) = getLatLon(idx - 1L) 192 | pw3.println(Seq( 193 | i, idxY, y, idxX, x, idx, lastx, lasty, 194 | s"LINESTRING($lastx $lasty, $x $y)" 195 | ).mkString("\t")) 196 | i = i + 1L 197 | } 198 | } 199 | 200 | pw3.close() 201 | pw2.close() 202 | pw.close() 203 | 204 | true 205 | } 206 | 207 | def perCurveTestSuite(curve: ComposedCurve, output_rt: OutputDestination, output_qr: OutputDestination): Boolean = { 208 | curve.clearCache() 209 | val a = goRoundTrip(curve, output_rt) 210 | 211 | curve.clearCache() 212 | val b = computeQueryRanges(curve, output_qr) 213 | 214 | a && b 215 | } 216 | 217 | def createRoundTripOutput: OutputDestination = { 218 | val columns = OutputMetadata(Seq( 219 | ColumnSpec("curve", isQuoted = true), 220 | ColumnSpec("seconds", isQuoted = false), 221 | ColumnSpec("dimensions", isQuoted = false) 222 | )) 223 | val baseFile = "composed-curves-round-trip" 224 | new MultipleOutput(Seq( 225 | new MirroredTSV(s"/tmp/$baseFile.tsv", columns, writeHeader = true), 226 | new JSON(s"/tmp/$baseFile.js", columns) 227 | with FileOutputDestination { def fileName = s"/tmp/$baseFile.js" } 228 | )) 229 | } 230 | 231 | def createQueryRangesOutput: OutputDestination = { 232 | val columns = OutputMetadata(Seq( 233 | ColumnSpec("when", isQuoted = true), 234 | ColumnSpec("curve", isQuoted = true), 235 | ColumnSpec("test.type", isQuoted = true), 236 | ColumnSpec("label", isQuoted = true), 237 | ColumnSpec("precision", isQuoted = false), 238 | ColumnSpec("replications", isQuoted = false), 239 | ColumnSpec("dimensions", isQuoted = false), 240 | ColumnSpec("plys", isQuoted = false), 241 | ColumnSpec("avg.ranges", isQuoted = false), 242 | ColumnSpec("avg.cells", isQuoted = false), 243 | ColumnSpec("cells.per.second", isQuoted = false), 244 | ColumnSpec("cells.per.range", isQuoted = false), 245 | ColumnSpec("seconds.per.cell", isQuoted = false), 246 | ColumnSpec("seconds.per.range", isQuoted = false), 247 | ColumnSpec("score", isQuoted = false), 248 | ColumnSpec("adj.score", isQuoted = false), 249 | ColumnSpec("seconds", isQuoted = false) 250 | )) 251 | val baseFile = "composed-curves-query-ranges" 252 | new MultipleOutput(Seq( 253 | new MirroredTSV(s"/tmp/$baseFile.tsv", columns, writeHeader = true), 254 | new JSON(s"/tmp/$baseFile.js", columns) 255 | with FileOutputDestination { def fileName = s"/tmp/$baseFile.js" } 256 | )) 257 | } 258 | 259 | def printScalingResults(): Unit = { 260 | val (bitsLow, bitsHigh, bitsIncrement) = testLevel match { 261 | case Debug => (40, 40, 1) 262 | case Small => (20, 30, 10) 263 | case Medium => (20, 40, 10) 264 | case Large => (20, 40, 5) 265 | } 266 | 267 | val output_rt = createRoundTripOutput 268 | val output_qr = createQueryRangesOutput 269 | 270 | for (totalPrecision <- bitsLow to bitsHigh by bitsIncrement) { 271 | // 4D, horizontal 272 | FactoryXYZT(totalPrecision, 1).getCurves.map(curve => perCurveTestSuite(curve, output_rt, output_qr)) 273 | 274 | // 4D, mixed (2, 2) 275 | FactoryXYZT(totalPrecision, 2).getCurves.map(curve => perCurveTestSuite(curve, output_rt, output_qr)) 276 | 277 | // 4D, mixed (3, 1) 278 | FactoryXYZT(totalPrecision, -2).getCurves.map(curve => perCurveTestSuite(curve, output_rt, output_qr)) 279 | 280 | // 4D, vertical 281 | FactoryXYZT(totalPrecision, 3).getCurves.map(curve => perCurveTestSuite(curve, output_rt, output_qr)) 282 | 283 | // 3D, horizontal 284 | FactoryXYT(totalPrecision, 1).getCurves.map(curve => perCurveTestSuite(curve, output_rt, output_qr)) 285 | 286 | // 3D, mixed 287 | FactoryXYT(totalPrecision, 2).getCurves.map(curve => perCurveTestSuite(curve, output_rt, output_qr)) 288 | } 289 | 290 | output_qr.close() 291 | output_rt.close() 292 | } 293 | 294 | def writeCharlottesvilleRanges(): Unit = { 295 | for (totalPrecision <- 21 to 35 by 2) { 296 | FactoryXY(totalPrecision).getCurves.map(curve => writeCharlottesvilleRanges(curve, curve.M)) 297 | } 298 | } 299 | 300 | printScalingResults() 301 | } 302 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /src/main/scala/org/eichelberger/sfc/utils/RenderSource.scala: -------------------------------------------------------------------------------- 1 | package org.eichelberger.sfc.utils 2 | 3 | import java.awt.Color 4 | import java.io.{PrintStream, PrintWriter} 5 | 6 | import org.eichelberger.sfc.SpaceFillingCurve._ 7 | 8 | trait RenderTarget { 9 | def qw(s: String) = "\"" + s + "\"" 10 | def beforeRendering(sfc: RenderSource): Unit = {} 11 | def beforeSlice(sfc: RenderSource, slice: OrdinalVector): Unit = {} 12 | def beforeRow(sfc: RenderSource, row: OrdinalNumber): Unit = {} 13 | def beforeCol(sfc: RenderSource, col: OrdinalNumber): Unit = {} 14 | def renderCell(sfc: RenderSource, index: OrdinalNumber, point: OrdinalVector): Unit = {} 15 | def afterCol(sfc: RenderSource, col: OrdinalNumber): Unit = {} 16 | def afterRow(sfc: RenderSource, row: OrdinalNumber): Unit = {} 17 | def afterSlice(sfc: RenderSource, slice: OrdinalVector): Unit = {} 18 | def afterRendering(sfc: RenderSource): Unit = {} 19 | } 20 | 21 | trait RenderSource { 22 | this: SpaceFillingCurve => 23 | 24 | def getCurveName: String 25 | 26 | def numCells: Long = size 27 | 28 | def useSlices: Boolean = true 29 | 30 | def indexBounds: Seq[OrdinalPair] = 31 | precisions.toSeq.map(p => OrdinalPair(0L, (1L << p) - 1L)) 32 | 33 | def nonTerminalIndexBounds: Seq[OrdinalPair] = 34 | if (precisions.size > 2) indexBounds.dropRight(2) 35 | else Seq(OrdinalPair(0, 0)) 36 | 37 | def terminalIndexBounds: Seq[OrdinalPair] = 38 | indexBounds.takeRight(Math.min(2, precisions.size)) 39 | 40 | def renderSlices(target: RenderTarget) = { 41 | // loop over all dimensions higher than 2 42 | val slices = combinationsIterator(nonTerminalIndexBounds) 43 | while (slices.hasNext) { 44 | // identify the context (combination of dimensions > 2) 45 | val slice: OrdinalVector = slices.next() 46 | target.beforeSlice(this, slice) 47 | 48 | // dump this 1- or 2-d slice 49 | var row = -1L 50 | val cellItr = combinationsIterator(terminalIndexBounds) 51 | while (cellItr.hasNext) { 52 | val cell: OrdinalVector = cellItr.next() 53 | val fullVec = indexBounds.size match { 54 | case 1 | 2 => cell 55 | case _ => slice + cell 56 | } 57 | val idx = index(fullVec) 58 | 59 | // switch rows 60 | if (row != cell.toSeq.head) { 61 | if (row != -1L) target.afterRow(this, row) 62 | row = cell.toSeq.head 63 | target.beforeRow(this, row) 64 | } 65 | 66 | // print cell 67 | target.renderCell(this, idx, cell) 68 | } 69 | 70 | target.afterRow(this, row) 71 | 72 | // finish this slice 73 | target.afterSlice(this, slice) 74 | } 75 | } 76 | 77 | def renderWhole(target: RenderTarget) = { 78 | // dump this 1- or 2-d slice 79 | var row = -1L 80 | val cellItr = combinationsIterator(indexBounds) 81 | while (cellItr.hasNext) { 82 | val cell: OrdinalVector = cellItr.next() 83 | val idx = index(cell) 84 | 85 | // switch rows 86 | if (row != cell.toSeq.head) { 87 | if (row != -1L) target.afterRow(this, row) 88 | row = cell.toSeq.head 89 | target.beforeRow(this, row) 90 | } 91 | 92 | // print cell 93 | target.renderCell(this, idx, cell) 94 | } 95 | 96 | target.afterRow(this, row) 97 | } 98 | 99 | def render(target: RenderTarget) = { 100 | target.beforeRendering(this) 101 | 102 | if (useSlices) renderSlices(target) 103 | else renderWhole(target) 104 | 105 | target.afterRendering(this) 106 | } 107 | } 108 | 109 | // dump the layers of the SFC cell-indexes to STDOUT 110 | class ScreenRenderTarget extends RenderTarget { 111 | val pw: PrintStream = System.out 112 | 113 | var numRows: Long = 0L 114 | var numCols: Long = 0L 115 | 116 | override def beforeRendering(sfc: RenderSource): Unit = {} 117 | 118 | override def beforeSlice(sfc: RenderSource, slice: OrdinalVector): Unit = { 119 | pw.println(s"\n[${sfc.getCurveName}: SLICE $slice ]") 120 | 121 | val (nr: Long, nc: Long) = sfc.terminalIndexBounds.size match { 122 | case 0 => throw new Exception("Cannot print an empty SFC") 123 | case 1 => (1, sfc.terminalIndexBounds(0).max + 1L) 124 | case 2 => (sfc.terminalIndexBounds(0).max + 1L, sfc.terminalIndexBounds(1).max + 1L) 125 | } 126 | numRows = nr; numCols = nc 127 | } 128 | 129 | override def beforeRow(sfc: RenderSource, row: OrdinalNumber): Unit = { 130 | pw.print(s" ${format(row)} | ") 131 | } 132 | 133 | override def renderCell(sfc: RenderSource, index: OrdinalNumber, point: OrdinalVector): Unit = { 134 | pw.print(s"${format(index)} | ") 135 | } 136 | 137 | override def afterRow(sfc: RenderSource, row: OrdinalNumber): Unit = { 138 | pw.println() 139 | } 140 | 141 | override def afterSlice(sfc: RenderSource, slice: OrdinalVector): Unit = { 142 | // print the X axis 143 | separate(pw, numCols.toInt) 144 | pw.println() 145 | pw.print(" | ") 146 | for (x <- 0 until numCols.toInt) { 147 | pw.print(s"${format(x)} | ") 148 | } 149 | pw.println() 150 | } 151 | 152 | def format(x: Long): String = x.formatted("%4d") 153 | 154 | def separate(pw: PrintStream, numCols: Int): Unit = { 155 | val line = "------+"*numCols 156 | pw.print(s" +$line") 157 | } 158 | } 159 | 160 | object ShadeRampEndpoint { 161 | def apply(index: OrdinalNumber, color: Color): ShadeRampEndpoint = { 162 | val (hue, saturation, brightness) = { 163 | var arr: Array[Float] = null 164 | Color.RGBtoHSB(color.getRed, color.getGreen, color.getBlue, arr) 165 | (arr(0), arr(1), arr(2)) 166 | } 167 | 168 | new ShadeRampEndpoint(index, hue, saturation, brightness) 169 | } 170 | } 171 | import ShadeRampEndpoint._ 172 | 173 | case class ShadeRampEndpoint(index: OrdinalNumber, hue: Float, saturation: Float, brightness: Float) 174 | 175 | case class ShadeRamp(endLow: ShadeRampEndpoint, endHigh: ShadeRampEndpoint) { 176 | val indexSpan = endHigh.index - endLow.index 177 | val slopeHue = (endHigh.hue - endLow.hue) / indexSpan.toDouble 178 | val slopeSaturation = (endHigh.saturation - endLow.saturation) / indexSpan.toDouble 179 | val slopeBrightness = (endHigh.brightness - endLow.brightness) / indexSpan.toDouble 180 | 181 | val InvisibleColor = new Color(0, 0, 0, 0) 182 | 183 | def getColor(index: OrdinalNumber): Color = { 184 | if (index < endLow.index) return InvisibleColor 185 | if (index > endHigh.index) return InvisibleColor 186 | 187 | val dist = index - endLow.index 188 | val h = endLow.hue + dist * slopeHue 189 | val s = endLow.saturation + dist * slopeSaturation 190 | val b = endLow.brightness + dist * slopeBrightness 191 | Color.getHSBColor(h.toFloat, s.toFloat, b.toFloat) 192 | } 193 | 194 | def toHexByte(i: Int): String = 195 | (if (i < 16) "0" else "") + java.lang.Integer.toHexString(i) 196 | 197 | def getColorHex(index: OrdinalNumber): String = { 198 | val color = getColor(index) 199 | toHexByte(color.getRed) + toHexByte(color.getGreen) + toHexByte(color.getBlue) + toHexByte(color.getAlpha) 200 | } 201 | } 202 | 203 | // dump the layers of the SFC cell-indexes to STDOUT suitable for Graphviz rendering 204 | // 205 | // to render correctly: 206 | // neato -n input.dot -Tpng -o output.png 207 | class GraphvizRenderTarget extends RenderTarget { 208 | val pw: PrintStream = System.out 209 | val ptsSpacing = 75 210 | 211 | var numRows: Long = 0L 212 | var numCols: Long = 0L 213 | var numSlice = 0 214 | 215 | val drawNumbers = true 216 | val drawArrows = true 217 | val cellShadingRamp: Option[ShadeRamp] = None 218 | 219 | override def beforeRendering(sfc: RenderSource): Unit = { 220 | pw.println("// to render correctly:\n// neato -n input.dot -Tpng -o output.png") 221 | pw.println("digraph G {") 222 | pw.println("\toutputorder=\"nodesfirst\"") 223 | pw.println("\tnode [ shape=\"square\" width=\"1.058\" labelloc=\"c\" fontsize=\"30\" color=\"#000000\"]") 224 | } 225 | 226 | override def beforeSlice(sfc: RenderSource, slice: OrdinalVector): Unit = { 227 | pw.println(s"\n\tsubgraph {") 228 | pw.println("\tedge [ constraint=\"false\" tailclip=\"false\" headclip=\"false\" color=\"#000000FF\" ]") 229 | //pw.println(s"

${sfc.getCurveName}: SLICE $slice

") //@TODO resolve how to print slice titles 230 | 231 | val (nr: Long, nc: Long) = sfc.terminalIndexBounds.size match { 232 | case 0 => throw new Exception("Cannot print an empty SFC") 233 | case 1 => (1, sfc.terminalIndexBounds(0).max + 1L) 234 | case 2 => (sfc.terminalIndexBounds(0).max + 1L, sfc.terminalIndexBounds(1).max + 1L) 235 | } 236 | numRows = nr; numCols = nc 237 | } 238 | 239 | override def renderCell(sfc: RenderSource, index: OrdinalNumber, point: OrdinalVector): Unit = { 240 | if (drawArrows) 241 | if (index >= 1L) pw.println(s"\t\tnode_${index - 1L} -> node_$index") 242 | 243 | val (x: Long, y: Long) = point.size match { 244 | case 1 => (numSlice * (numRows + 1) * ptsSpacing + point(0) * ptsSpacing, 0L) 245 | case 2 => (numSlice * (numRows + 1) * ptsSpacing + point(0) * ptsSpacing, point(1) * ptsSpacing) 246 | } 247 | 248 | val shading = cellShadingRamp.map(ramp => 249 | "style=\"filled\" fillcolor=\"#" + ramp.getColorHex(index) + "\"").getOrElse("") 250 | val label = "label=\"" + (if (drawNumbers) index.toString else "") + "\"" 251 | pw.println(s"\t\tnode_$index [ $label pos=${qw(x.toString + "," + y.toString)} $shading ]") 252 | } 253 | 254 | override def afterSlice(sfc: RenderSource, slice: OrdinalVector): Unit = { 255 | pw.println(s"\t}") // end subgraph 256 | numSlice = numSlice + 1 257 | } 258 | 259 | override def afterRendering(sfc: RenderSource): Unit = { 260 | pw.println("}") // end graph 261 | } 262 | } 263 | 264 | 265 | // write an include file suitable for use in a larger 266 | // Persistence of Vision Raytracer scene 267 | // 268 | // only position information is written; no styling 269 | class PovrayRenderTarget extends RenderTarget { 270 | val pw: PrintStream = System.out 271 | 272 | var povCurveName: String = "UNKNOWN" 273 | 274 | override def beforeRendering(sfc: RenderSource): Unit = { 275 | povCurveName = sfc.getCurveName.replaceAll("[^a-zA-Z0-9]", "_").toUpperCase 276 | 277 | pw.println("// include file for POV-Ray") 278 | pw.println(s"// curve name: ${sfc.getCurveName}") 279 | 280 | pw.println("\n// curve_cells[CellNo][0]: X coord") 281 | pw.println("// curve_cells[CellNo][1]: Y coord") 282 | pw.println("// curve_cells[CellNo][2]: Z coord") 283 | 284 | pw.println(s"\n#declare num_curve_cells = ${sfc.numCells};") 285 | 286 | pw.println(s"\n#declare curve_cells = array[${sfc.numCells}][3];") 287 | } 288 | 289 | override def renderCell(sfc: RenderSource, index: OrdinalNumber, point: OrdinalVector): Unit = { 290 | val items = (0 to 2).map { i => s"#declare curve_cells[$index][$i] = ${point(i)};" } 291 | pw.println(items.mkString(" ")) 292 | } 293 | } 294 | 295 | 296 | // this is one of the simplest output formats, being a 297 | // plain CSV that dumps the index and the coordinates 298 | class CSVRenderTarget extends RenderTarget { 299 | val pw: PrintStream = System.out 300 | 301 | override def renderCell(sfc: RenderSource, index: OrdinalNumber, point: OrdinalVector): Unit = { 302 | val items = index +: point.x 303 | pw.println(items.mkString(",")) 304 | } 305 | } 306 | 307 | 308 | // brute-force-and-ignorance JSON output 309 | class JSONRenderTarget extends RenderTarget { 310 | val pw: PrintStream = System.out 311 | 312 | override def beforeRendering(sfc: RenderSource): Unit = { 313 | val jsCurveName = sfc.getCurveName.replaceAll("[^a-zA-Z0-9]", "_").toUpperCase 314 | pw.println(s"var sfc__$jsCurveName = {") 315 | pw.println(s"\tname: ${qw(sfc.getCurveName)},") 316 | pw.println(s"\tnum_dimensions: ${sfc.indexBounds.size},") 317 | pw.println(s"\tbounds: [") 318 | pw.println(s"\t\t${sfc.indexBounds.map(pair => s"{ min: ${pair.min}, max: ${pair.max} }").mkString(",\n\t\t")}") 319 | pw.println(s"\t],") 320 | pw.println(s"\tnodes: [") 321 | } 322 | 323 | override def renderCell(sfc: RenderSource, index: OrdinalNumber, point: OrdinalVector): Unit = { 324 | pw.println(s"\t\t{") 325 | pw.println(s"\t\t\tindex: $index,") 326 | pw.println(s"\t\t\tpoint: [${point.x.mkString(", ")}]") 327 | pw.println(s"\t\t},") 328 | } 329 | 330 | override def afterRendering(sfc: RenderSource): Unit = { 331 | pw.println("\t]\n};") 332 | } 333 | } --------------------------------------------------------------------------------