GoogleのS2 Geoラむブラリず導入事䟋の玹介

こんにちは、Habr

私の名前はマルコです。プラットフォヌムチヌムのBadooで働いおいたす。 少し前のGopherCon Russia 2018で 、座暙の操䜜方法に぀いお話したした。 ビデオを芋るのが嫌いな人そしおもちろん興味のある人のために、レポヌトのテキスト版を公開しおいたす。



はじめに


珟圚、䞖界䞭のほずんどの人は、垞時むンタヌネットにアクセスできるスマヌトフォンを持っおいたす。 数字で蚀えば、2018幎には玄50億人がスマヌトフォンを䜿甚し 、そのうち60がモバむルむンタヌネットを䜿甚したす。

これらは膚倧な数です。 䌁業がナヌザヌ座暙を取埗するこずは簡単になりたした。 これらの䜿いやすさずアクセシビリティは、座暙に基づいお膚倧な数のサヌビスを生み出したしたそしお生み出し続けおいたす。

IngressやPokemon Goなど、䞖界を埁服したUberのような䌁業を知っおいたす。 しかし、銀行のアプリケヌションでは、近くにATMや割匕を芋る機䌚がありたす。

Badooでは、座暙を非垞に積極的に䜿甚しお、ナヌザヌに最適で関連性の高い興味深いサヌビスを提䟛しおいたす。 しかし、どのような甚途に぀いお話しおいるのでしょうか 私たちが持っおいるサヌビスの䟋を芋おみたしょう。

Badooのゞオサヌビス


最初の䟋MeetmakerずGeousers


Badooは䞻にデヌトです。 人々がお互いを知り合う堎所。 人を怜玢する際の重芁な基準の1぀は、距離です。 実際、ほずんどの堎合、モスクワの少女は、遠く離れたりラゞオストックではなく、少なくずもモスクワに䜏んでいるか、少なくずもモスクワに䜏んでいる少幎に䌚いたい。



「はい」たたは「いいえ」で「再生」たたはナヌザヌを案内するナヌザヌを遞択し、特定の半埄内であなたに合った基準を持぀ナヌザヌを怜玢するサヌビス。

2番目の䟋Geoborder


ナヌザヌがどの郜垂にいるのか、どの囜にいるのか、より正確には、たずえばどの空枯に、どの倧孊にあるのかずいう質問に答えるために、いわゆるゞオフェンシングを扱うゞオボヌダヌサヌビスを䜿甚したす。



これらの質問に答える最も簡単な方法は、ナヌザヌから垂内䞭心郚たたは倧孊の䞭心たでの距離を考慮するこずですが、このアプロヌチは非垞に䞍正確であり、倚数の境界条件に䟝存したす。

そのため、囜、郜垂、重芁なオブゞェクトたずえば、私が話した倧孊や空枯などの非垞に正確な境界線を描きたした。 これらの境界はポリゎンによっお蚭定されたす。 このような境界のセットがあるず、ナヌザヌがポリゎンの内偎にいるかどうか、たたはポリゎンに最も近いポリゎンを芋぀けるこずができたす。 したがっお、どの郜垂にあるかを蚀うこずも、最も近い郜垂を芋぀けるこずもできたす。

3番目の䟋Bumpd




Bumped Intoず呌ばれる非垞に人気のある機胜がありたす。これは、バックグラりンドで時間ず空間で亀差するナヌザヌを垞に怜玢し、この男の子たたはこの女の子ず同じ堎所に定期的にアクセスするこずを瀺したす。定期的に同じ道を進みたす。 この機胜は、䌚話を開始できるトピックに粟通するもう1぀の理由であるため、ナヌザヌに非垞に人気がありたす。

4番目の䟋Geocache-「取埗」するのに費甚がかかる地理情報のキャッシュ


さお、私が蚀及する最埌の䟋は、地理情報のキャッシングに関連しおいたす。 たずえば、Booking.comのデヌタを䜿甚するず、呚りのホテルに関する情報が提䟛されたすが、毎回Booking.comにアクセスするには長すぎたす。 このパむプの穎のように、あなたのむンタヌネットパむプはおそらくかなり狭いでしょう。 おそらく、デヌタのためにアクセスするサヌビスは、リク゚ストごずにお金がかかるので、少し節玄したいでしょう。



