Skip to content

Commit

Permalink
Merge pull request #1 from spider-gazelle/add-additional-functions
Browse files Browse the repository at this point in the history
feat: add region coverer
  • Loading branch information
stakach authored Jul 27, 2024
2 parents 586b276 + 5cbc837 commit 9e5e5fb
Show file tree
Hide file tree
Showing 21 changed files with 2,880 additions and 178 deletions.
2 changes: 2 additions & 0 deletions .ameba.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Lint/Typos:
Enabled: false
5 changes: 2 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,14 @@ jobs:
crystal:
- latest
- nightly
- 1.0.0
runs-on: ${{ matrix.os }}
container: crystallang/crystal:${{ matrix.crystal }}-alpine
steps:
- uses: actions/checkout@v2
- name: Install dependencies
run: shards install --ignore-crystal-version
- name: Lint
run: ./bin/ameba
# - name: Lint
# run: ./bin/ameba
- name: Format
run: crystal tool format --check
- name: Run tests
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,21 @@ Useful for things like storing points [in InfluxDB](https://docs.influxdata.com/
require "s2_cells"
# index a location in your database
lat = -33.870456
lon = 151.208889
level = 24
cell = S2Cells.at(lat, lon).parent(level)
token = cell.to_token # => "3ba32f81"
# or
id = cell.id # => Int64
# Or a little more direct
S2Cells::LatLon.new(lat, lon).to_token(level)
# find all the indexes in an area
p1 = S2Cells::LatLng.from_degrees(33.0, -122.0)
p2 = S2Cells::LatLng.from_degrees(33.1, -122.1)
cells = S2Cells.in(p1, p2) # => Array(CellId)
# then can search your index:
# loc_index = ANY(cells.map(&.id))
```
10 changes: 10 additions & 0 deletions shard.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: 2.0
shards:
bisect:
git: https://github.com/spider-gazelle/bisect.git
version: 1.2.1

priority-queue:
git: https://github.com/spider-gazelle/priority-queue.git
version: 1.1.2

12 changes: 8 additions & 4 deletions shard.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
name: s2_cells
version: 1.0.1
version: 2.0.0
crystal: ">= 0.36.1"

development_dependencies:
ameba:
github: veelenga/ameba
dependencies:
priority-queue:
github: spider-gazelle/priority-queue

# development_dependencies:
# ameba:
# github: veelenga/ameba
183 changes: 178 additions & 5 deletions spec/s2_cells_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ require "./spec_helper"

module S2Cells
describe S2Cells do
it "should convert lat lon to a cell id" do
it "should convert lat lng to a cell id" do
{
{0x47a1cbd595522b39_u64, 49.703498679, 11.770681595},
{0x46525318b63be0f9_u64, 55.685376759, 12.588490937},
Expand All @@ -18,13 +18,14 @@ module S2Cells
{0x3b00955555555555_u64, 10.050986518, 78.293170610},
{0x1dcc469991555555_u64, -34.055420593, 18.551140038},
{0xb112966aaaaaaaab_u64, -69.219262171, 49.670072392},
}.each do |(id, lat, lon)|
point = LatLon.new(lat, lon).to_point
}.each do |(id, lat, lng)|
lat_lng = LatLng.from_degrees(lat, lng)
point = lat_lng.to_point
cell = CellId.from_point(point)
cell.id.should eq(id)

CellId.from_lat_lon(lat, lon).id.should eq(id)
S2Cells.at(lat, lon).id.should eq(id)
CellId.from_lat_lng(lat_lng).id.should eq(id)
S2Cells.at(lat, lng).id.should eq(id)
end
end

Expand Down Expand Up @@ -69,4 +70,176 @@ module S2Cells
end
end
end

it "should generate the correct face" do
CellId.from_lat_lng(0.0, 0.0).face.should eq 0
CellId.from_lat_lng(0.0, 90.0).face.should eq 1
CellId.from_lat_lng(90.0, 0.0).face.should eq 2
CellId.from_lat_lng(0.0, 180.0).face.should eq 3
CellId.from_lat_lng(0.0, -90.0).face.should eq 4
CellId.from_lat_lng(-90.0, 0.0).face.should eq 5
end

it "test parent child relationship" do
cell_id = CellId.from_face_pos_level(3, 0x12345678_u64, CellId::MAX_LEVEL - 4)

cell_id.face.should eq 3
cell_id.pos.to_s(2).should eq 0x12345700.to_s(2)
cell_id.level.should eq(CellId::MAX_LEVEL - 4)
cell_id.valid?.should be_true
cell_id.leaf?.should be_false

cell_id.child_begin(cell_id.level + 2).pos.should eq 0x12345610
cell_id.child_begin.pos.should eq 0x12345640
cell_id.parent.pos.should eq 0x12345400
cell_id.parent(cell_id.level - 2).pos.should eq 0x12345000

cell_id.child_begin.next.next.next.next.should eq cell_id.child_end
cell_id.child_begin(CellId::MAX_LEVEL).should eq cell_id.range_min
cell_id.child_end(CellId::MAX_LEVEL).should eq cell_id.range_max.next

# Check that cells are represented by the position of their center
# alngg the Hilbert curve.
(cell_id.range_min.id &+ cell_id.range_max.id).should eq(2_u64 &* cell_id.id)
end

it "should be able to switch between lat lang and cell ids" do
INVERSE_ITERATIONS.times do
cell_id = get_random_cell_id(CellId::MAX_LEVEL)
cell_id.leaf?.should be_true
cell_id.level.should eq CellId::MAX_LEVEL
center = cell_id.to_lat_lng
CellId.from_lat_lng(center).id.should eq cell_id.id
end
end

it "should be able to switch between tokens and cell ids" do
TOKEN_ITERATIONS.times do
cell_id = get_random_cell_id
token = cell_id.to_token
(token.size <= 16).should be_true
CellId.from_token(token).id.should eq cell_id.id
end
end

it "should be able to obtain neighbours" do
# Check the edge neighbors of face 1.
out_faces = {5, 3, 2, 0}
face_nbrs = CellId.from_face_pos_level(1, 0, 0).get_edge_neighbors
face_nbrs.each_with_index do |face_nbr, i|
face_nbr.face?.should be_true
face_nbr.face.should eq out_faces[i]?
end

# Check the vertex neighbors of the center of face 2 at level 5.
neighbors = CellId.from_point(Point.new(0, 0, 1)).get_vertex_neighbors(5)
neighbors.sort!
neighbors.each_with_index do |neighbor, i|
neighbor.id.should eq(CellId.from_face_ij(
2,
(1_u64 << 29) - (i < 2 ? 1 : 0),
(1_u64 << 29) - (i == 0 || i == 3 ? 1 : 0)
).parent(5).id)
end

neighbors.each_with_index do |neighbor, i|
neighbor.should eq(CellId.from_face_ij(
2,
(1_u64 << 29) - (i < 2 ? 1 : 0),
(1_u64 << 29) - (i == 0 || i == 3 ? 1 : 0)
).parent(5))
end

# Check the vertex neighbors of the corner of faces 0, 4, and 5.
cell_id = CellId.from_face_pos_level(0, 0, CellId::MAX_LEVEL)
neighbors = cell_id.get_vertex_neighbors(0)
neighbors.sort!
neighbors.size.should eq 3

CellId.from_face_pos_level(0, 0, 0).should eq neighbors[0]
CellId.from_face_pos_level(4, 0, 0).should eq neighbors[1]
CellId.from_face_pos_level(5, 0, 0).should eq neighbors[2]

# check a bunch
NEIGHBORS_ITERATIONS.times do
cell_id = get_random_cell_id
cell_id = cell_id.parent if cell_id.leaf?
max_diff = {6, CellId::MAX_LEVEL - cell_id.level - 1}.min
level = max_diff == 0 ? cell_id.level : cell_id.level + rand(max_diff)
raise "level < cell_id.level" unless level >= cell_id.level
raise "level == MAX_LEVEL" if level >= CellId::MAX_LEVEL

all, expected = {Set(CellId).new, Set(CellId).new}
neighbors = cell_id.get_all_neighbors(level)
all.concat neighbors
cell_id.children(level + 1).each do |child|
all.add(child.parent)
expected.concat(child.get_vertex_neighbors(level))
end

all_a = all.map(&.id).uniq!.sort
expect_a = expected.map(&.id).uniq!.sort

all_a.size.should eq expect_a.size
all_a.should eq expect_a
end
end

it "should work with faces" do
edge_counts = Hash(Point, Int32).new(0)
vertex_counts = Hash(Point, Int32).new(0)

6.times do |face|
cell_id = CellId.from_face_pos_level(face, 0, 0)
cell = Cell.new(cell_id)
cell.id.should eq cell_id
cell.face.should eq face
cell.level.should eq 0

cell.orientation.should eq(face & SWAP_MASK)
cell.leaf?.should eq false

4.times do |k|
edge_counts[cell.get_edge_raw(k)] += 1
vertex_counts[cell.get_vertex_raw(k)] += 1

cell.get_vertex_raw(k).dot_prod(cell.get_edge_raw(k)).should eq 0.0
cell.get_vertex_raw((k + 1) & 3)
.dot_prod(cell.get_edge_raw(k))
.should eq 0.0

cell
.get_vertex_raw(k)
.cross_prod(cell.get_vertex_raw((k + 1) & 3))
.normalize
.dot_prod(cell.get_edge(k))
.should be_close(1.0, 0.000001)
end
end

edge_counts.values.each { |count| count.should eq 2 }
vertex_counts.values.each { |count| count.should eq 3 }
end

it "generates the correct covering for a given region" do
coverer = RegionCoverer.new

p1 = LatLng.from_degrees(33.0, -122.0)
p2 = LatLng.from_degrees(33.1, -122.1)

cell_ids = coverer.get_covering(LatLngRect.from_point_pair(p1, p2))
ids = cell_ids.map(&.id).sort

target = [
9291041754864156672_u64,
9291043953887412224_u64,
9291044503643226112_u64,
9291045878032760832_u64,
9291047252422295552_u64,
9291047802178109440_u64,
9291051650468806656_u64,
9291052200224620544_u64,
]
ids.should eq(target)
end
end
17 changes: 17 additions & 0 deletions spec/spec_helper.cr
Original file line number Diff line number Diff line change
@@ -1,2 +1,19 @@
require "spec"
require "random"
require "../src/s2_cells"

def get_random_cell_id(level : Int32 = Random.rand(S2Cells::CellId::MAX_LEVEL + 1))
face = Random.rand(S2Cells::CellId::NUM_FACES)
pos = Random.rand(UInt64::MAX) & ((1_u64 << (2 * S2Cells::CellId::MAX_LEVEL)) - 1)

S2Cells::CellId.from_face_pos_level(face, pos, level)
end

INVERSE_ITERATIONS = 20
TOKEN_ITERATIONS = 10
COVERAGE_ITERATIONS = 10
NEIGHBORS_ITERATIONS = 10
NORMALIZE_ITERATIONS = 20
REGION_COVERER_ITERATIONS = 10
RANDOM_CAPS_ITERATIONS = 10
SIMPLE_COVERINGS_ITERATIONS = 10
Loading

0 comments on commit 9e5e5fb

Please sign in to comment.