├── images ├── 1.png ├── 2.png ├── 3.png └── index.png ├── go.mod ├── model.go ├── .github └── workflows │ └── main.yml ├── .golangci.yml ├── utility.go ├── index_test.go ├── LICENSE ├── index.go ├── README.md ├── Makefile ├── go.sum ├── lookups.go ├── example └── main.go ├── .gitignore └── lookups_test.go /images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4lie/lookups/HEAD/images/1.png -------------------------------------------------------------------------------- /images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4lie/lookups/HEAD/images/2.png -------------------------------------------------------------------------------- /images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4lie/lookups/HEAD/images/3.png -------------------------------------------------------------------------------- /images/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4lie/lookups/HEAD/images/index.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/4lie/lookups 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.1 // indirect 7 | github.com/golang/geo v0.0.0-20200730024412-e86565bf3f35 8 | github.com/stretchr/testify v1.6.1 9 | ) 10 | -------------------------------------------------------------------------------- /model.go: -------------------------------------------------------------------------------- 1 | package lookups 2 | 3 | import "github.com/golang/geo/s2" 4 | 5 | type ( 6 | Props map[string]interface{} 7 | 8 | Coordinate struct { 9 | Latitude float64 `json:"latitude"` 10 | Longitude float64 `json:"longitude"` 11 | SequenceID int `json:"sequence_id"` 12 | } 13 | 14 | PolyProps struct { 15 | Props Props `json:"props"` 16 | Polygon *s2.Polygon `json:"polygon"` 17 | } 18 | 19 | CoordinateProps struct { 20 | Props []Props `json:"props"` 21 | Coordinate Coordinate `json:"coordinate"` 22 | } 23 | ) 24 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | 13 | build: 14 | name: Build 15 | runs-on: ubuntu-20.04 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | - name: Install go 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: 1.15 23 | - name: Install golangci-lint 24 | run: curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(go env GOPATH)/bin v1.30.0 25 | - name: Lint 26 | run: make lint 27 | - name: Test 28 | run: make test 29 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # This file contains all available configuration options 2 | # with their default values. 3 | 4 | # options for analysis running 5 | run: 6 | # default concurrency is a available CPU number 7 | concurrency: 4 8 | 9 | # timeout for analysis, e.g. 30s, 5m, default is 1m 10 | deadline: 10m 11 | 12 | # exit code when at least one issue was found, default is 1 13 | issues-exit-code: 1 14 | 15 | # include test files or not, default is true 16 | tests: true 17 | 18 | # all available settings of specific linters 19 | linters-settings: 20 | dupl: 21 | # tokens count to trigger issue, 150 by default 22 | threshold: 200 23 | 24 | linters: 25 | enable-all: true 26 | 27 | issues: 28 | # Excluding configuration per-path, per-linter, per-text and per-source 29 | exclude-rules: 30 | - path: \.go 31 | linters: 32 | - gci 33 | - nolintlint 34 | -------------------------------------------------------------------------------- /utility.go: -------------------------------------------------------------------------------- 1 | package lookups 2 | 3 | import "github.com/golang/geo/s2" 4 | 5 | func PolyPropsFromCoordinates(coordinates [][]Coordinate, props map[string]interface{}) PolyProps { 6 | return PolyProps{ 7 | Polygon: PolygonFromCoordinates(coordinates), 8 | Props: props, 9 | } 10 | } 11 | 12 | func PolygonFromCoordinates(coordinates [][]Coordinate) *s2.Polygon { 13 | loops := make([]*s2.Loop, 0, len(coordinates)) 14 | 15 | for _, l := range coordinates { 16 | loop := LoopFromCoordinates(l) 17 | loop.Normalize() 18 | loops = append(loops, loop) 19 | } 20 | 21 | return s2.PolygonFromLoops(loops) 22 | } 23 | 24 | func LoopFromCoordinates(coordinates []Coordinate) *s2.Loop { 25 | points := make([]s2.Point, 0, len(coordinates)) 26 | 27 | for _, p := range coordinates { 28 | points = append(points, s2.PointFromLatLng(s2.LatLngFromDegrees(p.Latitude, p.Longitude))) 29 | } 30 | 31 | return s2.LoopFromPoints(points) 32 | } 33 | -------------------------------------------------------------------------------- /index_test.go: -------------------------------------------------------------------------------- 1 | package lookups_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/4lie/lookups" 7 | 8 | "github.com/golang/geo/s2" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestS2Index(t *testing.T) { 13 | a := assert.New(t) 14 | 15 | rawPolygon := [][]float64{ 16 | { 17 | 35.837037430733666, 18 | 50.993492603302, 19 | }, 20 | { 21 | 35.837037430733666, 22 | 50.994919538497925, 23 | }, 24 | { 25 | 35.837837616204105, 26 | 50.994919538497925, 27 | }, 28 | { 29 | 35.837837616204105, 30 | 50.993492603302, 31 | }, 32 | } 33 | 34 | points := make([]s2.Point, 0, len(rawPolygon)) 35 | 36 | for _, p := range rawPolygon { 37 | points = append(points, s2.PointFromLatLng(s2.LatLngFromDegrees(p[0], p[1]))) 38 | } 39 | 40 | polygon := s2.PolygonFromLoops([]*s2.Loop{s2.LoopFromPoints(points)}) 41 | 42 | s := lookups.NewS2Index(15) 43 | 44 | ids := s.Cover(polygon) 45 | 46 | a.Len(ids, 2) 47 | a.Contains(ids, "3f8dbf754") 48 | a.Contains(ids, "3f8dbf75c") 49 | } 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 4lie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.go: -------------------------------------------------------------------------------- 1 | package lookups 2 | 3 | import "github.com/golang/geo/s2" 4 | 5 | // GeoIndex is an interface for geo indexing. 6 | type GeoIndex interface { 7 | Find(Coordinate) string 8 | Cover(*s2.Polygon) []string 9 | } 10 | 11 | // S2Index is an s2-based polygon indexer. 12 | type S2Index struct { 13 | level int 14 | } 15 | 16 | // NewS2Index returns a new s2 indexer instance. 17 | func NewS2Index(level int) S2Index { 18 | return S2Index{level: level} 19 | } 20 | 21 | // Find returns an s2 cell that contains the given point. 22 | func (s S2Index) Find(coordinate Coordinate) string { 23 | ll := s2.LatLngFromDegrees(coordinate.Latitude, coordinate.Longitude) 24 | 25 | return s2.CellIDFromLatLng(ll).Parent(s.level).ToToken() 26 | } 27 | 28 | // Covers returns all S2 cells that cover the given polygon. 29 | func (s S2Index) Cover(polygon *s2.Polygon) []string { 30 | if polygon.NumEdges() < 1 { 31 | return []string{} 32 | } 33 | 34 | ids := s2.SimpleRegionCovering(polygon, polygon.Edge(0).V0, s.level) 35 | 36 | out := make([]string, 0, len(ids)) 37 | 38 | for _, id := range ids { 39 | out = append(out, id.ToToken()) 40 | } 41 | 42 | return out 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Geo Lookups 2 | 3 | [![Build](https://github.com/4lie/lookups/actions/workflows/main.yml/badge.svg?branch=master)](https://github.com/4lie/lookups/actions/workflows/main.yml) 4 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/4lie/lookups)](https://pkg.go.dev/github.com/4lie/lookups) 5 | 6 | ## Introduction 7 | 8 | Fast and in-memory geo lookup library. 9 | 10 | Simply add polygons and run queries. 11 | It uses Google's S2 Library for indexing and it's super fast :rocket:. 12 | 13 | ## Install 14 | 15 | ``` bash 16 | go get github.com/4lie/lookups 17 | ``` 18 | 19 | ## Examples 20 | 21 | `lookups_test.go` contains several useful examples. You can check visualizations of them here. 22 | 23 | | Image | Description | 24 | |---------------------------------------------|----------------------------| 25 | | | How indexing works | 26 | | | Simple polygon example | 27 | | | Polygon with holes example | 28 | | | Multiple polygons example | 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #@IgnoreInspection BashAddShebang 2 | 3 | export APP=lookups 4 | 5 | export ROOT=$(realpath $(dir $(lastword $(MAKEFILE_LIST)))) 6 | 7 | all: install format lint 8 | 9 | install: 10 | go mod download 11 | 12 | check-formatter: 13 | which gofumpt || GO111MODULE=off go get -u mvdan.cc/gofumpt 14 | which goimports || GO111MODULE=off go get -u golang.org/x/tools/cmd/goimports 15 | which godot || GO111MODULE=off go get -u github.com/tetafro/godot/cmd/godot 16 | 17 | format: check-formatter 18 | find $(ROOT) -type f -name "*.go" -not -path "$(ROOT)/vendor/*" | xargs -n 1 -I R goimports -w R 19 | find $(ROOT) -type f -name "*.go" -not -path "$(ROOT)/vendor/*" | xargs -n 1 -I R gofmt -s -w R 20 | find $(ROOT) -type f -name "*.go" -not -path "$(ROOT)/vendor/*" | xargs -n 1 -I R gofumpt -s -w R 21 | find $(ROOT) -type f -name "*.go" -not -path "$(ROOT)/vendor/*" | xargs -n 1 -I R godot -w R 22 | 23 | check-linter: 24 | which golangci-lint || GO111MODULE=off curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.30.0 25 | 26 | lint: check-linter 27 | golangci-lint run --deadline 10m $(ROOT)/... 28 | 29 | test: 30 | go test -v -race -p 1 ./... 31 | 32 | ci-test: 33 | go test -v -race -p 1 -coverprofile=coverage.txt -covermode=atomic ./... 34 | go tool cover -func coverage.txt 35 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/golang/geo v0.0.0-20200730024412-e86565bf3f35 h1:enTowfyfjtomBQhxX9mhUD+0tZhpe4rIzStO4aNlou8= 5 | github.com/golang/geo v0.0.0-20200730024412-e86565bf3f35/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= 6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 7 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 9 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 10 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 12 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 13 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 14 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 15 | -------------------------------------------------------------------------------- /lookups.go: -------------------------------------------------------------------------------- 1 | package lookups 2 | 3 | import ( 4 | "github.com/golang/geo/s2" 5 | ) 6 | 7 | const ( 8 | // Default level of s2 cells. 9 | DefaultS2CellLevel = 15 10 | ) 11 | 12 | //nolint:gochecknoglobals 13 | var ( 14 | // Default value for geo indexer. 15 | DefaultGeoIndexer = NewS2Index(DefaultS2CellLevel) 16 | ) 17 | 18 | type ( 19 | // Lookuper is an interface for lookup services. 20 | Lookuper interface { 21 | Lookup(coordinates []Coordinate) []CoordinateProps 22 | } 23 | 24 | // lookups is an engine of lookups. 25 | lookups struct { 26 | geoIndex GeoIndex 27 | geoPolygons map[string][]PolyProps 28 | } 29 | ) 30 | 31 | // New returns a new lookups engine instance. 32 | func New(polyProps []PolyProps, geoIndex GeoIndex) Lookuper { 33 | geoPolygons := make(map[string][]PolyProps) 34 | 35 | for _, polyProp := range polyProps { 36 | ids := geoIndex.Cover(polyProp.Polygon) 37 | 38 | for _, id := range ids { 39 | geoPolygons[id] = append(geoPolygons[id], polyProp) 40 | } 41 | } 42 | 43 | return &lookups{ 44 | geoIndex: geoIndex, 45 | geoPolygons: geoPolygons, 46 | } 47 | } 48 | 49 | // Lookup returns list of properties of given coordinates. 50 | func (l lookups) Lookup(coordinates []Coordinate) []CoordinateProps { 51 | result := make([]CoordinateProps, 0, len(coordinates)) 52 | 53 | for _, coordinate := range coordinates { 54 | cell := l.geoIndex.Find(coordinate) 55 | candidates := l.geoPolygons[cell] 56 | props := make([]Props, 0, len(candidates)) 57 | 58 | for _, candidate := range candidates { 59 | ll := s2.LatLngFromDegrees(coordinate.Latitude, coordinate.Longitude) 60 | point := s2.PointFromLatLng(ll) 61 | 62 | if candidate.Polygon.ContainsPoint(point) { 63 | props = append(props, candidate.Props) 64 | } 65 | } 66 | 67 | result = append(result, CoordinateProps{ 68 | Props: props, 69 | Coordinate: coordinate, 70 | }) 71 | } 72 | 73 | return result 74 | } 75 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/4lie/lookups" 7 | ) 8 | 9 | //nolint:gomnd,funlen 10 | func main() { 11 | polyProps := []lookups.PolyProps{ 12 | lookups.PolyPropsFromCoordinates([][]lookups.Coordinate{ 13 | { 14 | { 15 | Latitude: 35.839542, 16 | Longitude: 50.991776, 17 | }, 18 | { 19 | Latitude: 35.834498, 20 | Longitude: 50.991669, 21 | }, 22 | { 23 | Latitude: 35.834289, 24 | Longitude: 51.002011, 25 | }, 26 | { 27 | Latitude: 35.83956, 28 | Longitude: 51.001968, 29 | }, 30 | }, 31 | { 32 | { 33 | Latitude: 35.839194, 34 | Longitude: 50.993954, 35 | }, 36 | { 37 | Latitude: 35.837316, 38 | Longitude: 50.992484, 39 | }, 40 | { 41 | Latitude: 35.836977, 42 | Longitude: 50.996207, 43 | }, 44 | }, 45 | { 46 | { 47 | Latitude: 35.838012, 48 | Longitude: 50.999587, 49 | }, 50 | { 51 | Latitude: 35.835472, 52 | Longitude: 50.997097, 53 | }, 54 | { 55 | Latitude: 35.835141, 56 | Longitude: 51.001368, 57 | }, 58 | }, 59 | }, 60 | map[string]interface{}{ 61 | "polygon": "blue", 62 | }, 63 | ), 64 | lookups.PolyPropsFromCoordinates([][]lookups.Coordinate{ 65 | { 66 | { 67 | Latitude: 35.84163, 68 | Longitude: 50.995038, 69 | }, 70 | { 71 | Latitude: 35.837455, 72 | Longitude: 51.001432, 73 | }, 74 | { 75 | Latitude: 35.841925, 76 | Longitude: 51.001239, 77 | }, 78 | }, 79 | }, 80 | map[string]interface{}{ 81 | "polygon": "green", 82 | }, 83 | ), 84 | } 85 | 86 | queries := []lookups.Coordinate{ 87 | { 88 | Latitude: 35.840743, 89 | Longitude: 50.996776, 90 | }, 91 | { 92 | Latitude: 35.837681, 93 | Longitude: 50.994222, 94 | }, 95 | { 96 | Latitude: 35.835976, 97 | Longitude: 50.999308, 98 | }, 99 | { 100 | Latitude: 35.838429, 101 | Longitude: 51.000767, 102 | }, 103 | { 104 | Latitude: 35.835524, 105 | Longitude: 50.993471, 106 | }, 107 | } 108 | 109 | lookuper := lookups.New(polyProps, lookups.DefaultGeoIndexer) 110 | 111 | coordinatesProps := lookuper.Lookup(queries) 112 | 113 | for _, coordinateProps := range coordinatesProps { 114 | fmt.Println(coordinateProps.Props) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/go,intellij 2 | # Edit at https://www.gitignore.io/?templates=go,intellij 3 | 4 | ### Go ### 5 | # Binaries for programs and plugins 6 | *.exe 7 | *.exe~ 8 | *.dll 9 | *.so 10 | *.dylib 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | 21 | ### Go Patch ### 22 | /vendor/ 23 | /Godeps/ 24 | 25 | ### Intellij ### 26 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 27 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 28 | 29 | # User-specific stuff 30 | .idea/**/workspace.xml 31 | .idea/**/tasks.xml 32 | .idea/**/usage.statistics.xml 33 | .idea/**/dictionaries 34 | .idea/**/shelf 35 | 36 | # Generated files 37 | .idea/**/contentModel.xml 38 | 39 | # Sensitive or high-churn files 40 | .idea/**/dataSources/ 41 | .idea/**/dataSources.ids 42 | .idea/**/dataSources.local.xml 43 | .idea/**/sqlDataSources.xml 44 | .idea/**/dynamic.xml 45 | .idea/**/uiDesigner.xml 46 | .idea/**/dbnavigator.xml 47 | 48 | # Gradle 49 | .idea/**/gradle.xml 50 | .idea/**/libraries 51 | 52 | # Gradle and Maven with auto-import 53 | # When using Gradle or Maven with auto-import, you should exclude module files, 54 | # since they will be recreated, and may cause churn. Uncomment if using 55 | # auto-import. 56 | # .idea/modules.xml 57 | # .idea/*.iml 58 | # .idea/modules 59 | # *.iml 60 | # *.ipr 61 | 62 | # CMake 63 | cmake-build-*/ 64 | 65 | # Mongo Explorer plugin 66 | .idea/**/mongoSettings.xml 67 | 68 | # File-based project format 69 | *.iws 70 | 71 | # IntelliJ 72 | out/ 73 | 74 | # mpeltonen/sbt-idea plugin 75 | .idea_modules/ 76 | 77 | # JIRA plugin 78 | atlassian-ide-plugin.xml 79 | 80 | # Cursive Clojure plugin 81 | .idea/replstate.xml 82 | 83 | # Crashlytics plugin (for Android Studio and IntelliJ) 84 | com_crashlytics_export_strings.xml 85 | crashlytics.properties 86 | crashlytics-build.properties 87 | fabric.properties 88 | 89 | # Editor-based Rest Client 90 | .idea/httpRequests 91 | 92 | # Android studio 3.1+ serialized cache file 93 | .idea/caches/build_file_checksums.ser 94 | 95 | ### Intellij Patch ### 96 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 97 | 98 | # *.iml 99 | # modules.xml 100 | # .idea/misc.xml 101 | # *.ipr 102 | 103 | # Sonarlint plugin 104 | .idea/**/sonarlint/ 105 | 106 | # SonarQube Plugin 107 | .idea/**/sonarIssues.xml 108 | 109 | # Markdown Navigator plugin 110 | .idea/**/markdown-navigator.xml 111 | .idea/**/markdown-navigator/ 112 | 113 | # End of https://www.gitignore.io/api/go,intellij 114 | 115 | .idea 116 | .DS_Store 117 | coverage.txt 118 | -------------------------------------------------------------------------------- /lookups_test.go: -------------------------------------------------------------------------------- 1 | package lookups_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/4lie/lookups" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | //nolint:funlen 11 | func TestLookups(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | polyProps []lookups.PolyProps 15 | queries []lookups.Coordinate 16 | expectedResult [][]lookups.Props 17 | }{ 18 | { 19 | name: "simple polygon", 20 | polyProps: []lookups.PolyProps{ 21 | lookups.PolyPropsFromCoordinates([][]lookups.Coordinate{ 22 | { 23 | { 24 | Latitude: 35.837037430733666, 25 | Longitude: 50.993492603302, 26 | }, 27 | { 28 | Latitude: 35.837037430733666, 29 | Longitude: 50.994919538497925, 30 | }, 31 | { 32 | Latitude: 35.837837616204105, 33 | Longitude: 50.994919538497925, 34 | }, 35 | { 36 | Latitude: 35.837837616204105, 37 | Longitude: 50.993492603302, 38 | }, 39 | }, 40 | }, 41 | map[string]interface{}{ 42 | "polygon": "blue", 43 | }, 44 | ), 45 | }, 46 | queries: []lookups.Coordinate{ 47 | { 48 | Latitude: 35.837411, 49 | Longitude: 50.994426, 50 | }, 51 | { 52 | Latitude: 35.836411, 53 | Longitude: 50.996733, 54 | }, 55 | }, 56 | expectedResult: [][]lookups.Props{ 57 | { 58 | {"polygon": "blue"}, 59 | }, 60 | {}, 61 | }, 62 | }, 63 | { 64 | name: "polygon with holes", 65 | polyProps: []lookups.PolyProps{ 66 | lookups.PolyPropsFromCoordinates([][]lookups.Coordinate{ 67 | { 68 | { 69 | Latitude: 35.839542, 70 | Longitude: 50.991776, 71 | }, 72 | { 73 | Latitude: 35.834498, 74 | Longitude: 50.991669, 75 | }, 76 | { 77 | Latitude: 35.834289, 78 | Longitude: 51.002011, 79 | }, 80 | { 81 | Latitude: 35.83956, 82 | Longitude: 51.001968, 83 | }, 84 | }, 85 | { 86 | { 87 | Latitude: 35.839194, 88 | Longitude: 50.993954, 89 | }, 90 | { 91 | Latitude: 35.837316, 92 | Longitude: 50.992484, 93 | }, 94 | { 95 | Latitude: 35.836977, 96 | Longitude: 50.996207, 97 | }, 98 | }, 99 | { 100 | { 101 | Latitude: 35.838012, 102 | Longitude: 50.999587, 103 | }, 104 | { 105 | Latitude: 35.835472, 106 | Longitude: 50.997097, 107 | }, 108 | { 109 | Latitude: 35.835141, 110 | Longitude: 51.001368, 111 | }, 112 | }, 113 | }, 114 | map[string]interface{}{ 115 | "polygon": "blue", 116 | }, 117 | ), 118 | }, 119 | queries: []lookups.Coordinate{ 120 | { 121 | Latitude: 35.840743, 122 | Longitude: 50.996776, 123 | }, 124 | { 125 | Latitude: 35.837681, 126 | Longitude: 50.994222, 127 | }, 128 | { 129 | Latitude: 35.835976, 130 | Longitude: 50.999308, 131 | }, 132 | { 133 | Latitude: 35.838429, 134 | Longitude: 51.000767, 135 | }, 136 | }, 137 | expectedResult: [][]lookups.Props{ 138 | {}, 139 | {}, 140 | {}, 141 | { 142 | {"polygon": "blue"}, 143 | }, 144 | }, 145 | }, 146 | { 147 | name: "polygon with holes and a simple polygon", 148 | polyProps: []lookups.PolyProps{ 149 | lookups.PolyPropsFromCoordinates([][]lookups.Coordinate{ 150 | { 151 | { 152 | Latitude: 35.839542, 153 | Longitude: 50.991776, 154 | }, 155 | { 156 | Latitude: 35.834498, 157 | Longitude: 50.991669, 158 | }, 159 | { 160 | Latitude: 35.834289, 161 | Longitude: 51.002011, 162 | }, 163 | { 164 | Latitude: 35.83956, 165 | Longitude: 51.001968, 166 | }, 167 | }, 168 | { 169 | { 170 | Latitude: 35.839194, 171 | Longitude: 50.993954, 172 | }, 173 | { 174 | Latitude: 35.837316, 175 | Longitude: 50.992484, 176 | }, 177 | { 178 | Latitude: 35.836977, 179 | Longitude: 50.996207, 180 | }, 181 | }, 182 | { 183 | { 184 | Latitude: 35.838012, 185 | Longitude: 50.999587, 186 | }, 187 | { 188 | Latitude: 35.835472, 189 | Longitude: 50.997097, 190 | }, 191 | { 192 | Latitude: 35.835141, 193 | Longitude: 51.001368, 194 | }, 195 | }, 196 | }, 197 | 198 | map[string]interface{}{ 199 | "polygon": "blue", 200 | }, 201 | ), 202 | lookups.PolyPropsFromCoordinates([][]lookups.Coordinate{ 203 | { 204 | { 205 | Latitude: 35.84163, 206 | Longitude: 50.995038, 207 | }, 208 | { 209 | Latitude: 35.837455, 210 | Longitude: 51.001432, 211 | }, 212 | { 213 | Latitude: 35.841925, 214 | Longitude: 51.001239, 215 | }, 216 | }, 217 | }, 218 | 219 | map[string]interface{}{ 220 | "polygon": "green", 221 | }, 222 | ), 223 | }, 224 | queries: []lookups.Coordinate{ 225 | { 226 | Latitude: 35.840743, 227 | Longitude: 50.996776, 228 | }, 229 | { 230 | Latitude: 35.837681, 231 | Longitude: 50.994222, 232 | }, 233 | { 234 | Latitude: 35.835976, 235 | Longitude: 50.999308, 236 | }, 237 | { 238 | Latitude: 35.838429, 239 | Longitude: 51.000767, 240 | }, 241 | { 242 | Latitude: 35.835524, 243 | Longitude: 50.993471, 244 | }, 245 | }, 246 | expectedResult: [][]lookups.Props{ 247 | { 248 | {"polygon": "green"}, 249 | }, 250 | {}, 251 | {}, 252 | { 253 | {"polygon": "blue"}, 254 | {"polygon": "green"}, 255 | }, 256 | { 257 | {"polygon": "blue"}, 258 | }, 259 | }, 260 | }, 261 | } 262 | for i := range tests { 263 | test := tests[i] 264 | 265 | t.Run(test.name, func(t *testing.T) { 266 | a := assert.New(t) 267 | 268 | a.Equal(len(test.expectedResult), len(test.queries)) 269 | 270 | l := lookups.New(test.polyProps, lookups.DefaultGeoIndexer) 271 | 272 | result := l.Lookup(test.queries) 273 | 274 | a.Equal(len(test.queries), len(result)) 275 | 276 | for i := range result { 277 | a.Equal(test.expectedResult[i], result[i].Props) 278 | } 279 | }) 280 | } 281 | } 282 | --------------------------------------------------------------------------------