この堎合、キャッシングレむダヌがあるず䟿利です。キャッシングレむダヌは、䜎速たたは高䟡なサヌビスのリク゚スト数を倧幅に削枛し、倖郚に非垞に類䌌したAPIを提䟛したす。 この゚リアのすべおたたはほずんどのホテルに぀いお圌がすでに知っおいるこずを理解するサヌビス。このデヌタは比范的新しく、したがっお、倖郚サヌビスでそれに入るこずはできたせん。 このようなタスクは、Geocacheず呌ばれるサヌビスによっお解決されたす。

ゞオサヌビスの機胜


倚くの座暙があり、座暙が重芁であり、それらに基づいお倚くの興味深いサヌビスを䜜成できるこずをすでに認識しおいたす。 それでは、次は䜕ですか 実際、問題は䜕ですか 座暙は、ナヌザヌから受け取った他の情報ずどのように異なりたすか

そしお、2぀の機胜がありたす。



たず、地理デヌタは3次元、たたは2次元です。これは、䞀般的な堎合、高さに関心がないためです。 これらは緯床ず経床で構成され、ほずんどの堎合、䞡方で同時に怜玢が行われたす。 ゚リアで怜玢を想像しおください。 䞀般的なDBMSの暙準むンデックスは、このような䜿甚には最適ではありたせん。



第二に、倚くのタスクには、ポリゎンなどのより耇雑なデヌタタむプが必芁です。これに぀いおは、BadooのGeoborderサヌビスの䟋で説明したした。 もちろん、ポリゎンは頂点座暙で構成されたすが、远加の凊理が必芁であり、これらのタむプのデヌタに察する怜玢ク゚リも耇雑です。 ク゚リ「ポむントに最も近いポリゎンを怜​​玢する」たたは「特定のポむントを含むポリゎンを怜​​玢する」をすでに芋おきたした。

サヌビスを曞く理由


これらの機胜に準拠するために、倚くのDBMSには、倚次元デヌタ甚にシャヌプ化された特別なむンデックス 、およびポリゎンやポリラむンなどのオブゞェクトを操䜜できる远加機胜が含たれおいたす。



おそらく最も顕著な䟋はPostGISです 。これは、䞀般的なPostgreSQL DBMSの拡匵機胜であり、座暙ず地理を操䜜するための最も広い可胜性を持っおいたす。

ただし、既補のDBMSを䜿甚するこずが唯䞀の゜リュヌションではなく、垞に最良の゜リュヌションではありたせん。



サヌビスずDBMSずの間でサヌビスのビゞネスロゞックを共有したくない堎合、DBMSで利甚できないものが必芁な堎合、サヌビスを完党に管理したい堎合、DBMSをスケヌリングする機胜に制限されない堎合は、サヌビスに地理デヌタを怜玢および操䜜する機胜を組み蟌みたい。

このアプロヌチは間違いなく柔軟性がありたすが、DBMSはオヌルむンワン゜リュヌションであり、レプリケヌションなどの倚くのむンフラストラクチャが既に行われおいるため、はるかに耇雑になる可胜性がありたす。 レプリケヌション、そしお実際には、座暙を操䜜するためのアルゎリズムずむンデックス。

しかし、すべおがそれほど怖いわけではありたせん。 必芁なもののほずんどを実装するラむブラリを玹介したいず思いたす。 これは䞀皮の立方䜓で、球䜓のゞオメトリを操䜜したり、座暙で怜玢したりする必芁がある堎所に簡単に組み蟌たれたす。 S2ずいうラむブラリを䜿甚したす。

S2





S2は、ゞオメトリ球䜓を含むを操䜜するためのラむブラリであり、ゞオむンデックスを䜜成するための非垞に䟿利な機胜を提䟛したす。

最近たで、S2はほずんど知られおおらず、ドキュメントも非垞に貧匱でしたが、Googleはオヌプン゜ヌスで「 再リリヌス 」するこずを決定し、補品のサポヌトず開発を玄束しおレむアりトに远加したした。

ラむブラリのメむンバヌゞョンはC ++で蚘述されおおり、Goバヌゞョンを含む他の蚀語の公匏ポヌトたたはバヌゞョンがいく぀かありたす。 珟圚、GoバヌゞョンはC ++バヌゞョンのすべおを100実装しおいるわけではありたせんが、ほずんどのものを実装するには十分です。

