Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add region coverer #1

Merged
merged 6 commits into from
Jul 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading