├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── CONTRIBUTORS.txt ├── LICENSE ├── README.md ├── api ├── build.sbt └── src │ └── main │ └── scala │ └── org │ └── locationtech │ └── sfcurve │ ├── SpaceFillingCurve2D.scala │ ├── SpaceFillingCurveProvider.scala │ └── SpaceFillingCurves.scala ├── benchmarks ├── build.sbt └── src │ └── main │ └── scala │ └── org │ └── locationtech │ └── sfcurve │ └── benchmarks │ ├── CurveBenchmark.scala │ ├── Hilbert2DBenchmark.scala │ └── SFCurveBenchmarks.scala ├── built.sbt ├── hilbert ├── build.sbt └── src │ ├── main │ ├── resources │ │ └── META-INF │ │ │ └── services │ │ │ └── org.locationtech.sfcurve.SpaceFillingCurveProvider │ └── scala │ │ └── org │ │ └── locationtech │ │ └── sfcurve │ │ └── hilbert │ │ └── HilbertCurve2D.scala │ └── test │ └── scala │ └── org │ └── locationtech │ └── sfcurve │ └── hilbert │ └── HilbertCurveSpec.scala ├── project ├── Dependencies.scala ├── Version.scala ├── build.properties └── plugins.sbt └── zorder ├── build.sbt └── src ├── main ├── resources │ └── META-INF │ │ └── services │ │ └── org.locationtech.sfcurve.SpaceFillingCurveProvider └── scala │ └── org │ └── locationtech │ └── sfcurve │ └── zorder │ ├── MergeQueue.scala │ ├── Z2.scala │ ├── Z3.scala │ ├── ZCurve2D.scala │ ├── ZN.scala │ ├── ZOrderSFCProvider.scala │ └── ZRange.scala └── test └── scala └── org └── locationtech └── sfcurve └── zorder ├── Z2IteratorSpec.scala ├── Z2Spec.scala ├── Z3RangeSpec.scala ├── Z3Spec.scala └── ZCurve2DSpec.scala /.gitignore: -------------------------------------------------------------------------------- 1 | build/zinc* 2 | build/scala* 3 | build/*.jar 4 | *.iws 5 | *.ipr 6 | *.iml 7 | *.idea/ 8 | *.log 9 | .idea/** 10 | **/.classpath 11 | **/.project 12 | **/.settings 13 | target 14 | **/src/main/resources/git.properties 15 | \#*# 16 | *~ 17 | .#* 18 | 19 | project/boot 20 | project/plugins/project 21 | project/plugins/target 22 | project/target 23 | .lib 24 | 25 | *.pyc 26 | .project 27 | .classpath 28 | .cache 29 | .settings 30 | .history 31 | .idea 32 | .DS_Store 33 | *.iml 34 | *.swp 35 | *.swo 36 | *.sublime-* 37 | .vagrant 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | 4 | language: scala 5 | scala: 6 | - 2.10.6 7 | - 2.11.7 8 | 9 | jdk: 10 | - oraclejdk8 11 | 12 | cache: 13 | directories: 14 | - $HOME/.ivy2/cache 15 | - $HOME/.sbt/boot/ 16 | 17 | before_cache: 18 | # Tricks to avoid unnecessary cache updates 19 | - find $HOME/.ivy2 -name "ivydata-*.properties" -delete 20 | - find $HOME/.sbt -name "*.lock" -delete 21 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to SFCurve 2 | 3 | We welcome contributions. To start, it helps the community if you can let us know what you are working on ahead of time. 4 | 5 | Our developer list is hosted at LocationTech here: https://locationtech.org/mailman/listinfo/sfcurve-dev. 6 | 7 | SFCurve is an open source project under the Apache 2.0 License. In order to accept code contributions, the submitter must 8 | 9 | 1. Sign a CLA with the Eclipse Foundation and use the registered email with the Git commits associated to the GitHub pull request. 10 | 11 | 2. Acknowledge that the code contribution is IP clean by 'signing off' the commits in the pull request with the '-s' option to 'git commit'. 12 | 13 | If you have questions or concerns about this, please ask on the sfcurve-dev list. 14 | 15 | 16 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | This file includes the contributors to the SFCurve project under LocationTech. 2 | 3 | Project Leads 4 | Rob Emanuele 5 | Jim Hughes 6 | 7 | Committers 8 | Eugene Cheipesh 9 | 10 | Facebook Open Academy UT-Austin students 11 | Mark Sandan 12 | Brett Smith 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under the Apache 2 license, quoted below. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | use this file except in compliance with the License. You may obtain a copy of 5 | the License at 6 | 7 | [http://www.apache.org/licenses/LICENSE-2.0] 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | License for the specific language governing permissions and limitations under 13 | the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SFCurve 2 | ===== 3 | 4 | [![Join the chat at https://gitter.im/locationtech/sfcurve](https://badges.gitter.im/locationtech/sfcurve.svg)](https://gitter.im/locationtech/sfcurve?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | 6 | ![sfcurve-space-diagram](https://cloud.githubusercontent.com/assets/2320142/6543539/449db6e2-c4ed-11e4-865a-584e056b5469.png) 7 | 8 | ### What is SFCurve? 9 | 10 | This library represents a collaborative attempt to create a solid, robust and modular library for dealing with space filling curves on the JVM. 11 | 12 | To read up on the collaborative effort to define the interface, and to participate in the discussion, see [Issue #3](https://github.com/geotrellis/curve/issues/3). 13 | 14 | ### Where can I learn more about the project? 15 | 16 | A more detailed account of the origin and intention can be found in the [proposal of the SFCurve project submitted to LocationTech](http://www.locationtech.org/proposals/sfcurve). You can also find more information by contacting any of the current collaborators, or by asking about it on the [GeoTrellis](https://github.com/geotrellis/geotrellis), [GeoMesa](https://github.com/locationtech/geomesa), or [GeoWave](https://github.com/ngageoint/geowave) mailing lists. 17 | 18 | ### Can I use SFCurve now? 19 | 20 | This library is a complete work in progress, and is __NOT__ recommended for current use. In the future, though, we hope to be the definitive library for working with space filling curves on the JVM. If you have ideas on how to get us there, please participate! 21 | 22 | ### Working with SFCurve 23 | 24 | The build tool used in this project is [sbt](http://www.scala-sbt.org/). A script is included that will download the necessary `sbt` software, so you do not need `sbt` installed on the machine to work with this project. 25 | 26 | To drop into the `sbt` console, where you can execute various commands, simply type 27 | 28 | ``` 29 | > ./sbt 30 | ``` 31 | 32 | in your shell. 33 | 34 | In the below examples `>`, `bechmarks >` etc. represents the sbt console prompt. 35 | 36 | Once in the `sbt` console, to compile the code, issue the command: 37 | 38 | ``` 39 | > compile 40 | ``` 41 | 42 | To test the code: 43 | 44 | ``` 45 | > test 46 | ``` 47 | 48 | To start a scala console which has the core sfcurve code in the classpath: 49 | 50 | ``` 51 | > console 52 | ``` 53 | 54 | To run the benchmarks, drop into the benchmark subproject using the `project` command, and run: 55 | 56 | ``` 57 | > project benchmarks 58 | benchmarks > run 59 | ``` 60 | 61 | ### Publishing snapshot binaries 62 | 63 | If you run the `publish-local` sbt command, the subproject artifacts will be published to the local `ivy2` cache. 64 | 65 | If you run the `publish` sbt command, the subproject artifacts will be published to the local maven2 repository. 66 | 67 | This will publish artifacts for the latest scala version. If you want other scala versions, you can add a `+` in front of the command to do cross-version commands. So `+publish-local` will publish scala 2.10 and 2.11 artifacts to the local ivy2 cache. 68 | 69 | ### Showing the dependency graph 70 | 71 | Run `dependencyGraph` in the subproject sbt console. 72 | -------------------------------------------------------------------------------- /api/build.sbt: -------------------------------------------------------------------------------- 1 | import Dependencies._ 2 | 3 | name := "sfcurve-api" 4 | libraryDependencies ++= Seq( 5 | scalaTest % "test" 6 | ) 7 | scalacOptions ++= Seq("-optimize") 8 | -------------------------------------------------------------------------------- /api/src/main/scala/org/locationtech/sfcurve/SpaceFillingCurve2D.scala: -------------------------------------------------------------------------------- 1 | /*********************************************************************** 2 | * Copyright (c) 2015 Azavea. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the Apache License, Version 2.0 which 5 | * accompanies this distribution and is available at 6 | * http://www.opensource.org/licenses/apache2.0.php. 7 | ***********************************************************************/ 8 | 9 | package org.locationtech.sfcurve 10 | 11 | class RangeComputeHints extends java.util.HashMap[String, AnyRef] 12 | 13 | sealed trait IndexRange { 14 | def lower: Long 15 | def upper: Long 16 | def contained: Boolean 17 | def tuple = (lower, upper, contained) 18 | } 19 | 20 | case class CoveredRange(lower: Long, upper: Long) extends IndexRange { 21 | val contained = true 22 | } 23 | 24 | case class OverlappingRange(lower: Long, upper: Long) extends IndexRange { 25 | val contained = false 26 | } 27 | 28 | object IndexRange { 29 | trait IndexRangeOrdering extends Ordering[IndexRange] { 30 | override def compare(x: IndexRange, y: IndexRange): Int = { 31 | val c1 = x.lower.compareTo(y.lower) 32 | if(c1 != 0) return c1 33 | val c2 = x.upper.compareTo(y.upper) 34 | if(c2 != 0) return c2 35 | 0 36 | } 37 | } 38 | 39 | implicit object IndexRangeIsOrdered extends IndexRangeOrdering 40 | 41 | def apply(l: Long, u: Long, contained: Boolean): IndexRange = 42 | if(contained) CoveredRange(l, u) 43 | else OverlappingRange(l, u) 44 | } 45 | 46 | trait SpaceFillingCurve2D { 47 | def toIndex(x: Double, y: Double): Long 48 | def toPoint(i: Long): (Double, Double) 49 | def toRanges(xmin: Double, ymin: Double, xmax: Double, ymax: Double, hints: Option[RangeComputeHints] = None): Seq[IndexRange] 50 | } 51 | -------------------------------------------------------------------------------- /api/src/main/scala/org/locationtech/sfcurve/SpaceFillingCurveProvider.scala: -------------------------------------------------------------------------------- 1 | package org.locationtech.sfcurve 2 | 3 | trait SpaceFillingCurveProvider { 4 | def canProvide(name: String): Boolean 5 | def build2DSFC(args: Map[String, java.io.Serializable]): SpaceFillingCurve2D 6 | } 7 | -------------------------------------------------------------------------------- /api/src/main/scala/org/locationtech/sfcurve/SpaceFillingCurves.scala: -------------------------------------------------------------------------------- 1 | package org.locationtech.sfcurve 2 | 3 | import java.util.ServiceLoader 4 | 5 | object SpaceFillingCurves { 6 | 7 | import scala.collection.JavaConverters._ 8 | 9 | lazy val loader = ServiceLoader.load(classOf[SpaceFillingCurveProvider]) 10 | 11 | def apply(name: String, args: Map[String, java.io.Serializable]): SpaceFillingCurve2D = { 12 | val provider = 13 | loader.asScala.find(_.canProvide(name)).getOrElse(throw new RuntimeException(s"Cannot find provider for type: $name")) 14 | provider.build2DSFC(args) 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /benchmarks/build.sbt: -------------------------------------------------------------------------------- 1 | import Dependencies._ 2 | 3 | javaOptions += "-Xmx8G" 4 | libraryDependencies ++= Seq( 5 | "com.google.code.caliper" % "caliper" % "1.0-SNAPSHOT" from "http://plastic-idolatry.com/jars/caliper-1.0-SNAPSHOT.jar", 6 | "com.google.guava" % "guava" % "r09", 7 | "com.google.code.java-allocation-instrumenter" % "java-allocation-instrumenter" % "2.0", 8 | "com.google.code.gson" % "gson" % "1.7.1" 9 | ) 10 | fork := true 11 | 12 | // custom kludge to get caliper to see the right classpath 13 | // we need to add the runtime classpath as a "-cp" argument to the 14 | // `javaOptions in run`, otherwise caliper will not see the right classpath 15 | // and die with a ConfigurationException unfortunately `javaOptions` is a 16 | // SettingsKey and `fullClasspath in Runtime` is a TaskKey, so we need to 17 | // jump through these hoops here in order to feed the result of the latter 18 | // into the former 19 | 20 | lazy val benchmarkKey = AttributeKey[Boolean]("javaOptionsPatched") 21 | 22 | onLoad in Global ~= { previous => state => 23 | previous { 24 | state.get(benchmarkKey) match { 25 | case None => 26 | // get the runtime classpath, turn into a colon-delimited string 27 | Project 28 | .runTask(fullClasspath in Runtime, state) 29 | .get 30 | ._2 31 | .toEither match { 32 | case Right(x) => 33 | val classPath = 34 | x.files 35 | .mkString(":") 36 | // return a state with javaOptionsPatched = true and javaOptions set correctly 37 | Project 38 | .extract(state) 39 | .append( 40 | Seq(javaOptions in run ++= Seq("-Xmx8G", "-cp", classPath)), 41 | state.put(benchmarkKey, true) 42 | ) 43 | case _ => state 44 | } 45 | case Some(_) => 46 | state // the javaOptions are already patched 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /benchmarks/src/main/scala/org/locationtech/sfcurve/benchmarks/CurveBenchmark.scala: -------------------------------------------------------------------------------- 1 | /*********************************************************************** 2 | * Copyright (c) 2015 Azavea. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the Apache License, Version 2.0 which 5 | * accompanies this distribution and is available at 6 | * http://www.opensource.org/licenses/apache2.0.php. 7 | ***********************************************************************/ 8 | 9 | package org.locationtech.sfcurve.benchmarks 10 | 11 | import com.google.caliper.Benchmark 12 | import com.google.caliper.Runner 13 | import com.google.caliper.SimpleBenchmark 14 | 15 | abstract class BenchmarkRunner(cls: java.lang.Class[_ <: Benchmark]) { 16 | def main(args: Array[String]): Unit = Runner.main(cls, args: _*) 17 | } 18 | 19 | trait CurveBenchmark extends SimpleBenchmark { 20 | /** 21 | * Sugar to run 'f' for 'reps' number of times. 22 | */ 23 | def run(reps: Int)(f: => Unit) = { 24 | var i = 0 25 | while (i < reps) { f; i += 1 } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /benchmarks/src/main/scala/org/locationtech/sfcurve/benchmarks/Hilbert2DBenchmark.scala: -------------------------------------------------------------------------------- 1 | /*********************************************************************** 2 | * Copyright (c) 2015 Azavea. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the Apache License, Version 2.0 which 5 | * accompanies this distribution and is available at 6 | * http://www.opensource.org/licenses/apache2.0.php. 7 | ***********************************************************************/ 8 | 9 | package org.locationtech.sfcurve.benchmarks 10 | 11 | import org.locationtech.sfcurve.hilbert._ 12 | import com.google.caliper.Param 13 | 14 | 15 | object HilbertBenchmark extends BenchmarkRunner(classOf[HilbertBenchmark]) 16 | 17 | class HilbertBenchmark extends CurveBenchmark { 18 | 19 | 20 | def timeHilbertBadCase(reps: Int) = run(reps)(HilbertBadCase) 21 | def HilbertBadCase = { 22 | 23 | var i = 2 //resolution bits 24 | while (i < 20){ 25 | val sfc = new HilbertCurve2D(i) 26 | val range = sfc.toRanges(-178.123456, -86.398493, 179.3211113, 87.393483) 27 | i += 1 28 | } 29 | 30 | } 31 | 32 | def timeHilbertCityCase(reps: Int) = run(reps)(HilbertCityCase) 33 | def HilbertCityCase = { 34 | var i = 10 35 | var lx = -180 36 | var ly = 89.68103 37 | var ux = -179.597625 38 | var uy = 90 39 | var yrun = 0 40 | var xrun = 0 41 | 42 | while (i < 24){ 43 | val sfc = new HilbertCurve2D(i) 44 | while((yrun + ly) > -90){ 45 | xrun = 0 46 | while ((xrun + ux) < 180){ 47 | val range = sfc.toRanges(lx+xrun, ly+yrun, ux+xrun, uy+yrun) 48 | xrun += 5 49 | } 50 | yrun -= 5 51 | } 52 | i += 1 53 | } 54 | } 55 | 56 | def timeHilbertStateCase(reps: Int) = run(reps)(HilbertStateCase) 57 | def HilbertStateCase = { 58 | var i = 6 59 | var lx = -180 60 | var ly = 86.022914 61 | var ux = -173.078613 62 | var uy = 90 63 | var yrun = 0 64 | var xrun = 0 65 | 66 | while (i < 24){ 67 | val sfc = new HilbertCurve2D(i) 68 | while ((yrun + ly) > -90){ 69 | xrun = 0 70 | while((xrun + ux) < 180){ 71 | val range = sfc.toRanges(lx+xrun, ly+yrun, ux+xrun, uy+yrun) 72 | xrun += 5 73 | } 74 | yrun -= 5 75 | } 76 | 77 | i += 1 78 | } 79 | } 80 | 81 | def timeHilbertCountryCase(reps: Int) = run(reps)(HilbertCountryCase) 82 | def HilbertCountryCase = { 83 | var i = 6 84 | var lx = -180 85 | var ly = 82.689749 86 | var ux = -171.408692 87 | var uy = 90 88 | var yrun = 0 89 | var xrun = 0 90 | 91 | while (i < 24){ 92 | val sfc = new HilbertCurve2D(i) 93 | while ((yrun + ly) > -90){ 94 | xrun = 0 95 | while((xrun + ux) < 180){ 96 | val range = sfc.toRanges(lx+xrun, ly+yrun, ux+xrun, uy+yrun) 97 | xrun += 5 98 | } 99 | yrun -= 5 100 | } 101 | 102 | i += 1 103 | } 104 | } 105 | //Are we doing the error on tiling vs. coordinates? 106 | 107 | } 108 | -------------------------------------------------------------------------------- /benchmarks/src/main/scala/org/locationtech/sfcurve/benchmarks/SFCurveBenchmarks.scala: -------------------------------------------------------------------------------- 1 | /*********************************************************************** 2 | * Copyright (c) 2015 Azavea. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the Apache License, Version 2.0 which 5 | * accompanies this distribution and is available at 6 | * http://www.opensource.org/licenses/apache2.0.php. 7 | ***********************************************************************/ 8 | 9 | package org.locationtech.sfcurve.benchmarks 10 | 11 | import org.locationtech.sfcurve.zorder._ 12 | import com.google.caliper.Param 13 | 14 | object SFCurveBenchmarks extends BenchmarkRunner(classOf[SFCurveBenchmarks]) 15 | class SFCurveBenchmarks extends CurveBenchmark { 16 | 17 | val pts = (0 until 300).toArray 18 | 19 | /*def timeZ2IndexCreate(reps: Int) = run(reps)(z2IndexCreation) 20 | def z2IndexCreation = { 21 | 22 | var res = 2 23 | while(res < 24){ 24 | new ZCurve2D(res) 25 | res += 1 26 | } 27 | }*/ 28 | 29 | def timeZ2BadCase(reps: Int) = run(reps)(Z2BadCase) 30 | def Z2BadCase = { 31 | var t1 = Z2(0,90) 32 | var t2 = Z2(180,0) 33 | println("t1: " + t1.z + " t2: " + t2.z) 34 | var i = 2 //resolution bits 35 | while (i < 20){ 36 | val sfc = new ZCurve2D(i) 37 | val range = sfc.toRanges(-178.123456, -86.398493, 179.3211113, 87.393483) 38 | i += 1 39 | } 40 | 41 | } 42 | 43 | def timeZ2CityCase(reps: Int) = run(reps)(Z2CityCase) 44 | def Z2CityCase = { 45 | var i = 10 46 | var lx = -180 47 | var ly = 89.68103 48 | var ux = -179.597625 49 | var uy = 90 50 | var yrun = 0 51 | var xrun = 0 52 | 53 | while (i < 24){ 54 | val sfc = new ZCurve2D(i) 55 | while((yrun + ly) > -90){ 56 | xrun = 0 57 | while ((xrun + ux) < 180){ 58 | val range = sfc.toRanges(lx+xrun, ly+yrun, ux+xrun, uy+yrun) 59 | xrun += 5 60 | } 61 | yrun -= 5 62 | } 63 | i += 1 64 | } 65 | } 66 | 67 | def timeZ2StateCase(reps: Int) = run(reps)(Z2StateCase) 68 | def Z2StateCase = { 69 | var i = 6 70 | var lx = -180 71 | var ly = 86.022914 72 | var ux = -173.078613 73 | var uy = 90 74 | var yrun = 0 75 | var xrun = 0 76 | 77 | while (i < 24){ 78 | val sfc = new ZCurve2D(i) 79 | while ((yrun + ly) > -90){ 80 | xrun = 0 81 | while((xrun + ux) < 180){ 82 | val range = sfc.toRanges(lx+xrun, ly+yrun, ux+xrun, uy+yrun) 83 | xrun += 5 84 | } 85 | yrun -= 5 86 | } 87 | 88 | i += 1 89 | } 90 | } 91 | 92 | def timeZ2CountryCase(reps: Int) = run(reps)(Z2CountryCase) 93 | def Z2CountryCase = { 94 | var i = 6 95 | var lx = -180 96 | var ly = 82.689749 97 | var ux = -171.408692 98 | var uy = 90 99 | var yrun = 0 100 | var xrun = 0 101 | 102 | while (i < 24){ 103 | val sfc = new ZCurve2D(i) 104 | while ((yrun + ly) > -90){ 105 | xrun = 0 106 | while((xrun + ux) < 180){ 107 | val range = sfc.toRanges(lx+xrun, ly+yrun, ux+xrun, uy+yrun) 108 | xrun += 5 109 | } 110 | yrun -= 5 111 | } 112 | 113 | i += 1 114 | } 115 | } 116 | 117 | /*def timeZ3IndexCreate(reps: Int) = run(reps)(z3IndexCreation) 118 | def z3IndexCreation = { 119 | 120 | var x = 100 121 | var y = 100 122 | var z = 100 123 | 124 | while(x < 200) { 125 | while(y < 200) { 126 | while(z < 200) { 127 | Z3(pts(x), pts(y), pts(z)) 128 | z += 1 129 | } 130 | y += 1 131 | } 132 | x += 1 133 | } 134 | } 135 | 136 | def timeZ3ZRanges(reps: Int) = run(reps)(z3ZRangesCreation) 137 | def z3ZRangesCreation = { 138 | var x = 100 139 | var y = 100 140 | var z = 100 141 | 142 | while(x < 100){ 143 | while(y < 100){ 144 | while(z < 100){ 145 | var z31 = Z3(x-100, y-100, z-100) 146 | var z32 = Z3(x, y, z) 147 | Z3.zranges(z31, z32) 148 | z += 1 149 | } 150 | y += 1 151 | } 152 | x += 1 153 | } 154 | }*/ 155 | 156 | } 157 | -------------------------------------------------------------------------------- /built.sbt: -------------------------------------------------------------------------------- 1 | import Dependencies._ 2 | 3 | lazy val commonSettings = Seq( 4 | version := Version.sfcurve, 5 | scalaVersion := Version.scala, 6 | crossScalaVersions := Version.crossScala, 7 | description := "SFCurve is a space filling curve library for the JVM.", 8 | organization := "org.locationtech.sfcurve", 9 | licenses := Seq("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0.html")), 10 | homepage := Some(url("https://github.com/locationtech/sfcurve")), 11 | scalacOptions ++= Seq( 12 | "-deprecation", 13 | "-unchecked", 14 | "-language:implicitConversions", 15 | "-language:reflectiveCalls", 16 | "-language:postfixOps", 17 | "-language:existentials", 18 | "-feature"), 19 | publishTo <<= version { (v: String) => 20 | if (v.trim.endsWith("SNAPSHOT")) 21 | Some("Eclipse Snapshot Repository" at "https://repo.eclipse.org/content/repositories/sfcurve-snapshots") 22 | else 23 | Some("Eclipse Repository" at "https://repo.eclipse.org/content/repositories/sfcurve-releases") 24 | }, 25 | 26 | credentials += Credentials(Path.userHome / ".ivy2" / ".credentials"), 27 | //publishTo := Some(Resolver.file("file", new File(Path.userHome.absolutePath+"/.m2/repository"))), 28 | publishMavenStyle := true, 29 | publishArtifact in Test := false, 30 | pomIncludeRepository := { _ => false }, 31 | 32 | pomExtra := ( 33 | 34 | https://github.com/locationtech/sfcurve 35 | scm:https://github.com/locationtech/sfcurve 36 | 37 | 38 | 39 | jnh5y 40 | Jim Hughes 41 | http://github.com/jnh5y/ 42 | 43 | 44 | lossyrob 45 | Rob Emanuele 46 | http://github.com/lossyrob/ 47 | 48 | ), 49 | shellPrompt := { s => Project.extract(s).currentProject.id + " > " } 50 | ) ++ net.virtualvoid.sbt.graph.Plugin.graphSettings 51 | 52 | lazy val root = 53 | Project("sfcurve", file(".")) 54 | .aggregate(api, zorder, hilbert) 55 | .settings(commonSettings: _*) 56 | 57 | lazy val api: Project = 58 | Project("api", file("api")) 59 | .settings(commonSettings: _*) 60 | 61 | lazy val zorder: Project = 62 | Project("zorder", file("zorder")) 63 | .settings(commonSettings: _*) 64 | .dependsOn(api) 65 | 66 | lazy val hilbert: Project = 67 | Project("hilbert", file("hilbert")) 68 | .settings(commonSettings: _*) 69 | .dependsOn(api) 70 | 71 | 72 | lazy val benchmarks: Project = 73 | Project("benchmarks", file("benchmarks")) 74 | .settings(commonSettings: _*) 75 | .dependsOn(api, zorder, hilbert) 76 | -------------------------------------------------------------------------------- /hilbert/build.sbt: -------------------------------------------------------------------------------- 1 | import Dependencies._ 2 | 3 | name := "sfcurve-hilbert" 4 | libraryDependencies ++= Seq( 5 | uzaygezen, 6 | scalaTest % "test" 7 | ) 8 | scalacOptions ++= Seq("-optimize") 9 | -------------------------------------------------------------------------------- /hilbert/src/main/resources/META-INF/services/org.locationtech.sfcurve.SpaceFillingCurveProvider: -------------------------------------------------------------------------------- 1 | org.locationtech.sfcurve.hilbert.HilbertCurve2DProvider -------------------------------------------------------------------------------- /hilbert/src/main/scala/org/locationtech/sfcurve/hilbert/HilbertCurve2D.scala: -------------------------------------------------------------------------------- 1 | /*********************************************************************** 2 | * Copyright (c) 2015 Azavea. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the Apache License, Version 2.0 which 5 | * accompanies this distribution and is available at 6 | * http://www.opensource.org/licenses/apache2.0.php. 7 | ***********************************************************************/ 8 | 9 | package org.locationtech.sfcurve.hilbert 10 | 11 | import java.io.Serializable 12 | 13 | import com.google.common.base.{Function, Functions} 14 | import com.google.common.collect.ImmutableList 15 | import com.google.uzaygezen.core._ 16 | import com.google.uzaygezen.core.ranges._ 17 | import org.locationtech.sfcurve._ 18 | 19 | class HilbertCurve2DProvider extends SpaceFillingCurveProvider { 20 | override def canProvide(name: String): Boolean = "hilbert" == name 21 | 22 | override def build2DSFC(args: Map[String, Serializable]): SpaceFillingCurve2D = 23 | new HilbertCurve2D(args(HilbertCurve2DProvider.RESOLUTION_PARAM).asInstanceOf[Int]) 24 | } 25 | 26 | object HilbertCurve2DProvider { 27 | val RESOLUTION_PARAM = "hilbert.resolution" 28 | } 29 | 30 | class HilbertCurve2D(resolution: Int) extends SpaceFillingCurve2D { 31 | val precision = math.pow(2, resolution).toLong 32 | val chc = new CompactHilbertCurve(Array(resolution, resolution)) 33 | 34 | final def getNormalizedLongitude(x: Double): Long = 35 | ((x + 180) * (precision - 1) / 360d).toLong 36 | // (x * (precision - 1) / 360d).toLong 37 | 38 | final def getNormalizedLatitude(y: Double): Long = 39 | ((y + 90) * (precision - 1) / 180d).toLong 40 | // (y * (precision - 1) / 180d).toLong 41 | 42 | final def setNormalizedLatitude(latNormal: Long) = { 43 | if(!(latNormal >= 0 && latNormal <= precision)) 44 | throw new NumberFormatException("Normalized latitude must be greater than 0 and less than the maximum precision") 45 | 46 | latNormal * 180d / (precision - 1) 47 | } 48 | 49 | final def setNormalizedLongitude(lonNormal: Long) = { 50 | if(!(lonNormal >= 0 && lonNormal <= precision)) 51 | throw new NumberFormatException("Normalized longitude must be greater than 0 and less than the maximum precision") 52 | 53 | lonNormal * 360d / (precision - 1) 54 | } 55 | 56 | 57 | def toIndex(x: Double, y: Double): Long = { 58 | val normX = getNormalizedLongitude(x) 59 | val normY = getNormalizedLatitude(y) 60 | val p = 61 | Array[BitVector]( 62 | BitVectorFactories.OPTIMAL(resolution), 63 | BitVectorFactories.OPTIMAL(resolution) 64 | ) 65 | 66 | p(0).copyFrom(normX) 67 | p(1).copyFrom(normY) 68 | 69 | val hilbert = BitVectorFactories.OPTIMAL.apply(resolution * 2) 70 | 71 | chc.index(p,0,hilbert) 72 | hilbert.toLong 73 | } 74 | 75 | def toPoint(i: Long): (Double, Double) = { 76 | val h = BitVectorFactories.OPTIMAL.apply(resolution*2) 77 | h.copyFrom(i) 78 | val p = 79 | Array[BitVector]( 80 | BitVectorFactories.OPTIMAL(resolution), 81 | BitVectorFactories.OPTIMAL(resolution) 82 | ) 83 | 84 | chc.indexInverse(h,p) 85 | 86 | val x = setNormalizedLongitude(p(0).toLong) - 180 87 | val y = setNormalizedLatitude(p(1).toLong) - 90 88 | (x, y) 89 | } 90 | 91 | def toRanges(xmin: Double, ymin: Double, xmax: Double, ymax: Double, hints: Option[RangeComputeHints] = None): Seq[IndexRange] = { 92 | val chc = new CompactHilbertCurve(Array[Int](resolution, resolution)) 93 | val region = new java.util.ArrayList[LongRange]() 94 | 95 | val minNormalizedLongitude = getNormalizedLongitude(xmin) 96 | val minNormalizedLatitude = getNormalizedLatitude(ymin) 97 | 98 | val maxNormalizedLongitude = getNormalizedLongitude(xmax) 99 | val maxNormalizedLatitude = getNormalizedLatitude(ymax) 100 | 101 | region.add(LongRange.of(minNormalizedLongitude,maxNormalizedLongitude)) 102 | region.add(LongRange.of(minNormalizedLatitude,maxNormalizedLatitude)) 103 | 104 | val zero = new LongContent(0L) 105 | val LongRangeIDFunction: Function[LongRange, LongRange] = Functions.identity() 106 | 107 | val inspector = 108 | SimpleRegionInspector.create( 109 | ImmutableList.of(region), 110 | new LongContent(1L), 111 | LongRangeIDFunction, 112 | LongRangeHome.INSTANCE, 113 | zero 114 | ) 115 | 116 | val combiner = 117 | new PlainFilterCombiner[LongRange, java.lang.Long, LongContent, LongRange](LongRange.of(0, 1)) 118 | 119 | val queryBuilder = BacktrackingQueryBuilder.create(inspector, combiner, Int.MaxValue, true, LongRangeHome.INSTANCE, zero) 120 | 121 | chc.accept(new ZoomingSpaceVisitorAdapter(chc, queryBuilder)) 122 | 123 | val query = queryBuilder.get() 124 | 125 | val ranges = query.getFilteredIndexRanges 126 | 127 | //result 128 | var result = List[IndexRange]() 129 | val itr = ranges.iterator 130 | 131 | while(itr.hasNext) { 132 | val l = itr.next() 133 | val range = l.getIndexRange 134 | val start = range.getStart.asInstanceOf[Long] 135 | val end = range.getEnd.asInstanceOf[Long] 136 | val contained = l.isPotentialOverSelectivity 137 | result = IndexRange(start, end, contained) :: result 138 | } 139 | result 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /hilbert/src/test/scala/org/locationtech/sfcurve/hilbert/HilbertCurveSpec.scala: -------------------------------------------------------------------------------- 1 | /*********************************************************************** 2 | * Copyright (c) 2015 Azavea. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the Apache License, Version 2.0 which 5 | * accompanies this distribution and is available at 6 | * http://www.opensource.org/licenses/apache2.0.php. 7 | ***********************************************************************/ 8 | 9 | package org.locationtech.sfcurve.hilbert 10 | 11 | import org.locationtech.sfcurve.SpaceFillingCurves 12 | import org.scalatest.funspec.AnyFunSpec 13 | import org.scalatest.matchers.should.Matchers 14 | 15 | class HilbertCurveSpec extends AnyFunSpec with Matchers { 16 | 17 | val EPSILON: Double = 1E-3 18 | 19 | describe("SPI access") { 20 | it("resolves a HilbertCurve2D") { 21 | val sfc = SpaceFillingCurves("hilbert", Map(HilbertCurve2DProvider.RESOLUTION_PARAM -> Int.box(10))) 22 | sfc.isInstanceOf[HilbertCurve2D] should equal(true) 23 | } 24 | } 25 | describe("A HilbertCurve implementation using UG lib") { 26 | 27 | it("translates (Double,Double) to Long and Long to (Double, Double)"){ 28 | val resolution = 16 29 | val gridCells = math.pow(2, resolution) 30 | 31 | val sfc = new HilbertCurve2D(resolution) 32 | val index: Long = sfc.toIndex(0.0, 0.0) 33 | 34 | val xEpsilon = 360.0 / gridCells 35 | val yEpsilon = 180.0 / gridCells 36 | 37 | val point = sfc.toPoint(index) 38 | 39 | point._1 should be (-(xEpsilon / 2.0) +- 0.000001) 40 | point._2 should be (-(yEpsilon / 2.0) +- 0.000001) 41 | } 42 | 43 | it("implements a range query"){ 44 | 45 | val sfc = new HilbertCurve2D(3) 46 | val range = sfc.toRanges(-178.123456, -86.398493, 179.3211113, 87.393483) 47 | 48 | range should have length 3 49 | 50 | // the last range is not wholly contained within the query region 51 | val (_, _, lastcontains) = range(2).tuple 52 | lastcontains should be(false) 53 | } 54 | 55 | it("Takes a Long value to a Point (Double, Double)"){ 56 | 57 | val value = 0L 58 | val sfc = new HilbertCurve2D(8) 59 | val point: (Double, Double) = sfc.toPoint(value) 60 | print(point) 61 | 62 | } 63 | 64 | it("Takes a Point (Double, Double) to a Long value"){ 65 | 66 | val sfc = new HilbertCurve2D(8) 67 | val value: Long = sfc.toIndex(0.0,0.0) 68 | print(value) 69 | 70 | } 71 | 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object Dependencies { 4 | def scalaTest = "org.scalatest" %% "scalatest" % "3.2.13" 5 | def uzaygezen = "com.google.uzaygezen" % "uzaygezen-core" % "0.2" 6 | } 7 | -------------------------------------------------------------------------------- /project/Version.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Azavea. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | object Version { 18 | val sfcurve = "0.2.3-SNAPSHOT" 19 | val scala = "2.13.8" 20 | val crossScala = Seq("2.13.8", "2.12.16", "2.11.12") 21 | } 22 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.9 -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.1") 2 | addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.7.5") 3 | -------------------------------------------------------------------------------- /zorder/build.sbt: -------------------------------------------------------------------------------- 1 | import Dependencies._ 2 | 3 | name := "sfcurve-zorder" 4 | libraryDependencies ++= Seq( 5 | scalaTest % "test" 6 | ) 7 | scalacOptions ++= Seq("-optimize") 8 | -------------------------------------------------------------------------------- /zorder/src/main/resources/META-INF/services/org.locationtech.sfcurve.SpaceFillingCurveProvider: -------------------------------------------------------------------------------- 1 | org.locationtech.sfcurve.zorder.ZOrderSFCProvider -------------------------------------------------------------------------------- /zorder/src/main/scala/org/locationtech/sfcurve/zorder/MergeQueue.scala: -------------------------------------------------------------------------------- 1 | /*********************************************************************** 2 | * Copyright (c) 2015 Azavea. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the Apache License, Version 2.0 which 5 | * accompanies this distribution and is available at 6 | * http://www.opensource.org/licenses/apache2.0.php. 7 | ***********************************************************************/ 8 | 9 | package org.locationtech.sfcurve.zorder 10 | 11 | import org.locationtech.sfcurve.IndexRange 12 | 13 | class MergeQueue(initialSize: Int = 1) { 14 | private var array = if(initialSize <= 1) { Array.ofDim[IndexRange](1) } else { Array.ofDim[IndexRange](initialSize) } 15 | private var _size = 0 16 | 17 | def size = _size 18 | 19 | private def removeElement(i: Int): Unit = { 20 | if(i < _size - 1) { 21 | val result = array.clone 22 | System.arraycopy(array, i + 1, result, i, _size - i - 1) 23 | array = result 24 | } 25 | _size = _size - 1 26 | } 27 | 28 | private def insertElement(range: IndexRange, i: Int): Unit = { 29 | ensureSize(_size + 1) 30 | if(i == _size) { 31 | array(i) = range 32 | } else { 33 | val result = array.clone 34 | System.arraycopy(array, 0, result, 0, i) 35 | System.arraycopy(array, i, result, i + 1, _size - i) 36 | result(i) = range 37 | array = result 38 | } 39 | _size += 1 40 | } 41 | 42 | 43 | /** Ensure that the internal array has at least `n` cells. */ 44 | protected def ensureSize(n: Int): Unit = { 45 | // Use a Long to prevent overflows 46 | val arrayLength: Long = array.length 47 | if (n > arrayLength - 1) { 48 | var newSize: Long = arrayLength * 2 49 | while (n > newSize) { 50 | newSize = newSize * 2 51 | } 52 | // Clamp newSize to Int.MaxValue 53 | if (newSize > Int.MaxValue) newSize = Int.MaxValue 54 | 55 | val newArray: Array[IndexRange] = new Array(newSize.toInt) 56 | System.arraycopy(array, 0, newArray, 0, _size) 57 | array = newArray 58 | } 59 | } 60 | 61 | val ordering = IndexRange.IndexRangeIsOrdered 62 | 63 | /** Inserts a single range into the priority queue. 64 | * 65 | * @param range the element to insert. 66 | */ 67 | final def +=(range: IndexRange): Unit = { 68 | val res = if(_size == 0) { -1 } else { java.util.Arrays.binarySearch(array, 0, _size, range, ordering) } 69 | if(res < 0) { 70 | val i = -(res + 1) 71 | var (thisStart, thisEnd, contained) = range.tuple 72 | var removeLeft = false 73 | 74 | var removeRight = false 75 | var rightRemainder: Option[IndexRange] = None 76 | 77 | // Look at the left range 78 | if(i != 0) { 79 | val (prevStart, prevEnd, b) = array(i - 1).tuple 80 | if(prevStart == thisStart) { 81 | contained = contained && b 82 | removeLeft = true 83 | } 84 | if (prevEnd + 1 >= thisStart) { 85 | contained = contained && b 86 | removeLeft = true 87 | thisStart = prevStart 88 | if(prevEnd > thisEnd) { 89 | thisEnd = prevEnd 90 | } 91 | } 92 | } 93 | 94 | // Look at the right range 95 | if(i < _size && _size > 0) { 96 | val (nextStart, nextEnd, b) = array(i).tuple 97 | if(thisStart == nextStart) { 98 | removeRight = true 99 | thisEnd = nextEnd 100 | contained = contained && b 101 | } else { 102 | if(thisEnd + 1 >= nextStart) { 103 | removeRight = true 104 | contained = contained && b 105 | if(nextEnd - 1 >= thisEnd) { 106 | thisEnd = nextEnd 107 | } else if (nextEnd < thisEnd - 1) { 108 | rightRemainder = Some(IndexRange(nextEnd + 1, thisEnd, contained && b)) 109 | thisEnd = nextEnd 110 | } 111 | } 112 | } 113 | } 114 | 115 | if(removeRight) { 116 | if(!removeLeft) { 117 | array(i) = IndexRange(thisStart, thisEnd, contained) 118 | } else { 119 | array(i-1) = IndexRange(thisStart, thisEnd, contained) 120 | removeElement(i) 121 | } 122 | } else if(removeLeft) { 123 | array(i-1) = IndexRange(thisStart, thisEnd, contained) 124 | } else { 125 | insertElement(range, i) 126 | } 127 | 128 | rightRemainder match { 129 | case Some(r) => this += r 130 | case None => 131 | } 132 | } 133 | } 134 | 135 | def toSeq: Seq[IndexRange] = 136 | Seq.tabulate[IndexRange](size)(array.apply) 137 | } 138 | -------------------------------------------------------------------------------- /zorder/src/main/scala/org/locationtech/sfcurve/zorder/Z2.scala: -------------------------------------------------------------------------------- 1 | /*********************************************************************** 2 | * Copyright (c) 2015 Azavea. 3 | * Copyright (c) 2013-2015 Commonwealth Computer Research, Inc. 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License, Version 2.0 which 6 | * accompanies this distribution and is available at 7 | * http://www.opensource.org/licenses/apache2.0.php. 8 | ***********************************************************************/ 9 | 10 | package org.locationtech.sfcurve.zorder 11 | 12 | class Z2(val z: Long) extends AnyVal { 13 | import Z2._ 14 | 15 | def < (other: Z2) = z < other.z 16 | def > (other: Z2) = z > other.z 17 | 18 | def <= (other: Z2) = z <= other.z 19 | def >= (other: Z2) = z >= other.z 20 | 21 | def + (offset: Long) = new Z2(z + offset) 22 | def - (offset: Long) = new Z2(z - offset) 23 | 24 | def == (other: Z2) = other.z == z 25 | 26 | def decode: (Int, Int) = (combine(z), combine(z>>1)) 27 | 28 | def dim(i: Int) = Z2.combine(z >> i) 29 | 30 | def d0 = dim(0) 31 | def d1 = dim(1) 32 | 33 | def mid(p: Z2): Z2 = { 34 | val (x, y) = decode 35 | val (px, py) = p.decode 36 | Z2((x + px) >>> 1, (y + py) >>> 1) // overflow safe mean 37 | } 38 | 39 | def bitsToString = f"(${z.toBinaryString}%16s)(${dim(0).toBinaryString}%8s,${dim(1).toBinaryString}%8s)" 40 | override def toString = f"$z $decode" 41 | } 42 | 43 | object Z2 extends ZN { 44 | 45 | override val Dimensions = 2 46 | override val BitsPerDimension = 31 47 | override val TotalBits = 62 48 | override val MaxMask = 0x7fffffffL // ignore the sign bit, using it breaks < relationship 49 | 50 | def apply(zvalue: Long): Z2 = new Z2(zvalue) 51 | 52 | // Bits of x and y will be encoded as ....y1x1y0x0 53 | def apply(x: Int, y: Int): Z2 = new Z2(split(x) | split(y) << 1) 54 | 55 | def unapply(z: Z2): Option[(Int, Int)] = Some(z.decode) 56 | 57 | /** insert 0 between every bit in value. Only first 31 bits can be considered. */ 58 | override def split(value: Long): Long = { 59 | var x: Long = value & MaxMask 60 | x = (x ^ (x << 32)) & 0x00000000ffffffffL 61 | x = (x ^ (x << 16)) & 0x0000ffff0000ffffL 62 | x = (x ^ (x << 8)) & 0x00ff00ff00ff00ffL // 11111111000000001111111100000000.. 63 | x = (x ^ (x << 4)) & 0x0f0f0f0f0f0f0f0fL // 1111000011110000 64 | x = (x ^ (x << 2)) & 0x3333333333333333L // 11001100.. 65 | x = (x ^ (x << 1)) & 0x5555555555555555L // 1010... 66 | x 67 | } 68 | 69 | /** combine every other bit to form a value. Maximum value is 31 bits. */ 70 | override def combine(z: Long): Int = { 71 | var x = z & 0x5555555555555555L 72 | x = (x ^ (x >> 1)) & 0x3333333333333333L 73 | x = (x ^ (x >> 2)) & 0x0f0f0f0f0f0f0f0fL 74 | x = (x ^ (x >> 4)) & 0x00ff00ff00ff00ffL 75 | x = (x ^ (x >> 8)) & 0x0000ffff0000ffffL 76 | x = (x ^ (x >> 16)) & 0x00000000ffffffffL 77 | x.toInt 78 | } 79 | 80 | override def contains(range: ZRange, value: Long): Boolean = { 81 | val (x, y) = Z2(value).decode 82 | x >= Z2(range.min).d0 && x <= Z2(range.max).d0 && y >= Z2(range.min).d1 && y <= Z2(range.max).d1 83 | } 84 | 85 | override def overlaps(range: ZRange, value: ZRange): Boolean = { 86 | def overlaps(a1: Int, a2: Int, b1: Int, b2: Int) = math.max(a1, b1) <= math.min(a2, b2) 87 | overlaps(Z2(range.min).d0, Z2(range.max).d0, Z2(value.min).d0, Z2(value.max).d0) && 88 | overlaps(Z2(range.min).d1, Z2(range.max).d1, Z2(value.min).d1, Z2(value.max).d1) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /zorder/src/main/scala/org/locationtech/sfcurve/zorder/Z3.scala: -------------------------------------------------------------------------------- 1 | /*********************************************************************** 2 | * Copyright (c) 2013-2015 Commonwealth Computer Research, Inc. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the Apache License, Version 2.0 which 5 | * accompanies this distribution and is available at 6 | * http://www.opensource.org/licenses/apache2.0.php. 7 | *************************************************************************/ 8 | package org.locationtech.sfcurve.zorder 9 | 10 | class Z3(val z: Long) extends AnyVal { 11 | import Z3._ 12 | 13 | def < (other: Z3) = z < other.z 14 | def > (other: Z3) = z > other.z 15 | def >= (other: Z3) = z >= other.z 16 | def <= (other: Z3) = z <= other.z 17 | def + (offset: Long) = new Z3(z + offset) 18 | def - (offset: Long) = new Z3(z - offset) 19 | def == (other: Z3) = other.z == z 20 | 21 | def d0 = combine(z) 22 | def d1 = combine(z >> 1) 23 | def d2 = combine(z >> 2) 24 | 25 | def decode: (Int, Int, Int) = (d0, d1, d2) 26 | 27 | def dim(i: Int): Int = if (i == 0) d0 else if (i == 1) d1 else if (i == 2) d2 else { 28 | throw new IllegalArgumentException(s"Invalid dimension $i - valid dimensions are 0,1,2") 29 | } 30 | 31 | def inRange(rmin: Z3, rmax: Z3): Boolean = { 32 | val (x, y, z) = decode 33 | x >= rmin.d0 && 34 | x <= rmax.d0 && 35 | y >= rmin.d1 && 36 | y <= rmax.d1 && 37 | z >= rmin.d2 && 38 | z <= rmax.d2 39 | } 40 | 41 | def mid(p: Z3): Z3 = { 42 | val (x, y, z) = decode 43 | val (px, py, pz) = p.decode 44 | Z3((x + px) >>> 1, (y + py) >>> 1, (z + pz) >>> 1) // overflow safe mean 45 | } 46 | 47 | def bitsToString = f"(${z.toBinaryString.toLong}%016d)(${d0.toBinaryString.toLong}%08d,${d1.toBinaryString.toLong}%08d,${d2.toBinaryString.toLong}%08d)" 48 | override def toString = f"$z $decode" 49 | } 50 | 51 | object Z3 extends ZN { 52 | 53 | override val Dimensions = 3 54 | override val BitsPerDimension = 21 55 | override val TotalBits = 63 56 | override val MaxMask = 0x1fffffL 57 | 58 | def apply(zvalue: Long) = new Z3(zvalue) 59 | 60 | /** 61 | * So this represents the order of the tuple, but the bits will be encoded in reverse order: 62 | * ....z1y1x1z0y0x0 63 | * This is a little confusing. 64 | */ 65 | def apply(x: Int, y: Int, z: Int): Z3 = { 66 | new Z3(split(x) | split(y) << 1 | split(z) << 2) 67 | } 68 | 69 | def unapply(z: Z3): Option[(Int, Int, Int)] = Some(z.decode) 70 | 71 | /** insert 00 between every bit in value. Only first 21 bits can be considered. */ 72 | override def split(value: Long): Long = { 73 | var x = value & MaxMask 74 | x = (x | x << 32) & 0x1f00000000ffffL 75 | x = (x | x << 16) & 0x1f0000ff0000ffL 76 | x = (x | x << 8) & 0x100f00f00f00f00fL 77 | x = (x | x << 4) & 0x10c30c30c30c30c3L 78 | (x | x << 2) & 0x1249249249249249L 79 | } 80 | 81 | /** combine every third bit to form a value. Maximum value is 21 bits. */ 82 | override def combine(z: Long): Int = { 83 | var x = z & 0x1249249249249249L 84 | x = (x ^ (x >> 2)) & 0x10c30c30c30c30c3L 85 | x = (x ^ (x >> 4)) & 0x100f00f00f00f00fL 86 | x = (x ^ (x >> 8)) & 0x1f0000ff0000ffL 87 | x = (x ^ (x >> 16)) & 0x1f00000000ffffL 88 | x = (x ^ (x >> 32)) & MaxMask 89 | x.toInt 90 | } 91 | 92 | override def contains(range: ZRange, value: Long): Boolean = { 93 | val (x, y, z) = Z3(value).decode 94 | x >= Z3(range.min).d0 && x <= Z3(range.max).d0 && 95 | y >= Z3(range.min).d1 && y <= Z3(range.max).d1 && 96 | z >= Z3(range.min).d2 && z <= Z3(range.max).d2 97 | } 98 | 99 | override def overlaps(range: ZRange, value: ZRange): Boolean = { 100 | def overlaps(a1: Int, a2: Int, b1: Int, b2: Int) = math.max(a1, b1) <= math.min(a2, b2) 101 | overlaps(Z3(range.min).d0, Z3(range.max).d0, Z3(value.min).d0, Z3(value.max).d0) && 102 | overlaps(Z3(range.min).d1, Z3(range.max).d1, Z3(value.min).d1, Z3(value.max).d1) && 103 | overlaps(Z3(range.min).d2, Z3(range.max).d2, Z3(value.min).d2, Z3(value.max).d2) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /zorder/src/main/scala/org/locationtech/sfcurve/zorder/ZCurve2D.scala: -------------------------------------------------------------------------------- 1 | /*********************************************************************** 2 | * Copyright (c) 2015 Azavea. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the Apache License, Version 2.0 which 5 | * accompanies this distribution and is available at 6 | * http://www.opensource.org/licenses/apache2.0.php. 7 | ***********************************************************************/ 8 | 9 | package org.locationtech.sfcurve.zorder 10 | 11 | import org.locationtech.sfcurve._ 12 | 13 | import scala.util.Try 14 | 15 | /** Represents a 2D Z order curve that we will use for benchmarking purposes in the early stages. 16 | * 17 | * @param resolution The number of cells in each dimension of the grid space that will be indexed. 18 | */ 19 | class ZCurve2D(resolution: Int) extends SpaceFillingCurve2D { 20 | // We are assuming Lat Lng extent for the whole world 21 | val xmin = -180.0 22 | val ymin = -90.0 23 | val xmax = 180.0 24 | val ymax = 90.0 25 | 26 | // Code taken from geotrellis.raster.RasterExtent 27 | val cellwidth = (xmax - xmin) / resolution 28 | val cellheight = (ymax - ymin) / resolution 29 | 30 | def mapToCol(x: Double) = 31 | ((x - xmin) / cellwidth).toInt 32 | 33 | def mapToRow(y: Double) = 34 | ((ymax - y) / cellheight).toInt 35 | 36 | def colToMap(col: Int) = 37 | math.max(math.min(col * cellwidth + xmin + (cellwidth / 2), xmax), xmin) 38 | 39 | def rowToMap(row: Int) = 40 | math.min(math.max(ymax - (row * cellheight) - (cellheight / 2), ymin), ymax) 41 | 42 | 43 | def toIndex(x: Double, y: Double): Long = { 44 | val col = mapToCol(x) 45 | val row = mapToRow(y) 46 | Z2(col, row).z 47 | } 48 | 49 | def toPoint(i: Long): (Double, Double) = { 50 | val (col, row) = new Z2(i).decode 51 | (colToMap(col), rowToMap(row)) 52 | } 53 | 54 | def bound(i: Long): (Double, Double, Double, Double) = { 55 | val (col, row) = new Z2(i).decode 56 | val x = colToMap(col) 57 | val y = rowToMap(row) 58 | (validateX(x - cellwidth), validateY(y-cellheight), validateX(x+cellwidth), validateY(y+cellheight)) 59 | } 60 | 61 | def validateX(x: Double) = math.min(math.max(xmin, x), xmax) 62 | def validateY(y: Double) = math.min(math.max(ymin, y), ymax) 63 | 64 | def toRanges(xmin: Double, ymin: Double, xmax: Double, ymax: Double, hints: Option[RangeComputeHints] = None): Seq[IndexRange] = { 65 | val colMin = mapToCol(xmin) 66 | val rowMin = mapToRow(ymax) 67 | val min = Z2(colMin, rowMin) 68 | val colMax = mapToCol(xmax) 69 | val rowMax = mapToRow(ymin) 70 | val max = Z2(colMax, rowMax) 71 | 72 | val maxRecurse = for { 73 | hint <- hints 74 | recurse <- Option(hint.get(ZCurve2D.MAX_RECURSE)) 75 | asInt <- Try(recurse.asInstanceOf[Int]).toOption 76 | } yield { 77 | asInt 78 | } 79 | 80 | Z2.zranges(Array(ZRange(min, max)), maxRecurse = maxRecurse) 81 | } 82 | } 83 | 84 | object ZCurve2D { 85 | val DEFAULT_MAX_RECURSION = 32 86 | val MAX_RECURSE = "zorder.max.recurse" 87 | 88 | def hints(maxRecurse: Int) = { 89 | val hints = new RangeComputeHints() 90 | hints.put(MAX_RECURSE, Int.box(maxRecurse)) 91 | Some(hints) 92 | } 93 | } -------------------------------------------------------------------------------- /zorder/src/main/scala/org/locationtech/sfcurve/zorder/ZN.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013-2016 Commonwealth Computer Research, Inc. 3 | * Copyright (c) 2015 Azavea. 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License, Version 2.0 which 6 | * accompanies this distribution and is available at 7 | * http://www.opensource.org/licenses/apache2.0.php. 8 | */ 9 | 10 | package org.locationtech.sfcurve.zorder 11 | 12 | import org.locationtech.sfcurve.IndexRange 13 | 14 | import scala.collection.mutable.ArrayBuffer 15 | 16 | /** 17 | * N-dimensional z-curve base class 18 | */ 19 | abstract class ZN { 20 | 21 | // number of bits used to store each dimension 22 | def BitsPerDimension: Int 23 | 24 | // number of dimensions 25 | def Dimensions: Int 26 | 27 | // max value for this z object - can be used to mask another long using & 28 | def MaxMask: Long 29 | 30 | // total bits used - usually bitsPerDim * dims 31 | def TotalBits: Int 32 | 33 | // number of quadrants in our quad/oct tree - has to be lazy to instantiate correctly 34 | private lazy val Quadrants = math.pow(2, Dimensions) 35 | 36 | /** 37 | * Insert (Dimensions - 1) zeros between each bit to create a zvalue from a single dimension. 38 | * Only the first BitsPerDimension can be considered. 39 | * 40 | * @param value value to split 41 | * @return 42 | */ 43 | def split(value: Long): Long 44 | 45 | /** 46 | * Combine every (Dimensions - 1) bits to re-create a single dimension. Opposite of split. 47 | * 48 | * @param z value to combine 49 | * @return 50 | */ 51 | def combine(z: Long): Int 52 | 53 | /** 54 | * Is the value contained in the range. Considers user-space. 55 | * 56 | * @param range range 57 | * @param value value to be tested 58 | * @return 59 | */ 60 | def contains(range: ZRange, value: Long): Boolean 61 | 62 | /** 63 | * Is the value contained in the range. Considers user-space. 64 | * 65 | * @param range range 66 | * @param value value to be tested 67 | * @return 68 | */ 69 | def contains(range: ZRange, value: ZRange): Boolean = contains(range, value.min) && contains(range, value.max) 70 | 71 | /** 72 | * Does the value overlap with the range. Considers user-space. 73 | * 74 | * @param range range 75 | * @param value value to be tested 76 | * @return 77 | */ 78 | def overlaps(range: ZRange, value: ZRange): Boolean 79 | 80 | /** 81 | * Returns (litmax, bigmin) for the given range and point 82 | * 83 | * @param p point 84 | * @param rmin minimum value 85 | * @param rmax maximum value 86 | * @return (litmax, bigmin) 87 | */ 88 | def zdivide(p: Long, rmin: Long, rmax: Long): (Long, Long) = ZN.zdiv(load, Dimensions)(p, rmin, rmax) 89 | 90 | def zranges(zbounds: ZRange): Seq[IndexRange] = zranges(Array(zbounds)) 91 | def zranges(zbounds: ZRange, precision: Int): Seq[IndexRange] = zranges(Array(zbounds), precision) 92 | def zranges(zbounds: ZRange, precision: Int, maxRanges: Option[Int]): Seq[IndexRange] = 93 | zranges(Array(zbounds), precision, maxRanges) 94 | 95 | /** 96 | * Calculates ranges in index space that match any of the input bounds. Uses breadth-first searching to 97 | * allow a limit on the number of ranges returned. 98 | * 99 | * To improve performance, the following decisions have been made: 100 | * uses loops instead of foreach/maps 101 | * uses java queues instead of scala queues 102 | * allocates initial sequences of decent size 103 | * sorts once at the end before merging 104 | * 105 | * @param zbounds search space 106 | * @param precision precision to consider, in bits (max 64) 107 | * @param maxRanges loose cap on the number of ranges to return. A higher number of ranges will have less 108 | * false positives, but require more processing. 109 | * @param maxRecurse max levels of recursion to apply before stopping 110 | * @return ranges covering the search space 111 | */ 112 | def zranges(zbounds: Array[ZRange], 113 | precision: Int = 64, 114 | maxRanges: Option[Int] = None, 115 | maxRecurse: Option[Int] = Some(ZN.DefaultRecurse)): Seq[IndexRange] = { 116 | 117 | import ZN.LevelTerminator 118 | 119 | // stores our results - initial size of 100 in general saves us some re-allocation 120 | val ranges = new java.util.ArrayList[IndexRange](100) 121 | 122 | // values remaining to process - initial size of 100 in general saves us some re-allocation 123 | val remaining = new java.util.ArrayDeque[(Long, Long)](100) 124 | 125 | // calculate the common prefix in the z-values - we start processing with the first diff 126 | val ZPrefix(commonPrefix, commonBits) = longestCommonPrefix(zbounds.flatMap(b => Seq(b.min, b.max)): _*) 127 | 128 | var offset = 64 - commonBits 129 | 130 | // checks if a range is contained in the search space 131 | def isContained(range: ZRange): Boolean = { 132 | var i = 0 133 | while (i < zbounds.length) { 134 | if (contains(zbounds(i), range)) { 135 | return true 136 | } 137 | i += 1 138 | } 139 | false 140 | } 141 | 142 | // checks if a range overlaps the search space 143 | def isOverlapped(range: ZRange): Boolean = { 144 | var i = 0 145 | while (i < zbounds.length) { 146 | if (overlaps(zbounds(i), range)) { 147 | return true 148 | } 149 | i += 1 150 | } 151 | false 152 | } 153 | 154 | // checks a single value and either: 155 | // eliminates it as out of bounds 156 | // adds it to our results as fully matching, or 157 | // queues up it's children for further processing 158 | def checkValue(prefix: Long, quadrant: Long): Unit = { 159 | val min: Long = prefix | (quadrant << offset) // QR + 000... 160 | val max: Long = min | (1L << offset) - 1 // QR + 111... 161 | val quadrantRange = ZRange(min, max) 162 | 163 | if (isContained(quadrantRange) || offset < 64 - precision) { 164 | // whole range matches, happy day 165 | ranges.add(IndexRange(min, max, contained = true)) 166 | } else if (isOverlapped(quadrantRange)) { 167 | // some portion of this range is excluded 168 | // queue up each sub-range for processing 169 | remaining.add((min, max)) 170 | } 171 | } 172 | 173 | // bottom out and get all the ranges that partially overlapped but we didn't fully process 174 | // note: this method is only called when we know there are items remaining in the queue 175 | def bottomOut(): Unit = { 176 | do { 177 | val minMax = remaining.poll 178 | if (!minMax.eq(LevelTerminator)) { 179 | ranges.add(IndexRange(minMax._1, minMax._2, contained = false)) 180 | } 181 | } while (!remaining.isEmpty) 182 | } 183 | 184 | // initial level - we just check the single quadrant 185 | checkValue(commonPrefix, 0) 186 | remaining.add(LevelTerminator) 187 | offset -= Dimensions 188 | 189 | // level of recursion 190 | var level = 0 191 | 192 | val rangeStop = maxRanges.getOrElse(Int.MaxValue) 193 | val recurseStop = maxRecurse.getOrElse(ZN.DefaultRecurse) 194 | 195 | do { 196 | val next = remaining.poll 197 | if (next.eq(LevelTerminator)) { 198 | // we've fully processed a level, increment our state 199 | if (!remaining.isEmpty) { 200 | level += 1 201 | offset -= Dimensions 202 | if (level >= recurseStop || offset < 0) { 203 | bottomOut() 204 | } else { 205 | remaining.add(LevelTerminator) 206 | } 207 | } 208 | } else { 209 | val prefix = next._1 210 | var quadrant = 0L 211 | while (quadrant < Quadrants) { 212 | checkValue(prefix, quadrant) 213 | quadrant += 1 214 | } 215 | // subtract one from remaining.size to account for the LevelTerminator 216 | if (ranges.size + remaining.size - 1 >= rangeStop) { 217 | bottomOut() 218 | } 219 | } 220 | } while (!remaining.isEmpty) 221 | 222 | // we've got all our ranges - now reduce them down by merging overlapping values 223 | ranges.sort(IndexRange.IndexRangeIsOrdered) 224 | 225 | var current = ranges.get(0) // note: should always be at least one range 226 | val result = ArrayBuffer.empty[IndexRange] 227 | var i = 1 228 | while (i < ranges.size()) { 229 | val range = ranges.get(i) 230 | if (range.lower <= current.upper + 1) { 231 | // merge the two ranges 232 | current = IndexRange(current.lower, math.max(current.upper, range.upper), current.contained && range.contained) 233 | } else { 234 | // append the last range and set the current range for future merging 235 | result.append(current) 236 | current = range 237 | } 238 | i += 1 239 | } 240 | // append the last range - there will always be one left that wasn't added 241 | result.append(current) 242 | 243 | result.toSeq 244 | } 245 | 246 | /** 247 | * Cuts Z-Range in two and trims based on user space, can be used to perform augmented binary search 248 | * 249 | * @param xd: division point 250 | * @param inRange: is xd in query range 251 | */ 252 | def cut(r: ZRange, xd: Long, inRange: Boolean): List[ZRange] = { 253 | if (r.min == r.max) { 254 | Nil 255 | } else if (inRange) { 256 | if (xd == r.min) { // degenerate case, two nodes min has already been counted 257 | ZRange(r.max, r.max) :: Nil 258 | } else if (xd == r.max) { // degenerate case, two nodes max has already been counted 259 | ZRange(r.min, r.min) :: Nil 260 | } else { 261 | ZRange(r.min, xd - 1) :: ZRange(xd + 1, r.max) :: Nil 262 | } 263 | } else { 264 | val (litmax, bigmin) = zdivide(xd, r.min, r.max) 265 | ZRange(r.min, litmax) :: ZRange(bigmin, r.max) :: Nil 266 | } 267 | } 268 | 269 | /** 270 | * Calculates the longest common binary prefix between two z longs 271 | * 272 | * @return (common prefix, number of bits in common) 273 | */ 274 | def longestCommonPrefix(values: Long*): ZPrefix = { 275 | var bitShift = TotalBits - Dimensions 276 | var head = values.head >>> bitShift 277 | while (values.tail.forall(v => (v >>> bitShift) == head) && bitShift > -1) { 278 | bitShift -= Dimensions 279 | head = values.head >>> bitShift 280 | } 281 | bitShift += Dimensions // increment back to the last valid value 282 | ZPrefix(values.head & (Long.MaxValue << bitShift), 64 - bitShift) 283 | } 284 | 285 | /** Loads either 1000... or 0111... into starting at given bit index of a given dimension */ 286 | private def load(target: Long, p: Long, bits: Int, dim: Int): Long = { 287 | val mask = ~(split(MaxMask >> (BitsPerDimension - bits)) << dim) 288 | val wiped = target & mask 289 | wiped | (split(p) << dim) 290 | } 291 | } 292 | 293 | object ZN { 294 | 295 | val DefaultRecurse = 7 296 | 297 | // indicator that we have searched a full level of the quad/oct tree 298 | private val LevelTerminator = (-1L, -1L) 299 | 300 | /** 301 | * Implements the the algorithm defined in: Tropf paper to find: 302 | * LITMAX: maximum z-index in query range smaller than current point, xd 303 | * BIGMIN: minimum z-index in query range greater than current point, xd 304 | * 305 | * @param load: function that knows how to load bits into appropraite dimension of a z-index 306 | * @param xd: z-index that is outside of the query range 307 | * @param rmin: minimum z-index of the query range, inclusive 308 | * @param rmax: maximum z-index of the query range, inclusive 309 | * @return (LITMAX, BIGMIN) 310 | */ 311 | private [zorder] def zdiv(load: (Long, Long, Int, Int) => Long, dims: Int) 312 | (xd: Long, rmin: Long, rmax: Long): (Long, Long) = { 313 | require(rmin < rmax, "min ($rmin) must be less than max $(rmax)") 314 | var zmin: Long = rmin 315 | var zmax: Long = rmax 316 | var bigmin: Long = 0L 317 | var litmax: Long = 0L 318 | 319 | def bit(x: Long, idx: Int) = { 320 | ((x & (1L << idx)) >> idx).toInt 321 | } 322 | def over(bits: Long) = 1L << (bits - 1) 323 | def under(bits: Long) = (1L << (bits - 1)) - 1 324 | 325 | var i = 64 326 | while (i > 0) { 327 | i -= 1 328 | 329 | val bits = i/dims+1 330 | val dim = i%dims 331 | 332 | ( bit(xd, i), bit(zmin, i), bit(zmax, i) ) match { 333 | case (0, 0, 0) => 334 | // continue 335 | 336 | case (0, 0, 1) => 337 | zmax = load(zmax, under(bits), bits, dim) 338 | bigmin = load(zmin, over(bits), bits, dim) 339 | 340 | case (0, 1, 0) => 341 | // sys.error(s"Not possible, MIN <= MAX, (0, 1, 0) at index $i") 342 | 343 | case (0, 1, 1) => 344 | bigmin = zmin 345 | return (litmax, bigmin) 346 | 347 | case (1, 0, 0) => 348 | litmax = zmax 349 | return (litmax, bigmin) 350 | 351 | case (1, 0, 1) => 352 | litmax = load(zmax, under(bits), bits, dim) 353 | zmin = load(zmin, over(bits), bits, dim) 354 | 355 | case (1, 1, 0) => 356 | // sys.error(s"Not possible, MIN <= MAX, (1, 1, 0) at index $i") 357 | 358 | case (1, 1, 1) => 359 | // continue 360 | } 361 | } 362 | (litmax, bigmin) 363 | } 364 | } 365 | 366 | case class ZPrefix(prefix: Long, precision: Int) // precision in bits 367 | -------------------------------------------------------------------------------- /zorder/src/main/scala/org/locationtech/sfcurve/zorder/ZOrderSFCProvider.scala: -------------------------------------------------------------------------------- 1 | package org.locationtech.sfcurve.zorder 2 | 3 | import org.locationtech.sfcurve.{SpaceFillingCurve2D, SpaceFillingCurveProvider} 4 | 5 | class ZOrderSFCProvider extends SpaceFillingCurveProvider { 6 | override def canProvide(name: String): Boolean = name == "zorder" 7 | 8 | override def build2DSFC(args: Map[String, java.io.Serializable]): SpaceFillingCurve2D = 9 | new ZCurve2D(args(ZOrderSFCProvider.RESOLUTION_PARAM).asInstanceOf[Int]) 10 | } 11 | 12 | object ZOrderSFCProvider { 13 | val RESOLUTION_PARAM = "zorder.resolution" 14 | } -------------------------------------------------------------------------------- /zorder/src/main/scala/org/locationtech/sfcurve/zorder/ZRange.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013-2016 Commonwealth Computer Research, Inc. 3 | * Copyright (c) 2015 Azavea. 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License, Version 2.0 which 6 | * accompanies this distribution and is available at 7 | * http://www.opensource.org/licenses/apache2.0.php. 8 | */ 9 | 10 | package org.locationtech.sfcurve.zorder 11 | 12 | /** 13 | * Represents a rectangle in defined by min and max as two opposing points 14 | * 15 | * @param min: lower-left point 16 | * @param max: upper-right point 17 | */ 18 | case class ZRange(min: Long, max: Long) { 19 | 20 | require(min <= max, s"Range bounds must be ordered, but $min > $max") 21 | 22 | def mid: Long = (max + min) >>> 1 // overflow safe mean 23 | 24 | def length: Long = max - min + 1 25 | 26 | // contains in index space (e.g. the long value) 27 | def contains(bits: Long): Boolean = bits >= min && bits <= max 28 | 29 | // contains in index space (e.g. the long value) 30 | def contains(r: ZRange): Boolean = contains(r.min) && contains(r.max) 31 | 32 | // overlaps in index space (e.g. the long value) 33 | def overlaps(r: ZRange): Boolean = contains(r.min) || contains(r.max) 34 | } 35 | 36 | object ZRange { 37 | def apply(min: Z2, max: Z2)(implicit d: DummyImplicit): ZRange = ZRange(min.z, max.z) 38 | def apply(min: Z3, max: Z3)(implicit d1: DummyImplicit, d2: DummyImplicit): ZRange = ZRange(min.z, max.z) 39 | } 40 | -------------------------------------------------------------------------------- /zorder/src/test/scala/org/locationtech/sfcurve/zorder/Z2IteratorSpec.scala: -------------------------------------------------------------------------------- 1 | /*********************************************************************** 2 | * Copyright (c) 2015 Azavea. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the Apache License, Version 2.0 which 5 | * accompanies this distribution and is available at 6 | * http://www.opensource.org/licenses/apache2.0.php. 7 | ***********************************************************************/ 8 | 9 | package org.locationtech.sfcurve.zorder 10 | 11 | import org.scalatest.funspec.AnyFunSpec 12 | import org.scalatest.matchers.should.Matchers 13 | 14 | class Z2IteratorSpec extends AnyFunSpec with Matchers { 15 | describe("Z2IteratorRange") { 16 | 17 | it("iterates"){ 18 | val min = Z2(5,3) 19 | val max = Z2(10,5) 20 | val range = ZRange(min, max) 21 | val it = new ZdivideIterator(min, max) 22 | 23 | it foreach { z2: Z2 => 24 | range.contains(z2.z) 25 | } 26 | } 27 | } 28 | 29 | describe("ZCurve2D"){ 30 | it("creates a bounding box"){ 31 | val sfc = new ZCurve2D(2) 32 | val range = sfc.toRanges(-178.123456, -86.398493, 179.3211113, 87.393483) 33 | 34 | range should have length 1 35 | } 36 | } 37 | } 38 | /** 39 | * Base iterator that is able to seek forward to some index. 40 | * This iterator will eventually hit z values that are outside of range defined by min and max 41 | */ 42 | class Z2Iterator(min: Z2, max: Z2) extends Iterator[Z2] { 43 | private var cur = min 44 | 45 | def hasNext: Boolean = cur.z <= max.z 46 | 47 | def next: Z2 = { 48 | val ret = cur 49 | cur += 1 50 | ret 51 | } 52 | 53 | def seek(min: Z2, max: Z2) = { 54 | cur = min 55 | } 56 | } 57 | 58 | /** 59 | * Iterator that uses zdivide to decide when to seek forward. 60 | * As we encounter z values outside of our query range we decide how many misses we can sustain 61 | * before using zdivide to seek forward. The assumption that there is some number of calls to .next() 62 | * that will equal a cost of .seek(). 63 | * 64 | * This is a mock class. 65 | */ 66 | case class ZdivideIterator(min: Z2, max: Z2) extends Z2Iterator(min, max) { 67 | val MAX_MISSES = 10 68 | val range = ZRange(min, max) 69 | var haveNext = false 70 | var _next: Z2 = new Z2(0) 71 | 72 | advance 73 | 74 | override def hasNext: Boolean = haveNext 75 | 76 | override def next: Z2 = { 77 | // it's safe to report cur, because we've advanced to it and hasNext has been called. 78 | val ret = _next 79 | advance 80 | ret 81 | } 82 | 83 | /** 84 | * Two possible post-conditions: 85 | * 1. cur is set to a valid object 86 | * 2. cur is set to null and source.hasNext == false 87 | */ 88 | def advance: Unit = { 89 | var misses = 0 90 | while (misses < MAX_MISSES && super.hasNext) { 91 | _next = super.next 92 | if (range.contains(_next.z)) { 93 | haveNext = true 94 | return 95 | } else { 96 | misses + 1 97 | } 98 | } 99 | 100 | if (_next < max) { 101 | val (litmax, bigmin) = Z2.zdivide(_next.z, min.z, max.z) 102 | _next = Z2(bigmin) 103 | haveNext = true 104 | } else { 105 | haveNext = false 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /zorder/src/test/scala/org/locationtech/sfcurve/zorder/Z2Spec.scala: -------------------------------------------------------------------------------- 1 | /*********************************************************************** 2 | * Copyright (c) 2015 Azavea. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the Apache License, Version 2.0 which 5 | * accompanies this distribution and is available at 6 | * http://www.opensource.org/licenses/apache2.0.php. 7 | ***********************************************************************/ 8 | 9 | package org.locationtech.sfcurve.zorder 10 | 11 | import org.scalatest.funspec.AnyFunSpec 12 | import org.scalatest.matchers.should.Matchers 13 | 14 | class Z2Spec extends AnyFunSpec with Matchers { 15 | 16 | describe("Z2 encoding") { 17 | it("interlaces bits"){ 18 | Z2(1,0).z should equal(1) 19 | Z2(2,0).z should equal(4) 20 | Z2(3,0).z should equal(5) 21 | Z2(0,1).z should equal(2) 22 | Z2(0,2).z should equal(8) 23 | Z2(0,3).z should equal(10) 24 | 25 | } 26 | 27 | it("deinterlaces bits") { 28 | Z2(23,13).decode should equal(23, 13) 29 | Z2(Int.MaxValue, 0).decode should equal(Int.MaxValue, 0) 30 | Z2(0, Int.MaxValue).decode should equal(0, Int.MaxValue) 31 | Z2(Int.MaxValue, Int.MaxValue).decode should equal(Int.MaxValue, Int.MaxValue) 32 | } 33 | 34 | it("unapply"){ 35 | val Z2(x,y) = Z2(3,5) 36 | x should be (3) 37 | y should be (5) 38 | } 39 | 40 | it("replaces example in Tropf, Herzog paper"){ 41 | // Herzog example inverts x and y, with x getting higher sigfigs 42 | val rmin = Z2(5,3) 43 | val rmax = Z2(10,5) 44 | val p = Z2(4, 7) 45 | 46 | rmin.z should equal (27) 47 | rmax.z should equal (102) 48 | p.z should equal (58) 49 | 50 | val (litmax, bigmin) = Z2.zdivide(p.z, rmin.z, rmax.z) 51 | 52 | litmax should equal (55) 53 | bigmin should equal (74) 54 | } 55 | 56 | it("replicates the wikipedia example") { 57 | val rmin = Z2(2,2) 58 | val rmax = Z2(3,6) 59 | val p = Z2(5, 1) 60 | 61 | rmin.z should equal (12) 62 | rmax.z should equal (45) 63 | p.z should equal (19) 64 | 65 | val (litmax, bigmin) = Z2.zdivide(p.z, rmin.z, rmax.z) 66 | 67 | litmax should equal (15) 68 | bigmin should equal (36) 69 | } 70 | 71 | it("support maxRanges") { 72 | val ranges = Seq( 73 | ZRange(0L, 4611686018427387903L), // (sfc.index(-180, -90), sfc.index(180, 90)), // whole world 74 | ZRange(864691128455135232L, 4323455642275676160L), // (sfc.index(35, 65), sfc.index(45, 75)), // 10^2 degrees 75 | ZRange(4105065703422263800L, 4261005727442805282L), // (sfc.index(-90, -45), sfc.index(90, 45)), // half world 76 | ZRange(4069591195588206970L, 4261005727442805282L), // (sfc.index(35, 55), sfc.index(45, 75)), // 10x20 degrees 77 | ZRange(4105065703422263800L, 4202182393016524625L), // (sfc.index(35, 65), sfc.index(37, 68)), // 2x3 degrees 78 | ZRange(4105065703422263800L, 4203729178335734358L), // (sfc.index(35, 65), sfc.index(40, 70)), // 5^2 degrees 79 | ZRange(4097762467352558080L, 4097762468106131815L), // (sfc.index(39.999, 60.999), sfc.index(40.001, 61.001)), // small bounds 80 | ZRange(4117455696967246884L, 4117458209718964953L), // (sfc.index(51.0, 51.0), sfc.index(51.1, 51.1)), // small bounds 81 | ZRange(4117455696967246884L, 4117455697154258685L), // (sfc.index(51.0, 51.0), sfc.index(51.001, 51.001)), // small bounds 82 | ZRange(4117455696967246884L, 4117455696967246886L) // (sfc.index(51.0, 51.0), sfc.index(51.0000001, 51.0000001)) // 60 bits in common 83 | ) 84 | 85 | ranges.foreach { r => 86 | val ret = Z2.zranges(Array(r), maxRanges = Some(1000)) 87 | ret.length should be >= 0 88 | ret.length should be <= 1000 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /zorder/src/test/scala/org/locationtech/sfcurve/zorder/Z3RangeSpec.scala: -------------------------------------------------------------------------------- 1 | package org.locationtech.sfcurve.zorder 2 | 3 | import org.scalatest.funspec.AnyFunSpec 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | class Z3RangeSpec extends AnyFunSpec with Matchers { 7 | 8 | describe("Z3Range") { 9 | 10 | val zmin = Z3(2, 2, 0) 11 | val zmax = Z3(3, 6, 0) 12 | val range = ZRange(zmin, zmax) 13 | 14 | it("require ordered min and max") { 15 | ZRange(Z3(2, 2, 0), Z3(1, 4, 0)) // should be valid 16 | intercept[IllegalArgumentException] { 17 | ZRange(zmax, zmin) 18 | } 19 | } 20 | 21 | it("for uncuttable ranges") { 22 | val range = ZRange(zmin, zmin) 23 | Z3.cut(range, Z3(0, 0, 0).z, inRange = false) shouldBe empty 24 | } 25 | it("for out of range zs") { 26 | val zcut = Z3(5, 1, 0).z 27 | Z3.cut(range, zcut, inRange = false) shouldEqual 28 | List(ZRange(zmin, Z3(3, 3, 0)), ZRange(Z3(2, 4, 0), zmax)) 29 | } 30 | 31 | it("support length") { 32 | range.length shouldEqual 130 33 | } 34 | 35 | it("support overlaps") { 36 | Z3.overlaps(range, range) shouldBe true 37 | Z3.overlaps(range, ZRange(Z3(3, 0, 0), Z3(3, 2, 0))) shouldBe true 38 | Z3.overlaps(range, ZRange(Z3(0, 0, 0), Z3(2, 2, 0))) shouldBe true 39 | Z3.overlaps(range, ZRange(Z3(1, 6, 0), Z3(4, 6, 0))) shouldBe true 40 | Z3.overlaps(range, ZRange(Z3(2, 0, 0), Z3(3, 1, 0))) shouldBe false 41 | Z3.overlaps(range, ZRange(Z3(4, 6, 0), Z3(6, 7, 0))) shouldBe false 42 | } 43 | 44 | it("support contains ranges") { 45 | Z3.contains(range, range) shouldBe true 46 | Z3.contains(range, ZRange(Z3(2, 2, 0), Z3(3, 3, 0))) shouldBe true 47 | Z3.contains(range, ZRange(Z3(3, 5, 0), Z3(3, 6, 0))) shouldBe true 48 | Z3.contains(range, ZRange(Z3(2, 2, 0), Z3(4, 3, 0))) shouldBe false 49 | Z3.contains(range, ZRange(Z3(2, 1, 0), Z3(3, 3, 0))) shouldBe false 50 | Z3.contains(range, ZRange(Z3(2, 1, 0), Z3(3, 7, 0))) shouldBe false 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /zorder/src/test/scala/org/locationtech/sfcurve/zorder/Z3Spec.scala: -------------------------------------------------------------------------------- 1 | /*********************************************************************** 2 | * Copyright (c) 2015 Azavea. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the Apache License, Version 2.0 which 5 | * accompanies this distribution and is available at 6 | * http://www.opensource.org/licenses/apache2.0.php. 7 | ***********************************************************************/ 8 | 9 | package org.locationtech.sfcurve.zorder 10 | 11 | import org.scalatest.funspec.AnyFunSpec 12 | import org.scalatest.matchers.should.Matchers 13 | 14 | class Z3Spec extends AnyFunSpec with Matchers { 15 | describe("Z3 encoding") { 16 | it("interlaces bits"){ 17 | // (x,y,z) - x has the lowest sigfig bit 18 | Z3(1,0,0).z should equal(1) 19 | Z3(0,1,0).z should equal(2) 20 | Z3(0,0,1).z should equal(4) 21 | Z3(1,1,1).z should equal(7) 22 | } 23 | 24 | it("deinterlaces bits") { 25 | Z3(23,13,200).decode should equal(23, 13, 200) 26 | 27 | //only 21 bits are saved, so Int.MaxValue is CHOPPED 28 | Z3(Int.MaxValue, 0, 0).decode should equal(2097151, 0, 0) 29 | Z3(Int.MaxValue, 0, Int.MaxValue).decode should equal(2097151, 0, 2097151) 30 | } 31 | 32 | it("unapply"){ 33 | val Z3(x,y,z) = Z3(3,5,1) 34 | x should be (3) 35 | y should be (5) 36 | z should be (1) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /zorder/src/test/scala/org/locationtech/sfcurve/zorder/ZCurve2DSpec.scala: -------------------------------------------------------------------------------- 1 | package org.locationtech.sfcurve.zorder 2 | 3 | import org.locationtech.sfcurve.SpaceFillingCurves 4 | import org.scalatest.funspec.AnyFunSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class ZCurve2DSpec extends AnyFunSpec with Matchers { 8 | describe("SPI access") { 9 | it("provides an SFC") { 10 | val sfc = SpaceFillingCurves("zorder", Map(ZOrderSFCProvider.RESOLUTION_PARAM -> Int.box(100))) 11 | sfc.isInstanceOf[ZCurve2D] should equal(true) 12 | } 13 | 14 | it("computes a covering set of ranges") { 15 | val sfc = SpaceFillingCurves("zorder", Map(ZOrderSFCProvider.RESOLUTION_PARAM -> Int.box(1024))) 16 | val ranges = sfc.toRanges(-80.0, 35.0, -75.0, 40.0, ZCurve2D.hints(maxRecurse = 32)) 17 | 18 | ranges.length shouldBe 44 19 | val (l, r, contains) = ranges.head.tuple 20 | l shouldBe 197616 21 | r shouldBe 197631 22 | contains shouldBe true 23 | } 24 | } 25 | 26 | } 27 | --------------------------------------------------------------------------------