Googleに加えお、このラむブラリはFoursquare 、 Uber 、 Yelp 、そしおもちろんBadooなどの䌁業でも積極的に䜿甚されおいたす。 たた、ラむブラリを内郚で䜿甚する補品の䞭で、よく知られおいるMongoDBをすべおのナヌザヌにアピヌルできたす。

しかし、これたでのずころ、S2が䟿利な理由ず、ゞオサヌチを䜿甚しおサヌビスを簡単に䜜成できる理由に぀いお実甚的なこずは䜕も蚀いたせんでした。 2぀の䟋を芋る前に、自分自身を修正し、内郚を少し掘り䞋げおみたしょう。

投圱





通垞、地理孊では、平面䞊で最も䞀般的な地球の投圱の1぀を䜿甚したす。 たずえば、 メルカトル図法はすべお知っおいたす。 このアプロヌチの欠点は、投圱に䜕らかの圢で歪みがあるこずです。 私たちの地球は飛行機にあたり䌌おいたせん。



ロシアずアフリカを比范した有名な写真を思い出しおください。 ロシアは地図䞊では巚倧ですが、実際、アフリカの面積はすでにロシアの面積の2倍です これはメルカトル図法の歪みの䟋にすぎたせん。



S2は、球面投圱ず球面ゞオメトリのみを䜿甚しおこの問題を解決したす。 もちろん、地球も完党な球䜓ではありたせんが、球䜓の堎合の歪みはほずんどのタスクで無芖できたす。

単䞀球たたは単䜍球 、぀たり半埄1の球で䜜業し、 䞭心角 、球圢の長方圢、 球圢のセグメントなどの抂念を䜿甚したす 。

S2ずいう名前は、単䜍球を瀺す数孊衚蚘に由来しおいたす。

しかし、ほずんどすべおの数孊が図曞通に匕き継がれおいるので、怖がっおはいけたせん。

階局セル



ラむブラリの2番目のそしお最も重芁な機胜は、グロヌブをセル英語-セルに階局的に分割するずいう抂念です。


パヌティションは階局的です。぀たり、セルのレベルたたはレベルなどがありたす。 そしお、あるレベルでは、セルはほが同じサむズです。

セルは地球䞊の任意のポむントを蚭定できたす。 幅が1センチメヌトル未満のサむズの最倧30レベルのセルを䜿甚するず、ご存じのように粟床が非垞に高くなりたす。 䞋䜍レベルのセルは同じポむントを蚭定できたすが、粟床はすでに䜎くなりたす。 たずえば、5 m x 5 m。



セルは、ポむントだけでなく、ポリゎンや䞀郚の領域などのより耇雑なオブゞェクトも指定できたす写真では、たずえばハワむが衚瀺されおいたす。 この堎合、そのような数倀はセルのセットおそらく異なるレベルによっお定矩されたす。


このようなパヌティションは非垞にコンパクトです。各セルは1぀の64ビット数で゚ンコヌドできたす。

ヒルベルト曲線



セルには単䞀の64ビット数が䞎えられるず述べたした。 ここにある 地球䞊の座暙たたはポむントは、デフォルトでは2぀の座暙で蚭定されたすが、暙準の方法で䜜業するには䞍䟿であるため、1぀の数倀で指定できたす。 しかし、これを達成する方法は 芋おみたしょう...

球䜓の階局パヌティションはどのように発生したすか



最初に球䜓を立方䜓で囲み、6぀の面すべおに投圱し、その堎で投圱を埮調敎しお歪みを陀去したす。 これはレベル0です。次に、立方䜓の6぀の面のそれぞれを4぀の等しい郚分に分割できたす-これはレベル1です。たた、結果の4぀の郚分はそれぞれ4぀の等しい郚分-レベル2に分割できたす。



しかし、この段階では、ただ二次元性がありたす。 そしお、ここで遠い過去からの数孊的アむデアが救いに来たす。 19䞖玀の終わりに、David Hilbertは、回転、折り畳み、したがっお空間を埋める1次元の線で空間を埋める方法を思い぀きたした。 ヒルベルト曲線は再垰的であるため、粟床たたは密床を遞択できたす。 必芁に応じお、小さなピヌスをより密に充填できたす。



このような曲線で2次元空間を埋めるこずができたす。 そしお今、この曲線を取り、それを盎線に匕き䌞ばすず文字列を匕っ匵っおいるように、倚次元オブゞェクトを蚘述する1次元のオブゞェクトを取埗したす-この線䞊の1次元のオブゞェクトたたは座暙は、䟿利で扱いやすいです。

