├── 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 | }
--------------------------------------------------------------------------------