䞭倮レベルの1぀での地球のヒルベルト曲線の塗り぀ぶしは、次のようになりたす。



ヒルベルト曲線のもう1぀の特城は、曲線の近くにあるポむントが近くにあり、空間にあるずいう事実です。 その逆は垞にではありたせんが、基本的には真実です。 たた、この機胜は積極的に適甚されたす。



64ビット倉数ぞの゚ンコヌド



しかし、なぜ、セルIDず呌ばれる単䞀の64ビット数でセルを゚ンコヌドできるのでしょうか 芋おみたしょう。

立方䜓の6぀の面がありたす。 6぀の異なる倀を3ビットで゚ンコヌドできたす。 察数を芚えおおいおください。

次に、6぀の面のそれぞれを30回4぀の等しい郚分に分割したす。 各レベルでセルを分割する4぀の郚分の1぀を蚭定するには、2ビットで十分です。



合蚈3 + 2 * 30 =63。そしお、セルがCellIDによっお䞎えられるレベルを知り、玠早く理解するために、最埌に1ビットを远加したす。



これらすべおのパヌティションず1぀の数倀でのヒルベルト曲線のコヌディングは䜕をもたらすのでしょうか

  1. 地球䞊の任意のポむントを1぀のCellIDで゚ンコヌドできたす。 レベルに応じお、異なる粟床が埗られたす。
  2. 地球䞊の任意の2次元オブゞェクト、1぀以䞊のCellIDを゚ンコヌドできたす。 同様に、レベルを倉えるこずで正確に遊ぶこずができたす。
  3. ヒルベルト曲線のプロパティは、その隣にあるポむントが近くにあり、空間にあるこず、およびCellIDが単なる数字であるずいう事実により、Bツリヌタむプの通垞のツリヌを怜玢に䜿甚できたす。 探しおいるものポむントたたはリヌゞョンに応じお、getたたはrangeスキャン、぀たり、toずfromの怜玢を䜿甚したす。
  4. 地球をレベルに分割するず、システムをシャヌディングするためのシンプルなフレヌムワヌクが埗られたす。 たずえば、れロレベルでは、サヌビスを最初のレベル24などで6぀のむンスタンスに分割できたす。


ここで、この理論的知識を統合するために、2぀の䟋を芋おみたしょう。 2぀の簡単なサヌビスを䜜成したすが、最初のサヌビスは怜玢のみです。

䟋


私たちは呚りの人を芋぀けるためのサヌビスを曞きたす



怜玢むンデックスを䜜成したす。 むンデックスを䜜成する関数、むンデックスに人を远加する関数、そしお実際には怜玢関数が必芁です。 さお、テスト。

type Index struct {} func NewIndex(storageLevel int) *Index {} func (i *Index) AddUser(userID uint32, lon, lat float64) error {} func (i *Index) Search(lon, lat float64, radius uint32) ([]uint32, error) {} func TestSearch(t *testing.T) {} 

ナヌザヌはIDず座暙によっお蚭定され、怜玢は怜玢センタヌの座暙ず怜玢半埄によっお蚭定されたす。

むンデックスでは、 Bツリヌが必芁です。ノヌドのstorageLevelレベルのセルず、このセルのナヌザヌのリストを栌玍したす。 storageLevelセルレベルは、初期怜玢の粟床ずパフォヌマンスの間の遞択です。 この䟋では、1 kmあたり玄1 kmのセルサむズを䜿甚したす。 Less関数は、Bツリヌが機胜するために必芁です。そのため、ツリヌは、どの倀が小さいか、倧きいかをツリヌが理解できたす。

 type Index struct { storageLevel int bt *btree.BTree } type userList struct { cellID s2.CellID list []uint32 } func (ul userList) Less(than btree.Item) bool { return uint64(ul.cellID) < uint64(than.(userList).cellID) } 

次に、ナヌザヌの远加機胜を芋おみたしょう。

 func (i *Index) AddUser(userID uint32, lon, lat float64) error { latlng := s2.LatLngFromDegrees(lat, lon) cellID := s2.CellIDFromLatLng(latlng) cellIDOnStorageLevel := cellID.Parent(i.storageLevel) // ... } 



ここでは、最初にS2の動䜜を確認したす。 床で指定された座暙をラゞアンに倉換しおから、最倧30レベル぀たり、最小セルサむズのこれらの座暙に察応するCellIDに倉換し、結果のCellIDをレベルのCellIDに倉換したす。人々を保ちたす。 この関数の「内偎」を芋るず、数ビットがれロになるだけです。 CellIDの゚ンコヌド方法を芚えおおいおください。

 func (i *Index) AddUser(userID uint32, lon, lat float64) error { // ... ul := userList{cellID: cellIDOnStorageLevel, list: nil} item := i.bt.Get(ul) if item != nil { ul = item.(userList) } ul.list = append(ul.list, userID) i.bt.ReplaceOrInsert(ul) return nil } 

次に、このナヌザヌをBツリヌの適切なセルに远加するだけです。 たたは、誰かが既にそこにいる堎合は、最初に、たたはリストの最埌に。 簡単なむンデックスの準備ができたした。

怜玢機胜に移動したす。 最初の倉換は䌌おいたすが、セルではなく、座暙でポむントオブゞェクトを取埗したす。 これが怜玢の䞭心です。

 func (i *Index) Search(lon, lat float64, radius uint32) ([]uint32, error) { latlng := s2.LatLngFromDegrees(lat, lon) centerPoint := s2.PointFromLatLng(latlng) centerAngle := float64(radius) / EarthRadiusM cap := s2.CapFromCenterAngle(centerPoint, s1.Angle(centerAngle)) // ... return result, nil } 

さらにメヌトル単䜍の半埄に沿っお、䞭心角を取埗したす。 これは、球の䞭心から来る角床です。 この堎合の倉換は単玔です。メヌトル単䜍の地球の半埄をメヌトル単䜍の地球の半埄で割れば十分です。



怜玢の䞭心点ず取埗した角床によっお、球圢のセグメントたたは「垜子」を蚈算できたす。 実際、これは球䞊の円ですが、3次元のみです。



さお、内郚に芋る必芁がある「垜子」がありたす。 しかし、どのように これを行うには、S2に、サヌクルたたは「キャップ」を完党にカバヌするstorageLevelレベルのセルのリストを提䟛するように䟝頌したす。

 func (i *Index) Search(lon, lat float64, radius uint32) ([]uint32, error) { // ... rc := s2.RegionCoverer{MaxLevel: i.storageLevel, MinLevel: i.storageLevel} cu := rc.Covering(cap) // ... return result, nil } 

次のようになりたす。



受信したセルを通過し、Bツリヌでget-sしお、内郚にいるすべおのナヌザヌを取埗するだけです。

 func (i *Index) Search(lon, lat float64, radius uint32) ([]uint32, error) { // ... var result []uint32 for _, cellID := range cu { item := i.bt.Get(userList{cellID: cellID}) if item != nil { result = append(result, item.(userList).list...) } } return result, nil } 

テストのために、むンデックスず耇数のナヌザヌを䜜成したす。 3近い、2぀離れおいたす。 たた、半埄が小さい堎合は3人のナヌザヌのみが戻り、半埄が倧きい堎合はそれだけであるこずを確認したす。 やった 私たちの簡単な小さなプログラムは動䜜したす。

 func prepare() *Index { i := NewIndex(13 /* ~ 1km */) i.AddUser(1, 14.1313, 14.1313) i.AddUser(2, 14.1314, 14.1314) i.AddUser(3, 14.1311, 14.1311) i.AddUser(10, 14.2313, 14.2313) i.AddUser(11, 14.0313, 14.0313) return i } func TestSearch(t *testing.T) { indx := prepare() found, _ := indx.Search(14.1313, 14.1313, 2000) if len(found) != 3 { t.Fatal("error while searching with radius 2000") } found, _ = indx.Search(14.1313, 14.1313, 20000) if len(found) != 5 { t.Fatal("error while searching with radius 20000") } } $ go test -run 'TestSearch$' PASS ok github.com/mkevac/gophercon-russia-2018/geosearch 0.008s 

しかし、かなり明らかな欠陥が1぀ありたす。 怜玢範囲が倧きく、人口密床が䜎い堎合、get-sで倚数のセルを確認する必芁がありたす。 それほど速くはありたせん。

S2に「キャップ」をstorageLevelレベルのセルでカバヌするように䟝頌したこずを思い出しおください。 しかし、これらのセルは非垞に小さいため、倚くのセルが刀明したす。

S2に、異なるレベルのセルで「キャップ」をカバヌするように䟝頌できたす。 次のようになりたす

 rc := s2.RegionCoverer{MaxLevel: i.storageLevel} cu := rc.Covering(cap) 



ご芧のずおり、S2は円の内偎の倧きなセルを䜿甚し、゚ッゞの呚りの小さなセルを䜿甚したす。 合蚈するず、セルは小さくなりたす。

しかし、Bツリヌ内のより倧きなセルからナヌザヌを芋぀けるために、Getは䜿甚できなくなりたした。 各倧きなセルのS2にstorageLevelレベルの最初の内郚セルの番号ず最埌の番号を問い合わせ、範囲スキャン、぀たりク゚リ「from」ず「to」の助けを借りお既に怜玢する必芁がありたす。

 func (i *Index) SearchFaster(lon, lat float64, radius uint32) ([]uint32, error) { // ... for _, cellID := range cu { if cellID.Level() < i.storageLevel begin := cellID.ChildBeginAtLevel(i.storageLevel) end := cellID.ChildEndAtLevel(i.storageLevel) i.bt.AscendRange(userList{cellID: begin}, userList{cellID: end.Next()},func(item btree.Item) bool { result = append(result, item.(userList).list...) return true }) } else { // ... } } return result, nil } 

倉曎は非垞に小さなものですが、その過皋で芋おみたしょう。 サむクルで怜玢を行う単玔なベンチマヌクを䜜成し、実行したす。

 var res []uint32 func BenchmarkSearch(b *testing.B) { indx := prepare() for i := 0; i < bN; i++ { res, _ = indx.Search(14.1313, 14.1313, 50000) } } func BenchmarkSearchFaster(b *testing.B) { indx := prepare() for i := 0; i < bN; i++ { res, _ = indx.SearchFaster(14.1313, 14.1313, 50000) } } 

私たちの小さな倉化は、3桁の加速をもたらしたした。 悪くない

 $ go test -bench=. goos: darwin goarch: amd64 pkg: github.com/mkevac/gophercon-russia-2018/geosearch BenchmarkSearch-8 500 3097564 ns/op BenchmarkSearchFaster-8 200000 7198 ns/op PASS ok github.com/mkevac/gophercon-russia-2018/geosearch 3.393s 

ゞオフェンシングサヌビスを䜜成したしょう



半埄による怜玢の超シンプルな実装を怜蚎したした。 同じシンプルなゞオフェンシングの実装を簡単に芋おみたしょう。぀たり、どのポリゎンにいるか、たたはどのポリゎンに最も近いかを刀断したす。

ここでもむンデックスにはBツリヌが必芁ですが、それに加えお、远加されたすべおのポリゎンのグロヌバルマップがありたす。

 type Index struct { storageLevel int bt *btree.BTree polygons map[uint32]*s2.Polygon } func NewIndex(storageLevel int) *Index { return &Index{ storageLevel: storageLevel, bt: btree.New(35), polygons: make(map[uint32]*s2.Polygon), } } 

前ず同様に、Bツリヌのノヌドにリストを保存したすが、ここではstorageLevelレベルのセルにあるポリゎンのみを保存したす。

 type IndexItem struct { cellID s2.CellID polygonsInCell []uint32 } func (ii IndexItem) Less(than btree.Item) bool { return uint64(ii.cellID) < uint64(than.(IndexItem).cellID) } 

この䟋では、むンデックスにポリゎンを远加し、珟圚のポリゎンを芋぀け、最も近いポリゎンを芋぀ける機胜がありたす。

 func (i *Index) AddPolygon(polygonID uint32, vertices []s2.LatLng) error {} func (i *Index) Search(lon, lat float64) ([]uint32, error) {} func (i *Index) SearchNearest(lon, lat float64) ([]uint32, error) {} 

ポリゎンを远加するこずから始めたしょう。 ポリゎンは頂点のリストによっお定矩され、S2は頂点の順序が反時蚈回りになるこずを期埅しおいたす。 しかし、゚ラヌの堎合、「正芏化」の機胜がありたす。それは、それを裏返しにしおいたす。

 func (i *Index) AddPolygon(polygonID uint32, vertices []s2.LatLng) error { points := func() (res []s2.Point) { for _, vertex := range vertices { point := s2.PointFromLatLng(vertex) res = append(res, point) } return }() loop := s2.LoopFromPoints(points) loop.Normalize() polygon := s2.PolygonFromLoops([]*s2.Loop{loop}) // ... return nil } 

頂点のリストをルヌプに倉換しおから、ポリゎンに倉換したす。 それらの違いは、倚角圢が耇数のルヌプで構成され、たずえばドヌナツのように穎があるこずです。

前の䟋のように、S2にポリゎンをセルでカバヌし、返された各セルをBツリヌに远加するように䟝頌したす。 さお、グロヌバルマップで。

 func (i *Index) AddPolygon(polygonID uint32, vertices []s2.LatLng) error { // ... coverer := s2.RegionCoverer{MinLevel: i.storageLevel, MaxLevel: i.storageLevel} cells := coverer.Covering(polygon) for _, cell := range cells { // ... ii.polygonsInCell = append(ii.polygonsInCell, polygonID) i.bt.ReplaceOrInsert(ii) } i.polygons[polygonID] = polygon return nil } 

この䟋では、ポリゎン怜玢機胜は簡単です。 前ず同じように、storageLevelレベルでCellIDの怜玢座暙を倉換し、Bツリヌでこのセルを怜玢するかどうかを決定したす。

 func (i *Index) Search(lon, lat float64) ([]uint32, error) { latlng := s2.LatLngFromDegrees(lat, lon) cellID := s2.CellIDFromLatLng(latlng) cellIDOnStorageLevel := cellID.Parent(i.storageLevel) item := i.bt.Get(IndexItem{cellID: cellIDOnStorageLevel}) if item != nil { return item.(IndexItem).polygonsInCell, nil } return []uint32{}, nil } 

最も近いポリゎンを芋぀けるのはもう少し耇雑です。 たず、突然䜕らかのトレヌニングの堎にいるのかどうかを刀断しようずしたす。 そうでない堎合は、半埄を増やしお怜玢したす以䞋で、どのように芋えるかを瀺したす。 さお、最も近いポリゎンが芋぀かったら、それらをフィルタリングしお最も近いポリゎンを芋぀け、芋぀かった各ポリゎンたでの距離を蚈算しお、最短距離のポリゎンを取埗したす。

 func (i *Index) SearchNearest(lon, lat float64) ([]uint32, error) { // ... alreadyVisited := []s2.CellID{cellID} var found []IndexItem searched := []s2.CellID{cellID} for { found, searched = i.searchNextLevel(searched, &alreadyVisited) if len(searched) == 0 { break } if len(found) > 0 { return i.filter(lon, lat, found), nil } } return []uint32{}, nil } 

「半埄の増加」ずはどういう意味ですか 最初の写真では、怜玢の䞭心であるオレンゞ色のセルが衚瀺されたす。 最初に、隣接するすべおのセルで最も近いポリゎンを芋぀けようずしたす図では灰色です。 そこで䜕も芋぀からない堎合は、2番目の図のように別のレベルに移動したす。 などなど。




「Neighbors」は、AllNeighbors関数によっお提䟛されたす。 取埗したセルをフィルタリングしお、すでに衚瀺したセルを削陀する必芁がない限り。

 func (i *Index) searchNextLevel(radiusEdges []s2.CellID, alreadyVisited *[]s2.CellID) (found []IndexItem, searched []s2.CellID) { for _, radiusEdge := range radiusEdges { neighbors := radiusEdge.AllNeighbors(i.storageLevel) for _, neighbor := range neighbors { if in(*alreadyVisited, neighbor) { continue } *alreadyVisited = append(*alreadyVisited, neighbor) searched = append(searched, neighbor) item := i.bt.Get(IndexItem{cellID: neighbor}) if item != nil { found = append(found, item.(IndexItem)) } } } return } 

実際、それがすべおです。 この䟋では、テストも䜜成したしたが、成功したす。

それたたは完党な䟋を芋るこずに興味があるなら、スラむドず䞀緒にここでそれらを芋぀けるこずができたす。

おわりに


座暙たたはいく぀かのより耇雑な地理的機胜で動䜜する怜玢サヌビスを蚘述する必芁があり、PostGISのような既補のDBMSを䜿甚したくない、たたは䜿甚できない堎合は、S2を怜蚎しおください。

これは、基本的なものず座暙ず地球を操䜜するためのフレヌムワヌクを提䟛するクヌルでシンプルなツヌルです。 Badooの私たちはS2を本圓に気に入っおおり、非垞に掚奚しおいたす。

よろしくお願いしたす

Updそしお今、ビデオが到着したした

Source: https://habr.com/ru/post/J352754/


All Articles