├── .gitignore ├── .golangci.yml ├── .travis.yml ├── AUTHORS ├── LICENSE ├── Makefile ├── README.md ├── dbscan.go ├── dbscan_test.go ├── distance.go ├── distance_test.go ├── go.mod ├── go.sum ├── kdtree.go ├── kdtree_bench_test.go ├── kdtree_test.go ├── point.go └── point_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | coverage.txt 27 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - gofmt 4 | - gosec 5 | - unconvert 6 | - misspell 7 | - goimports 8 | - megacheck 9 | - staticcheck 10 | - unused 11 | - deadcode 12 | - typecheck 13 | - ineffassign 14 | - golint 15 | - stylecheck 16 | - unparam 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | sudo: false 3 | 4 | language: go 5 | 6 | go: 7 | - 1.11.x 8 | - 1.12.x 9 | - master 10 | 11 | env: 12 | - GO111MODULE=on 13 | 14 | before_install: 15 | - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.16.0 16 | 17 | matrix: 18 | allow_failures: 19 | - go: master 20 | 21 | after_success: 22 | - bash <(curl -s https://codecov.io/bash) 23 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | * Andrey Smirnov 2 | * Ethan Burns -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Andrey Smirnov 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 | 23 | ------------- 24 | 25 | Part of the code: files kdtree.go, kdtree_test.go and kdtree_bench_test.go 26 | is adapted version of the code from https://godoc.org/code.google.com/p/eaburns/kdtree, 27 | which is licensed under New BSD License: http://opensource.org/licenses/BSD-3-Clause 28 | 29 | Author: Ethan Burns 30 | 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: test check bench 2 | 3 | test: 4 | go test -race -v -coverprofile=coverage.txt -covermode=atomic 5 | 6 | bench: 7 | go test -v -run ^Test$$ -bench=. ./... -gocheck.b 8 | 9 | check: 10 | golangci-lint run 11 | 12 | .PHONY: test bench check 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Point Clustering 2 | 3 | [![Build Status](https://travis-ci.org/smira/go-point-clustering.svg?branch=master)](https://travis-ci.org/smira/go-point-clustering) 4 | [![codecov](https://codecov.io/gh/smira/go-point-clustering/branch/master/graph/badge.svg)](https://codecov.io/gh/smira/go-point-clustering) 5 | [![GoDoc](https://godoc.org/github.com/smira/go-point-clustering?status.svg)](https://godoc.org/github.com/smira/go-point-clustering) 6 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fsmira%2Fgo-point-clustering.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fsmira%2Fgo-point-clustering?ref=badge_shield) 7 | 8 | (Lat, lon) points fast clustering using [DBScan](https://en.wikipedia.org/wiki/DBSCAN) algorithm in Go. 9 | 10 | Given set of geo points, this library can find clusters according to specified params. There are several optimizations 11 | applied: 12 | 13 | * distance calculation is using "fast" implementations of sine/cosine, with `sqrt` being removed 14 | * to find points within `eps` distance [k-d tree](https://en.wikipedia.org/wiki/K-d_tree) is being used 15 | * edge case handling of identical points being present in the set 16 | 17 | ## Usage 18 | 19 | Build list of points: 20 | 21 | ```go 22 | points := cluster.PointList{{30.258387, 59.951557}, {30.434124, 60.029499}, ...} 23 | ``` 24 | 25 | Pick settings for DBScan algorithm: 26 | 27 | * `eps` is clustering radius (in kilometers) 28 | * `minPoints` is number of points in `eps`-radius of base point to consider it being part of the cluster 29 | 30 | `eps` and `minPoints` together define minimum density of the cluster. 31 | 32 | Run DBScan: 33 | 34 | ```go 35 | clusters, noise := cluster.DBScan(points, 0.8, 10) // eps is 800m, 10 points minimum in eps-neighborhood 36 | ``` 37 | 38 | `DBScan` function returns list of clusters (each `Cluster` being reference to the list of source `points`) and list 39 | of point indexes which don't fit into any cluster (`noise`). 40 | 41 | 42 | ## License 43 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fsmira%2Fgo-point-clustering.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fsmira%2Fgo-point-clustering?ref=badge_large) 44 | -------------------------------------------------------------------------------- /dbscan.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | import ( 4 | "github.com/willf/bitset" 5 | ) 6 | 7 | // DBSCAN in pseudocode (from http://en.wikipedia.org/wiki/DBSCAN): 8 | 9 | // DBSCAN(D, eps, MinPts) 10 | // C = 0 11 | // for each unvisited point P in dataset D 12 | // mark P as visited 13 | // NeighborPts = regionQuery(P, eps) 14 | // if sizeof(NeighborPts) < MinPts 15 | // mark P as NOISE 16 | // else 17 | // C = next cluster 18 | // expandCluster(P, NeighborPts, C, eps, MinPts) 19 | 20 | // expandCluster(P, NeighborPts, C, eps, MinPts) 21 | // add P to cluster C 22 | // for each point P' in NeighborPts 23 | // if P' is not visited 24 | // mark P' as visited 25 | // NeighborPts' = regionQuery(P', eps) 26 | // if sizeof(NeighborPts') >= MinPts 27 | // NeighborPts = NeighborPts joined with NeighborPts' 28 | // if P' is not yet member of any cluster 29 | // add P' to cluster C 30 | 31 | // regionQuery(P, eps) 32 | // return all points within P's eps-neighborhood (including P) 33 | 34 | // EpsFunction is a function that returns eps based on point pt 35 | type EpsFunction func(pt Point) float64 36 | 37 | // DBScan clusters incoming points into clusters with params (eps, minPoints) 38 | // 39 | // eps is clustering radius in km 40 | // minPoints in minimum number of points in eps-neighbourhood (density) 41 | func DBScan(points PointList, eps float64, minPoints int) (clusters []Cluster, noise []int) { 42 | visited := make([]bool, len(points)) 43 | members := make([]bool, len(points)) 44 | clusters = []Cluster{} 45 | noise = []int{} 46 | C := 0 47 | kdTree := NewKDTree(points) 48 | 49 | // Our SphericalDistanceFast returns distance which is not mutiplied 50 | // by EarthR * DegreeRad, adjust eps accordingly 51 | eps = eps / EarthR / DegreeRad 52 | 53 | neighborUnique := bitset.New(uint(len(points))) 54 | 55 | for i := 0; i < len(points); i++ { 56 | if visited[i] { 57 | continue 58 | } 59 | visited[i] = true 60 | 61 | neighborPts := kdTree.InRange(points[i], eps, nil) 62 | if len(neighborPts) < minPoints { 63 | noise = append(noise, i) 64 | } else { 65 | cluster := Cluster{C: C, Points: []int{i}} 66 | members[i] = true 67 | C++ 68 | // expandCluster goes here inline 69 | neighborUnique.ClearAll() 70 | for j := 0; j < len(neighborPts); j++ { 71 | neighborUnique.Set(uint(neighborPts[j])) 72 | } 73 | 74 | for j := 0; j < len(neighborPts); j++ { 75 | k := neighborPts[j] 76 | if !visited[k] { 77 | visited[k] = true 78 | moreNeighbors := kdTree.InRange(points[k], eps, nil) 79 | if len(moreNeighbors) >= minPoints { 80 | for _, p := range moreNeighbors { 81 | if !neighborUnique.Test(uint(p)) { 82 | neighborPts = append(neighborPts, p) 83 | neighborUnique.Set(uint(p)) 84 | } 85 | } 86 | } 87 | } 88 | 89 | if !members[k] { 90 | cluster.Points = append(cluster.Points, k) 91 | members[k] = true 92 | } 93 | } 94 | clusters = append(clusters, cluster) 95 | } 96 | } 97 | 98 | return 99 | } 100 | 101 | // RegionQuery is simple way O(N) to find points in neighbourhood 102 | // 103 | // It is roughly equivalent to kdTree.InRange(points[i], eps, nil) 104 | func RegionQuery(points PointList, P *Point, eps float64) []int { 105 | result := []int{} 106 | 107 | for i := 0; i < len(points); i++ { 108 | if points[i].sqDist(P) < eps*eps { 109 | result = append(result, i) 110 | } 111 | } 112 | 113 | return result 114 | } 115 | -------------------------------------------------------------------------------- /dbscan_test.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "sort" 7 | 8 | . "gopkg.in/check.v1" 9 | ) 10 | 11 | const jsonPointsData = "[[30.258387, 59.951557], [30.434124, 60.029499], [37.642517, 55.828777], [55.956112, 54.702778], [30.163561, 59.809929], [39.822922, 57.672379], [30.222057, 59.987171], [39.838715, 57.673241], [39.816383, 57.683563], [30.304995, 59.906403], [30.240265, 59.951828], [55.956112, 54.702778], [30.321133, 59.808334], [30.398251, 59.798973], [30.398251, 59.798973], [37.928783, 59.122658], [30.215363, 59.985455], [30.269926, 59.95079], [30.294989, 59.808968], [30.269926, 59.95079], [34.348171, 61.781609], [37.722153, 44.754757], [85.173615, 52.511944], [30.305201, 59.906319], [30.274799, 59.905045], [30.10704, 59.848518], [30.10704, 59.848518], [30.093334, 59.854534], [49.472778, 53.486389], [37.917946, 59.089333], [30.259613, 59.945427], [30.301603, 59.911541], [30.10704, 59.848518], [37.917946, 59.089333], [44.032108, 56.300205], [34.394905, 61.784111], [30.446316, 59.908722], [30.48535, 59.93512], [30.143106, 59.823051], [82.921669, 55.118889], [30.223917, 59.984699], [30.240755, 59.952072], [30.24472, 59.955975], [30.366283, 59.81361], [30.24472, 59.955975], [30.626213, 59.726067], [37.628971, 55.824425], [30.241648, 59.956799], [30.418478, 60.027519], [39.846497, 59.186054], [30.111698, 59.847164], [55.956112, 54.702778], [37.444386, 55.82655], [30.434814, 60.027538], [29.892851, 59.795109], [44.961666, 53.224445], [49.356831, 53.551579], [30.233097, 59.87418], [30.311964, 59.812389], [38.989265, 55.810677], [86.618256, 53.869011], [44.082855, 56.300865], [30.240265, 59.951828], [82.942223, 55.115253], [30.442141, 59.910454], [30.319765, 60.044281], [82.942223, 55.115253], [49.361942, 53.544167], [30.10704, 59.848518], [30.24472, 59.955975], [34.365894, 61.773148], [30.366207, 59.734398], [30.433374, 60.032806], [37.962112, 59.125305], [30.222057, 59.987171], [30.442352, 59.906124], [30.228682, 59.961269], [30.294121, 59.913307], [30.22628, 59.984489], [37.917946, 59.089333], [30.417412, 60.036903], [30.475889, 59.946159], [30.316261, 60.040699], [29.880516, 59.670269], [30.22628, 59.984489], [37.900555, 59.120556], [37.770718, 55.69952], [37.396965, 55.798645], [37.887531, 55.75211], [30.304995, 59.906403], [30.30105, 59.906067], [30.163561, 59.809929], [39.786743, 54.623516], [82.947525, 55.108124], [30.301603, 59.909935], [30.228937, 59.955486], [30.315216, 59.810055], [30.494091, 59.933277], [30.412436, 60.019444], [30.43158, 59.946812], [30.304995, 59.906403], [30.383959, 59.814262], [49.354168, 53.547222], [34.354527, 61.795582], [30.256298, 59.958683], [30.316261, 60.040699], [30.491566, 59.945103], [30.464399, 60.047668], [30.311195, 59.961575], [30.24472, 59.955975], [30.24472, 59.955975], [34.357529, 61.795834], [44.0658, 56.300865], [34.357529, 61.795834], [30.154856, 59.809929], [37.905693, 59.090752], [44.0658, 56.300865], [39.898235, 59.219349], [37.923332, 59.085556], [30.155834, 59.809006], [30.492496, 59.937935], [29.990747, 59.820244], [85.175552, 52.529167], [30.323803, 59.808727], [49.356949, 53.546619], [30.32514, 59.809891], [30.317495, 60.053444], [30.465143, 59.960094], [37.9851, 55.798199], [82.942223, 55.111389], [37.917946, 59.089333], [30.319202, 60.045364], [30.433372, 59.974979], [49.319721, 53.548889], [30.311195, 59.961575], [30.678865, 59.726051], [37.883728, 55.787594], [49.458611, 53.484165], [44.0658, 56.300865], [39.943333, 57.656326], [37.977474, 55.80064], [39.943333, 57.656326], [39.788334, 54.620098], [30.301603, 59.911541], [30.347841, 59.789471], [30.495152, 59.936497], [37.917946, 59.089333], [30.319202, 60.045364], [30.378305, 59.809898], [30.301603, 59.911541], [85.178482, 52.517792], [85.178482, 52.517792], [39.943333, 57.656326], [37.923306, 59.09375], [73.459656, 61.261547], [30.398174, 59.801132], [37.917946, 59.089333], [37.917946, 59.089333], [30.32514, 59.809891], [30.102442, 59.755318], [73.459656, 61.261547], [30.315216, 59.810055], [30.305201, 59.906319], [30.434814, 60.027538], [30.306477, 59.904129], [44.048553, 56.288578], [30.226967, 59.838863], [73.449112, 61.25729], [39.967579, 57.615368], [46.001667, 51.598331], [30.316261, 60.040699], [30.494766, 59.947014], [30.427782, 60.030994], [30.313948, 60.049637], [39.838715, 57.673241], [30.433374, 60.032806], [37.933613, 59.160557], [37.906082, 59.127373], [30.323803, 59.808727], [30.281393, 59.916348], [30.665346, 59.952682], [37.37714, 59.187527], [37.907055, 59.123611], [86.635559, 53.861958], [55.956112, 54.702778], [39.838715, 57.673241], [30.245201, 59.959896], [30.434124, 60.029499], [30.32514, 59.809891], [30.427782, 60.030994], [30.315357, 59.807198], [39.842876, 59.19659], [30.398251, 59.798973], [30.451073, 59.919037], [55.119999, 51.842499], [29.863771, 59.867134], [30.244602, 59.955917], [30.107775, 59.818859], [30.434814, 60.027538], [37.9851, 55.798199], [30.46216, 59.899651], [39.843777, 59.193974], [39.846497, 59.186054], [34.357529, 61.795834], [46.001667, 51.598331], [30.434814, 60.027538], [30.304995, 59.906403], [37.469891, 55.726955], [30.326496, 59.811073], [30.28503, 59.912971], [30.304995, 59.906403], [37.334568, 55.699932], [34.34557, 61.797569], [39.91386, 57.661652], [30.432152, 59.946026], [30.448624, 59.910351], [30.24472, 59.955975], [36.57172, 50.565109], [39.837872, 57.668083], [30.435919, 59.945473], [30.305201, 59.906319], [34.354527, 61.795582], [37.9589, 55.793499], [30.319431, 60.044483], [30.10704, 59.848518], [30.37072, 60.009029], [34.357529, 61.795834], [30.093334, 59.85556], [37.917946, 59.089333], [34.354527, 61.795582], [30.311073, 60.047928], [30.4863, 59.929993], [34.354527, 61.795582], [30.17742, 59.807434], [30.412264, 59.795349], [49.321388, 53.533611], [30.304995, 59.906403], [37.911888, 59.09864], [30.433374, 60.032806], [30.458305, 59.914459], [37.93261, 59.131889], [34.357529, 61.795834], [37.859818, 59.129833], [30.24472, 59.955975], [34.354527, 61.795582], [34.350929, 61.786236], [30.301603, 59.911541], [30.530924, 59.771492], [30.344059, 60.056522], [30.17696, 59.825451], [30.093334, 59.854534], [37.424526, 55.726292], [30.215363, 59.985455], [47.258827, 47.393539], [30.26918, 59.931633], [44.961666, 53.224445], [37.884361, 59.133583], [30.181974, 59.82613], [37.444386, 55.82655], [30.753729, 59.635494], [30.323803, 59.808727], [30.326645, 59.917656], [39.179169, 51.717499], [30.417341, 60.036743], [43.103508, 44.191944], [30.398174, 59.801132], [44.961666, 53.224445], [30.155834, 59.809006], [30.434814, 60.027538], [30.093334, 59.854534], [30.222057, 59.987171], [85.183609, 52.52639], [46.003017, 51.599789], [30.24472, 59.955975], [30.28533, 59.926342], [39.232166, 51.69875], [30.222057, 59.987171], [39.837654, 59.211777], [37.905693, 59.090752], [37.982121, 55.798565], [73.439758, 61.250549], [39.79274, 54.613075], [30.259916, 59.946293], [49.354168, 53.547222], [82.942223, 55.111389], [30.111698, 59.847164], [30.323803, 59.808727], [30.301088, 59.907742], [30.111698, 59.847164], [30.24472, 59.955975], [30.222057, 59.987171], [39.179169, 51.717499], [30.418261, 60.008503], [49.354168, 53.547222], [30.093334, 59.853798], [30.222057, 59.987171], [34.357529, 61.795834], [39.837872, 57.668083], [34.354527, 61.795582], [30.304995, 59.906403], [30.284208, 59.732933], [30.03228, 59.855011], [37.966045, 55.758251], [30.439301, 59.908752], [30.43483, 59.94669], [37.485554, 55.731613], [30.444202, 59.912117], [30.479795, 59.957111], [45.843334, 53.174168], [30.24472, 59.955975], [37.900555, 59.120556], [37.918869, 59.080795], [30.427782, 60.030994], [37.863804, 55.767727], [30.244759, 59.955982], [39.935604, 57.66478], [30.313948, 60.049637], [30.614849, 59.731327], [30.222057, 59.987171], [30.324594, 59.886959], [30.321112, 59.808956], [30.344084, 59.985577], [30.319431, 60.044483], [30.110039, 59.847805], [30.614849, 59.731327], [38.142944, 59.042419], [30.259567, 59.878056], [30.243324, 59.957798], [29.15855, 60.375378], [34.354527, 61.795582], [37.973999, 55.793999], [30.678865, 59.726051], [30.222057, 59.987171], [30.222057, 59.987171], [30.24472, 59.955975], [30.495152, 59.936497], [30.093334, 59.853798], [30.111698, 59.847164], [30.162861, 59.807167], [34.337158, 61.794823], [30.21718, 59.98576], [49.473701, 53.498291], [30.315216, 59.810055], [30.444202, 59.912117], [30.304995, 59.906403], [30.155834, 59.809006], [37.440723, 55.731659], [30.509666, 59.933533], [30.444202, 59.912117], [30.434814, 60.027538], [30.24472, 59.955975], [46.001667, 51.598331], [30.28503, 59.912971], [30.26158, 59.954937], [30.305841, 59.922596], [50.806854, 61.698402], [30.24472, 59.955975], [34.350983, 61.795288], [36.568531, 50.565289], [44.943333, 53.223331], [44.961666, 53.224445], [30.304998, 59.941277], [37.883728, 55.787594], [30.250736, 59.952724], [36.57172, 50.565109], [30.319431, 60.044483], [37.917946, 59.089333], [44.08337, 56.300865], [30.10704, 59.848518], [39.79274, 54.613075], [30.323803, 59.808727], [30.287428, 59.912277], [50.806854, 61.698402], [37.758884, 55.70232], [30.299744, 59.908398], [29.939953, 59.870312], [30.383055, 59.806625], [30.155834, 59.809006], [30.222057, 59.987171], [30.322643, 59.807716], [37.923306, 59.09375], [30.678865, 59.726051], [44.889999, 53.222221], [30.320679, 60.043739], [30.292629, 59.912815], [30.446951, 59.765621], [30.315216, 59.810055], [37.917946, 59.089333], [30.444202, 59.912117], [37.334568, 55.699932], [30.304995, 59.906403], [30.435919, 59.945473], [30.434814, 60.027538], [30.093334, 59.854534], [30.323803, 59.808727], [37.856239, 55.746342], [37.923306, 59.09375], [38.085228, 55.751438], [30.350834, 59.830704], [37.396965, 55.798645], [30.678865, 59.726051], [37.977474, 55.80064], [73.459656, 61.261547], [34.344975, 61.794956], [73.453178, 61.255978], [30.448603, 59.911942], [30.389116, 59.803574], [30.678865, 59.726051], [30.678865, 59.726051], [30.244602, 59.955917], [30.319431, 60.044483], [30.323215, 59.908554], [37.917946, 59.089333], [30.504477, 59.946659], [30.297989, 59.907173], [37.869804, 59.125111], [30.10704, 59.848518], [30.223917, 59.984699], [37.333935, 55.69841], [30.304995, 59.906403], [30.304995, 59.906403], [34.357529, 61.795834], [46.001667, 51.598331], [44.082855, 56.300865], [39.846497, 59.186054], [44.945, 53.233334], [30.398251, 59.798973], [30.316261, 60.040699], [30.433374, 60.032806], [85.173615, 52.511944], [30.222057, 59.987171], [44.082855, 56.300865], [37.846992, 55.745235], [30.45311, 59.903046], [37.628971, 55.824425], [30.442287, 60.040833], [30.106731, 59.819611], [37.785553, 55.750759], [30.215363, 59.985455], [30.155834, 59.809006], [39.83802, 57.668983], [30.480774, 59.904316], [37.910114, 59.094299], [30.222057, 59.987171], [30.315216, 59.810055], [30.216248, 59.985374], [30.262533, 59.953304], [30.215363, 59.985455], [34.361816, 61.773479], [37.917946, 59.089333], [44.943333, 53.223331], [30.217726, 59.953007], [30.433374, 60.032806], [30.412264, 59.795349], [30.456095, 59.906143], [37.758137, 55.700821], [30.223497, 59.987587], [30.280392, 59.901085], [30.244602, 59.955917], [30.323803, 59.808727], [30.266119, 59.952393], [30.327028, 59.781349], [30.398174, 59.801132], [37.759468, 55.702091], [30.319431, 60.044483], [49.472778, 53.486389], [37.923332, 59.085556], [30.491627, 59.939308], [30.435141, 59.96273], [30.222057, 59.987171], [37.917946, 59.089333], [37.9175, 55.7995], [38.036991, 55.808056], [37.751373, 55.706112], [85.17691, 52.515163], [85.183609, 52.52639], [30.323803, 59.808727], [30.442141, 59.910454], [30.399895, 59.799801], [30.222057, 59.987171], [30.200258, 59.849888], [30.222057, 59.987171], [30.315216, 59.810055], [49.473812, 53.48587], [30.268238, 59.800194], [30.433374, 60.032806], [30.405226, 59.966652], [30.24472, 59.955975], [30.495838, 59.934013], [39.837872, 57.668083], [30.494349, 59.937195], [30.434814, 60.027538], [30.309454, 59.904526], [30.093334, 59.854534], [30.162861, 59.807167], [37.9851, 55.798199], [30.433374, 60.032806], [37.985085, 55.798168], [30.3141, 60.047661], [30.434814, 60.027538], [30.184679, 59.862797], [37.730026, 44.755306], [44.952625, 53.220646], [30.163561, 59.809929], [30.155834, 59.809006], [39.792439, 54.614655], [30.432152, 59.946026], [34.33717, 61.797619], [30.316261, 60.040699], [30.306528, 59.905758], [30.269926, 59.95079], [30.383959, 59.814262], [39.788334, 54.620098], [37.92775, 59.090694], [30.244759, 59.955982], [37.444386, 55.82655], [30.446316, 59.908722], [39.788334, 54.620098], [30.216248, 59.985374], [30.448162, 59.916096], [30.155834, 59.809006], [30.10704, 59.848518], [30.456095, 59.906143], [30.323215, 59.908554], [30.111698, 59.847164], [44.082855, 56.300865], [30.093334, 59.853798], [30.317408, 60.045689], [39.935604, 57.66478], [37.396667, 55.798706], [30.295248, 59.808228], [30.162861, 59.807167], [30.443539, 59.912037], [39.943333, 57.656326], [44.019794, 56.316944], [30.572836, 59.693909], [30.222057, 59.987171], [37.628971, 55.824425], [30.111698, 59.847164], [39.172028, 51.715332], [37.765495, 55.703732], [37.759468, 55.702091], [30.428268, 59.975586], [37.758884, 55.70232], [30.515127, 59.851894], [34.357529, 61.795834], [30.096621, 59.797756], [30.614849, 59.731327], [30.315216, 59.810055], [30.222057, 59.987171], [37.959911, 59.104618], [30.216248, 59.985374], [30.265074, 59.883602], [49.354168, 53.547222], [30.222057, 59.987171], [30.323803, 59.808727], [30.397121, 60.021099], [37.92775, 59.090694], [30.309454, 59.904526], [37.628971, 55.824425], [30.398174, 59.801132], [30.444202, 59.912117], [49.472778, 53.486389], [30.444202, 59.912117], [37.628971, 55.824425], [37.628971, 55.824425], [30.10704, 59.848518], [30.163561, 59.809929], [49.354168, 53.547222], [30.256298, 59.958683], [30.497301, 59.934628], [34.354527, 61.795582], [34.354527, 61.795582], [30.457832, 59.975716], [37.884361, 59.133583], [49.360985, 53.544167], [37.930752, 59.089668], [30.448624, 59.910351], [49.361942, 53.544167], [30.446316, 59.908722], [30.446316, 59.908722], [30.430847, 59.953148], [30.495838, 59.934013], [73.454353, 61.259205], [30.434814, 60.027538], [73.454353, 61.259205], [30.321089, 59.833145], [30.324938, 59.809715], [30.572617, 59.956299], [30.319431, 60.044483], [30.270361, 59.933926], [30.372482, 59.933395], [37.923306, 59.09375], [30.243324, 59.957798], [55.956112, 54.702778], [27.919815, 60.594994], [39.833157, 54.613838], [30.111698, 59.847164], [73.449112, 61.25729], [30.111698, 59.847164], [73.460754, 61.261993], [37.730026, 44.755306], [30.446316, 59.908722], [30.262962, 59.834091], [85.177414, 52.515366], [30.176668, 59.82077], [30.428268, 59.975586], [30.20274, 59.839104], [49.478516, 53.490559], [30.40671, 59.803234], [30.022676, 59.705574], [30.48049, 59.940842], [30.297989, 59.907173], [29.892851, 59.795109], [30.43483, 59.94669], [30.308332, 60.065395], [30.304995, 59.906403], [37.9604, 55.787998], [30.515127, 59.851894], [85.17691, 52.515163], [30.266119, 59.952393], [56.064484, 54.828438], [37.396667, 55.798706], [37.883728, 55.787594], [44.952625, 53.220646], [37.396667, 55.798706], [30.215363, 59.985455], [30.389397, 59.804993], [30.227449, 59.956375], [37.765919, 55.679688], [30.37154, 60.081711], [44.943333, 53.223331], [30.319202, 60.045364], [37.986801, 55.793999], [29.943457, 59.875278], [30.234074, 59.94886], [37.856239, 55.746342], [55.956112, 54.702778], [37.923306, 59.09375], [37.910114, 59.094299], [30.155834, 59.809006], [34.357529, 61.795834], [50.739742, 61.819416], [30.396196, 59.941505], [30.315216, 59.810055], [30.2941, 59.925201], [38.081882, 55.776302], [28.554581, 60.936878], [30.252947, 59.832256], [43.927814, 56.275867], [30.40671, 59.803234], [30.106731, 59.819611], [30.296925, 59.866241], [30.181974, 59.82613], [30.163561, 59.809929], [49.323334, 53.520279], [30.223497, 59.987587], [30.308735, 59.824333], [30.216248, 59.985374], [30.497301, 59.934628], [30.398174, 59.801132], [30.162861, 59.807167], [30.222057, 59.987171], [30.25626, 59.877495], [30.31587, 60.046764], [49.354168, 53.547222], [30.3141, 60.047661], [30.481022, 59.915607], [34.357578, 61.785637], [34.357529, 61.795834], [30.497301, 59.934628], [37.917946, 59.089333], [55.996666, 54.696945], [86.618256, 53.869011], [30.222057, 59.987171], [37.918869, 59.080795], [82.921669, 55.118889], [30.231226, 59.946014], [44.08337, 56.300865], [34.433083, 61.75922], [34.357529, 61.795834], [45.968506, 51.611668], [45.019169, 53.192123], [30.301603, 59.911541], [37.911888, 59.09864], [73.457802, 61.260803], [49.354168, 53.547222], [39.838715, 57.673241], [37.92775, 59.090694], [30.402056, 59.800888], [73.452347, 61.24678], [30.586538, 59.739468], [30.245024, 59.956047], [37.968639, 59.12875], [30.266119, 59.952393], [37.92775, 59.090694], [30.427586, 60.014961], [30.256298, 59.958683], [30.24472, 59.955975], [30.678865, 59.726051], [30.495152, 59.936497], [30.678865, 59.726051], [73.459656, 61.261547], [30.266119, 59.952393], [44.082855, 56.300865], [86.618614, 53.867802], [30.156893, 59.808079], [30.320736, 60.040833], [30.243324, 59.957798], [34.350983, 61.795288], [38.089977, 55.756744], [30.262533, 59.953304], [30.41836, 59.94368], [30.029167, 59.849758], [30.319431, 60.044483], [30.614849, 59.731327], [44.082855, 56.300865], [30.463945, 59.902119], [30.460484, 59.894314], [30.46875, 59.922287], [37.728413, 55.693161], [30.231226, 59.946014], [30.24472, 59.955975], [37.396667, 55.798706], [37.758884, 55.70232], [37.758884, 55.70232], [30.227314, 59.981552], [30.216248, 59.985374], [30.243324, 59.957798], [37.92775, 59.090694], [30.24472, 59.955975], [30.306528, 59.905758], [30.354805, 59.761803], [30.306528, 59.905758], [37.917946, 59.089333], [30.434843, 59.776707], [34.357529, 61.795834], [30.354059, 60.0509], [30.323803, 59.808727], [30.399895, 59.799801], [30.564167, 59.750343], [30.319431, 60.044483], [30.398174, 59.801132], [34.357529, 61.795834], [73.449486, 61.257442], [39.179169, 51.717499], [30.402172, 59.809639], [30.304995, 59.906403], [39.781254, 54.612293], [39.159138, 51.710972], [39.781254, 54.612293], [30.110039, 59.847805], [30.396049, 60.03421], [30.067728, 59.845058], [49.330833, 53.545834], [34.344975, 61.794956], [30.319431, 60.044483], [30.305201, 59.906319], [34.350983, 61.795288], [46.001667, 51.598331], [30.256298, 59.958683], [46.001667, 51.598331], [30.446316, 59.908722], [30.277155, 59.72105], [30.169003, 59.813629], [30.332439, 59.826344], [30.111698, 59.847164], [39.873222, 59.219528], [30.494091, 59.933277], [49.77972, 53.904999], [34.357529, 61.795834], [30.398174, 59.801132], [30.491001, 59.9375], [30.444202, 59.912117], [49.346111, 53.540279], [30.431534, 59.946743], [30.111698, 59.847164], [30.169003, 59.813629], [29.822975, 59.835602], [30.398174, 59.801132], [30.321112, 59.808956], [30.614475, 59.735294], [49.77972, 53.904999], [30.24472, 59.955975], [30.162861, 59.807167], [30.247738, 59.952545], [30.333529, 60.045242], [39.838715, 57.673241], [30.320679, 60.043739], [34.354527, 61.795582], [30.323803, 59.808727], [30.222057, 59.987171], [30.304995, 59.906403], [37.917973, 59.097057], [30.279331, 59.884975], [37.905693, 59.090752], [30.304995, 59.906403], [37.905693, 59.090752], [30.222057, 59.987171], [30.243324, 59.957798], [30.222057, 59.987171], [30.266119, 59.952393], [37.912224, 59.154446], [38.087799, 55.758202], [30.495152, 59.936497], [39.846497, 59.186054], [30.157833, 59.830814], [85.136108, 52.511391], [30.215363, 59.985455], [30.256298, 59.958683], [30.247738, 59.952545], [39.838715, 57.673241], [30.311073, 60.047928], [30.416803, 60.007446], [30.301088, 59.907742], [30.304995, 59.906403], [30.155834, 59.809006], [30.448624, 59.910351], [30.30105, 59.906067], [30.222057, 59.987171], [37.722153, 44.754757], [30.304995, 59.906403], [30.614849, 59.731327], [30.304995, 59.906403], [34.360687, 61.788483], [30.678865, 59.726051], [30.155834, 59.809006], [49.354168, 53.547222], [49.472778, 53.486389], [37.728413, 55.693161], [30.306477, 59.904129], [30.491327, 59.960579], [30.168344, 59.825733], [30.301088, 59.907742], [73.453178, 61.255978], [85.177414, 52.515366], [73.456734, 61.255062], [30.448624, 59.910351], [37.858791, 55.74551], [30.111698, 59.847164], [30.388626, 59.804993], [44.943924, 53.213825], [37.923306, 59.09375], [30.222057, 59.987171], [30.496153, 59.935627], [39.79274, 54.613075], [30.245024, 59.956047], [34.391724, 61.791557], [30.311073, 60.047928], [39.832741, 54.777611], [30.230551, 59.824471], [30.443539, 59.912037], [34.379421, 61.78743], [30.336432, 59.865856], [30.321133, 59.808334], [30.231846, 59.956741], [30.16925, 59.809929], [49.354168, 53.547222], [30.222057, 59.987171], [39.846497, 59.186054], [30.321089, 59.833145], [30.10704, 59.848518], [30.319431, 60.044483], [37.920521, 59.125759], [39.179169, 51.717499], [39.175278, 51.723331], [37.773571, 55.701473], [30.155834, 59.809006], [37.917946, 59.089333], [30.188337, 59.821945], [37.917946, 59.089333], [30.444202, 59.912117], [30.294247, 59.811092], [37.986801, 55.793999], [37.917946, 59.089333], [30.244759, 59.955982], [30.32514, 59.809891], [37.65847, 44.815666], [44.890411, 53.221928], [30.155834, 59.809006], [30.301603, 59.907833], [30.163561, 59.809929], [30.181974, 59.82613], [30.398174, 59.801132], [30.309454, 59.904526], [37.759468, 55.702091], [30.446316, 59.908722], [30.460428, 59.821934], [55.119999, 51.842499], [30.433374, 60.032806], [30.434361, 59.942982], [39.179169, 51.717499], [30.241325, 59.853703], [30.301603, 59.907833], [30.306528, 59.905758], [30.603579, 59.747532], [46.003017, 51.599789], [30.456095, 59.906143], [39.185905, 51.712513], [30.311073, 60.047928], [73.45211, 61.245491], [30.24472, 59.955975], [50.803726, 61.69672], [39.76746, 54.63065], [30.113703, 59.795944], [37.75803, 55.70266], [30.485064, 59.919563], [39.788334, 54.620098], [30.155834, 59.809006], [44.952351, 53.213383], [37.917946, 59.089333], [37.762081, 55.625462], [30.244759, 59.955982], [85.194534, 52.534084], [37.917946, 59.089333], [37.917946, 59.089333], [30.093334, 59.853798], [30.155834, 59.809006], [30.433374, 60.032806], [30.434814, 60.027538], [30.319431, 60.044483], [30.319431, 60.044483], [30.305201, 59.906319], [37.423634, 55.730919], [30.465088, 59.91737], [39.846497, 59.186054], [37.917946, 59.089333], [30.330246, 59.958157], [39.943333, 57.656326], [37.905693, 59.090752], [30.238499, 59.952374], [37.721348, 55.69471], [38.084274, 55.768669], [49.354168, 53.547222], [30.434814, 60.027538], [30.456181, 60.003971], [30.10704, 59.848518], [37.646229, 55.836586], [53.218311, 56.855015], [85.17691, 52.515163], [37.646229, 55.836586], [30.10704, 59.848518], [37.905693, 59.090752], [30.246279, 59.954113], [37.768295, 55.636665], [30.434814, 60.027538], [30.495838, 59.934013], [37.968353, 55.791592], [85.17691, 52.515163], [30.304995, 59.906403], [30.497301, 59.934628], [37.917946, 59.089333], [37.602978, 55.798016], [37.905693, 59.090752], [73.454353, 61.259205], [30.304995, 59.906403], [30.074743, 59.816082], [44.08337, 56.300865], [82.921669, 55.118889], [30.315357, 59.807198], [34.344788, 61.79546], [30.41795, 60.008476], [30.321112, 59.808956], [30.304995, 59.906403], [39.943333, 57.656326], [30.093334, 59.853798], [30.252354, 59.966106], [36.568531, 50.565289], [30.162861, 59.807167], [30.28861, 59.859779], [37.884361, 59.133583], [30.495838, 59.934013], [30.155834, 59.809006], [30.301603, 59.907833], [29.943457, 59.875278], [30.252354, 59.966106], [30.326496, 59.811073], [36.575207, 50.560352], [30.398251, 59.798973], [30.304995, 59.906403], [37.905693, 59.090752], [30.497301, 59.934628], [55.119999, 51.842499], [30.325132, 60.041878], [34.357529, 61.795834], [30.43483, 59.94669], [30.434814, 60.027538], [49.360985, 53.544167], [30.426298, 59.948257], [30.990528, 59.886272], [30.495152, 59.936497], [39.233639, 51.845276], [30.678865, 59.726051], [55.956112, 54.702778], [37.912388, 59.104279], [30.262327, 59.841892], [30.436115, 59.961884], [30.300783, 59.897026], [30.432152, 59.946026], [30.495152, 59.936497], [73.411049, 61.262402], [37.917946, 59.089333], [30.319431, 60.044483], [30.446316, 59.908722], [30.20606, 59.843914], [34.357529, 61.795834], [30.330204, 60.044949], [30.077362, 59.847858], [85.203117, 52.545303], [34.359081, 61.795567], [30.495152, 59.936497], [30.614849, 59.731327], [73.457802, 61.260803], [37.917946, 59.089333], [37.911888, 59.09864], [30.24472, 59.955975], [37.917946, 59.089333], [30.237879, 59.949268], [55.126667, 51.850277], [30.446316, 59.908722], [37.639313, 55.823074], [30.446316, 59.908722], [30.16925, 59.809929], [73.399101, 61.266384], [43.921734, 56.318165], [30.162861, 59.807167], [30.43483, 59.94669], [30.311073, 60.047928], [30.163561, 59.809929], [30.494091, 59.933277], [30.215363, 59.985455], [37.636436, 55.820198], [30.433374, 60.032806], [37.636436, 55.820198], [30.24472, 59.955975], [37.636436, 55.820198], [30.4863, 59.929993], [30.319431, 60.044483], [30.431534, 59.946743], [37.636436, 55.820198], [30.222057, 59.987171], [37.917946, 59.089333], [30.614849, 59.731522], [34.398857, 61.767227], [30.444202, 59.912117], [34.316521, 61.811954], [30.067728, 59.845058], [30.24472, 59.955975], [30.111698, 59.847164], [30.321112, 59.808956], [34.357529, 61.795834], [30.458803, 59.878021], [53.2328, 56.860466], [30.093334, 59.854534], [30.222057, 59.987171], [30.432152, 59.946026], [30.308735, 59.824333], [30.222057, 59.987171], [30.304995, 59.906403], [43.911018, 56.322903], [30.222057, 59.987171], [37.917946, 59.089333], [30.111698, 59.847164], [39.838715, 57.673241], [37.965401, 55.7985], [30.398174, 59.801132], [37.333935, 55.69841], [30.434814, 60.027538], [30.093334, 59.854534], [30.678865, 59.726051], [30.222057, 59.987171], [73.460754, 61.261993], [39.943333, 57.656326], [30.289444, 59.910774], [37.785553, 55.750759], [37.858791, 55.74551], [30.420341, 59.933277], [30.309454, 59.904526], [30.222057, 59.987171], [30.581711, 59.754112], [30.678865, 59.726051], [30.093334, 59.853798], [53.328732, 56.845451], [30.678865, 59.726051], [37.858791, 55.74551], [30.304995, 59.906403], [30.222057, 59.987171], [39.838715, 57.673241], [30.10704, 59.848518], [37.951138, 59.128277], [30.614849, 59.731327], [30.304995, 59.906403], [30.093334, 59.854534], [37.917946, 59.089333], [39.828945, 59.198349], [44.082855, 56.300865], [30.282223, 59.825718], [30.394876, 59.929958], [30.215363, 59.985455], [30.103153, 59.772823], [30.256298, 59.958683], [37.917946, 59.089333], [37.883728, 55.787594], [30.444202, 59.912117], [30.244577, 59.952461], [30.222057, 59.987171], [30.43483, 59.94669], [30.412264, 59.795349], [34.350983, 61.795288], [30.226297, 59.984493], [30.434814, 60.027538], [30.304995, 59.906403], [30.370258, 60.049549], [39.87075, 59.19664], [30.162861, 59.807167], [30.24472, 59.955975], [55.119999, 51.842499], [30.16925, 59.809929], [39.838715, 57.673241], [37.728458, 55.692741], [37.721348, 55.69405], [46.001667, 51.598331], [30.497301, 59.934628], [30.323803, 59.808727], [30.678865, 59.726051], [37.910114, 59.094299], [30.314676, 59.910892], [30.321112, 59.808956], [30.24472, 59.955975], [30.311073, 60.047928], [37.9851, 55.798199], [30.20483, 59.847942], [30.309454, 59.904526], [37.858791, 55.74551], [30.222057, 59.987171], [34.436424, 61.754345], [30.269926, 59.95079], [30.155834, 59.809006], [30.125208, 59.807083], [30.155834, 59.809006], [37.6343, 55.819759], [30.304995, 59.906403], [30.569349, 59.749855], [38.073421, 55.756794], [30.216248, 59.985374], [46.001667, 51.598331], [49.472778, 53.486389], [30.250736, 59.952724], [30.222057, 59.987171], [37.726757, 55.695724], [30.326885, 59.818264], [34.357529, 61.795834], [39.788334, 54.620098], [30.321112, 59.808956], [38.042469, 55.784851], [30.326496, 59.811073], [30.426285, 59.942333], [30.427782, 60.030994], [49.472778, 53.486389], [30.372793, 59.809406], [38.087799, 55.758202], [30.222057, 59.987171], [30.24472, 59.955975], [39.913994, 57.661671], [30.309147, 59.722343], [37.9851, 55.798199], [30.313444, 60.051159], [55.971943, 54.725277], [30.215363, 59.985455], [30.434814, 60.027538], [30.163561, 59.809929], [37.9851, 55.798199], [30.155834, 59.809006], [38.054485, 55.754776], [37.905693, 59.090752], [46.001667, 51.598331], [30.434557, 60.028271], [30.491203, 59.941776], [30.398251, 59.798973], [38.081207, 55.760586], [30.30094, 59.877388], [37.449707, 55.839092], [30.319431, 60.044483], [30.321112, 59.808956], [30.269926, 59.95079], [30.228149, 59.979179], [37.917946, 59.089333], [37.86002, 55.741844], [30.416573, 60.030197], [30.509666, 59.933533], [30.398174, 59.801132], [44.08337, 56.300865], [30.215363, 59.985455], [30.678865, 59.726051], [39.79882, 54.613239], [30.244759, 59.955982], [30.323803, 59.808727], [37.720901, 55.683376], [37.917946, 59.089333], [37.905693, 59.090752], [37.334568, 55.699932], [30.24472, 59.955975], [30.24472, 59.955975], [30.301603, 59.909851], [34.357529, 61.795834], [30.444202, 59.912117], [30.433374, 60.032806], [30.320593, 60.016956], [30.154856, 59.809929], [30.418371, 59.943672], [50.806854, 61.698402], [37.79837, 44.672077], [37.982121, 55.798565], [30.222057, 59.987171], [73.456734, 61.255062], [30.093334, 59.854534], [30.320593, 60.016956], [37.917946, 59.089333], [30.324156, 60.044415], [30.495152, 59.936497], [30.304995, 59.906403], [30.162861, 59.807167], [39.883251, 57.625416], [34.291599, 61.771461], [38.088821, 55.754051], [30.24472, 59.955975], [49.361942, 53.544167], [30.245024, 59.956047], [30.245024, 59.956047], [30.434814, 60.027538], [30.491001, 59.9375], [30.310421, 59.916752], [34.344788, 61.79546], [39.943333, 57.656326], [49.475159, 53.485199], [30.24971, 59.951832], [30.321112, 59.808956], [30.433374, 60.032806], [30.398251, 59.798973], [30.678865, 59.726051], [37.905693, 59.090752], [37.661068, 55.759171], [30.434814, 60.027538], [30.162861, 59.807167], [30.318043, 60.045853], [30.316566, 60.051048], [30.39872, 59.81171], [30.168381, 59.81007], [30.614849, 59.730091], [30.222057, 59.987171], [37.928783, 59.122658], [30.43483, 59.94669], [30.614849, 59.730091], [37.858791, 55.74551], [30.256298, 59.958683], [30.315216, 59.810055], [30.230844, 59.945786], [37.759468, 55.702091], [34.357529, 61.795834], [30.446316, 59.908722], [38.093479, 55.754391], [30.448624, 59.910351], [37.917946, 59.089333], [30.497301, 59.934628], [38.994324, 55.789764], [37.89764, 59.130924], [46.001667, 51.598331], [30.319431, 60.044483], [37.985085, 55.798168], [30.093334, 59.854534], [30.316261, 60.040699], [30.409195, 59.879417], [49.354168, 53.547222], [30.304995, 59.906403], [30.323803, 59.808727], [37.9851, 55.798199], [30.123936, 59.854126], [30.432152, 59.946026], [37.923306, 59.09375], [34.355587, 61.801044], [30.495152, 59.936497], [30.10704, 59.848518], [30.10704, 59.848518], [30.432152, 59.946026], [30.43483, 59.94669], [37.444386, 55.82655], [30.272902, 60.002865], [30.31587, 60.046764], [30.240139, 59.843056], [30.422112, 59.867386], [30.093334, 59.853798], [30.111698, 59.847164], [30.429876, 59.947224], [30.10704, 59.848518], [30.464001, 59.911049], [30.111698, 59.847164], [30.34177, 59.95805], [30.33359, 60.052299], [37.917946, 59.089333], [37.905693, 59.090752], [37.770718, 55.69952], [37.761578, 55.702179], [37.965401, 55.7985], [44.0658, 56.300865], [37.985085, 55.798168], [30.111698, 59.847164], [30.155834, 59.809006], [39.179169, 51.717499], [37.917946, 59.089333], [37.654186, 55.741455], [30.43483, 59.94669], [39.79274, 54.613075], [30.168381, 59.81007], [30.168381, 59.81007], [30.313948, 60.049637], [30.319431, 60.044483], [38.073421, 55.756794], [37.917946, 59.089333], [30.222057, 59.987171], [37.923306, 59.09375], [30.155834, 59.809006], [30.093334, 59.853798], [30.093334, 59.85556], [30.319431, 60.044483], [30.092058, 59.846962], [30.156893, 59.808079], [30.231226, 59.946014], [37.856239, 55.746342], [34.354527, 61.795582], [30.448624, 59.910351], [30.434814, 60.027538], [30.283903, 59.902214], [30.495152, 59.936497], [37.917946, 59.089333], [30.429016, 60.033619], [82.947525, 55.108124], [30.446316, 59.908722], [30.331333, 60.043407], [30.155834, 59.809006], [37.636436, 55.820198], [30.495838, 59.934013], [30.28517, 59.976429], [30.440195, 59.913448], [30.315493, 59.926029], [30.266119, 59.952393], [30.111698, 59.847164], [30.319431, 60.044483], [39.788334, 54.620098], [37.334568, 55.703262], [37.334568, 55.703262], [30.300295, 59.933571], [30.107775, 59.818859], [30.297989, 59.907173], [37.642517, 55.828777], [30.378817, 60.126659], [30.222057, 59.987171], [30.321089, 59.833145], [30.321133, 59.808334], [30.328112, 59.916344], [30.32514, 59.809891], [44.082855, 56.300865], [38.000614, 59.119419], [38.000614, 59.119419], [37.917946, 59.089333], [30.325085, 59.912895], [73.456734, 61.255062], [30.245024, 59.956047], [30.301603, 59.909851], [30.427782, 60.030994], [30.215363, 59.985455], [46.001667, 51.598331], [34.354527, 61.795582], [49.354168, 53.547222], [30.093334, 59.854534], [44.039696, 56.284695], [30.378817, 60.126659], [37.917946, 59.089333], [39.826527, 59.192196], [34.344975, 61.794956], [30.398174, 59.801132], [30.162861, 59.807167], [37.883728, 55.787594], [30.446487, 59.938053], [30.222057, 59.987171], [30.34569, 59.90715], [30.235598, 59.825657], [30.321112, 59.808956], [37.917946, 59.089333], [30.316261, 60.040699], [30.315216, 59.810055], [30.315216, 59.810055], [46.003017, 51.599789], [30.294989, 59.808968], [34.344788, 61.79546], [30.277493, 59.942928], [30.323803, 59.808727], [30.319431, 60.044483], [30.163561, 59.809929], [30.50392, 59.98278], [30.24472, 59.955975], [30.432152, 59.946026], [30.572617, 59.956299], [30.434814, 60.027538], [30.093334, 59.85556], [34.357529, 61.795834], [30.325407, 59.913803], [34.357529, 61.795834], [30.332247, 60.01416], [30.516918, 59.944984], [30.319431, 60.044483], [34.357529, 61.795834], [30.50392, 59.98278], [30.398174, 59.801132], [37.910114, 59.094299], [30.111698, 59.847164], [30.222057, 59.987171], [30.446819, 59.886539], [30.296774, 59.859055], [30.093334, 59.854534], [86.75576, 53.86282], [30.434338, 59.953945], [44.048553, 56.288578], [30.43158, 59.946812], [30.022676, 59.705574], [30.495152, 59.936497], [30.40671, 59.803234], [73.457802, 61.260803], [37.728863, 44.737473], [30.30094, 59.877388], [46.001667, 51.598331], [73.457413, 61.313774], [30.213709, 60.142811], [39.95755, 57.658764], [39.90575, 59.200207], [30.576466, 59.741688], [30.435919, 59.945473], [37.930721, 59.103138], [55.996666, 54.696945], [44.082855, 56.300865], [85.183609, 52.52639], [38.073532, 55.761044], [34.357529, 61.795834], [37.883728, 55.787594], [30.50952, 59.977268], [49.361942, 53.544167], [30.177652, 59.819408], [30.448624, 59.910351], [30.222057, 59.987171], [37.9851, 55.798199], [30.163561, 59.809929], [30.303675, 59.893383], [30.233976, 59.995193], [30.323803, 59.808727], [73.457802, 61.260803], [34.357529, 61.795834], [30.448347, 60.030094], [30.321133, 59.808334], [30.464399, 60.047668], [30.464399, 60.047668], [30.08135, 59.740864], [34.395329, 61.784279], [37.905693, 59.090752], [30.428268, 59.975586], [30.227314, 59.981552], [30.134542, 59.834881], [37.917946, 59.089333], [30.269926, 59.95079], [30.155834, 59.809006], [30.403225, 59.804989], [30.1635, 59.824326], [30.448624, 59.910351], [30.155834, 59.809006], [30.448624, 59.910351], [30.215363, 59.985455], [30.434814, 60.027538], [30.252354, 59.966106], [30.403225, 59.804989], [30.798817, 60.316898], [34.357529, 61.795834], [34.354527, 61.795582], [30.217274, 59.952969], [39.717949, 54.650875], [39.79274, 54.613075], [37.982121, 55.798565], [30.3141, 60.047661], [30.376097, 59.804626], [49.328609, 53.528332], [73.458878, 61.261238], [30.301603, 59.909851], [30.444202, 59.912117], [30.448624, 59.910351], [44.943333, 53.223331], [37.923306, 59.09375], [30.327559, 59.862499], [37.968353, 55.791592], [30.24472, 59.955975], [30.315216, 59.810055], [49.356201, 53.543888], [82.921669, 55.118889], [30.315357, 59.807198], [37.905693, 59.090752], [30.321112, 59.808956], [30.416573, 60.030197]]" 12 | 13 | type DBScanSuite struct { 14 | points PointList 15 | } 16 | 17 | var _ = Suite(&DBScanSuite{}) 18 | 19 | func (s *DBScanSuite) SetUpTest(c *C) { 20 | type jsonPoints [][]float64 21 | 22 | pts := jsonPoints{} 23 | 24 | err := json.Unmarshal([]byte(jsonPointsData), &pts) 25 | c.Assert(err, IsNil) 26 | 27 | s.points = make(PointList, len(pts)) 28 | 29 | for i := 0; i < len(pts); i++ { 30 | s.points[i][0] = pts[i][0] 31 | s.points[i][1] = pts[i][1] 32 | } 33 | } 34 | 35 | func (s *DBScanSuite) TestRangeQueryKDTree(c *C) { 36 | // Verify that KD-Tree & RangeQuery give the same results 37 | tree := NewKDTree(s.points) 38 | eps := 0.8 / EarthR / DegreeRad 39 | 40 | _, ok := tree.invariantHolds(tree.Root) 41 | c.Assert(ok, Equals, true) 42 | 43 | for _, pt := range s.points { 44 | pts1 := tree.InRange(pt, eps, nil) 45 | pts2 := RegionQuery(s.points, &pt, eps) 46 | 47 | sort.Ints(pts1) 48 | sort.Ints(pts2) 49 | 50 | c.Assert(pts1, DeepEquals, pts2) 51 | } 52 | 53 | } 54 | 55 | func (s *DBScanSuite) TestDBScan(c *C) { 56 | clusters, noise := DBScan(s.points, 0.8, 10) 57 | 58 | goodClusters := []Cluster{ 59 | {C: 0, Points: []int{0, 17, 19, 30, 282, 353, 363, 446, 460, 510, 620, 694, 703, 711, 785, 801, 810, 943, 1104, 1136, 1147, 1184, 1235, 1344, 1466, 104, 569, 697, 760, 809, 1100, 1254, 10, 41, 42, 44, 47, 62, 69, 109, 110, 196, 216, 243, 273, 289, 309, 314, 327, 334, 350, 356, 409, 458, 487, 514, 593, 692, 698, 708, 722, 728, 730, 783, 799, 846, 875, 901, 912, 930, 1015, 1034, 1047, 1115, 1128, 1162, 1195, 1201, 1202, 1225, 1227, 1228, 1366, 1399, 1495, 1017, 186, 635, 95, 855, 628, 677, 721, 1256, 1326, 966, 975, 1475, 76, 451, 1480}}, 60 | {C: 1, Points: []int{1, 53, 72, 163, 172, 175, 187, 189, 198, 205, 238, 268, 312, 349, 392, 428, 452, 485, 491, 496, 499, 584, 889, 918, 919, 934, 945, 986, 1032, 1067, 1110, 1157, 1169, 1176, 1206, 1229, 1237, 1242, 1330, 1334, 1368, 1402, 1455, 1474, 48, 1188, 1502, 80, 263}}, 61 | {C: 2, Points: []int{4, 91, 114, 119, 267, 338, 345, 377, 439, 494, 503, 504, 520, 531, 567, 640, 654, 661, 706, 764, 777, 784, 816, 826, 856, 867, 879, 881, 908, 917, 968, 972, 1022, 1025, 1028, 1114, 1117, 1137, 1139, 1170, 1172, 1208, 1221, 1243, 1247, 1306, 1312, 1313, 1320, 1325, 1338, 1380, 1397, 1449, 1467, 1471, 233}}, 62 | {C: 3, Points: []int{6, 16, 40, 74, 78, 84, 252, 270, 276, 290, 295, 318, 332, 333, 340, 378, 417, 430, 438, 443, 445, 447, 456, 469, 479, 481, 518, 536, 549, 551, 554, 626, 656, 658, 662, 674, 726, 727, 791, 798, 800, 808, 819, 843, 858, 1030, 1040, 1054, 1057, 1060, 1070, 1078, 1086, 1098, 1105, 1109, 1134, 1144, 1148, 1161, 1168, 1192, 1213, 1249, 1318, 1355, 1369, 1383, 1415, 1447, 1463, 1473, 1185}}, 63 | {C: 4, Points: []int{7, 174, 185, 218, 297, 440, 489, 687, 787, 811, 1063, 1087, 1118}}, 64 | {C: 5, Points: []int{9, 23, 31, 89, 90, 94, 100, 143, 149, 162, 164, 206, 210, 220, 236, 246, 287, 299, 344, 374, 390, 414, 419, 420, 492, 509, 558, 612, 616, 683, 731, 733, 747, 757, 792, 796, 814, 815, 818, 821, 823, 830, 833, 880, 884, 893, 894, 922, 949, 955, 963, 973, 979, 1058, 1077, 1085, 1091, 1111, 1126, 1132, 1141, 1203, 1220, 1273, 1352, 1367, 1488, 77, 371, 384, 1073, 1231, 411, 523, 1364, 1405, 179, 209, 352}}, 65 | {C: 6, Points: []int{12, 58, 96, 123, 125, 158, 161, 178, 188, 190, 208, 260, 286, 320, 342, 370, 379, 386, 394, 444, 459, 476, 482, 548, 555, 587, 644, 738, 780, 790, 854, 876, 959, 962, 976, 1049, 1123, 1127, 1153, 1155, 1183, 1196, 1236, 1255, 1274, 1357, 1359, 1386, 1389, 1390, 1395, 1452, 1456, 1496, 1499, 1501, 1150}}, 66 | {C: 7, Points: []int{13, 14, 155, 192, 265, 406, 426, 462, 478, 560, 609, 650, 660, 689, 739, 742, 771, 779, 883, 978, 1065, 1178, 1190, 1238, 1379, 1412, 1425, 1468, 1476, 627, 840, 376, 1485, 746, 1246, 148}}, 67 | {C: 8, Points: []int{22, 150, 151, 429, 474, 604, 619, 835, 939, 948}}, 68 | {C: 9, Points: []int{25, 26, 32, 50, 68, 224, 285, 288, 323, 337, 368, 416, 521, 524, 538, 566, 597, 599, 751, 766, 776, 839, 861, 936, 941, 1048, 1062, 1088, 1281, 1282, 1291, 1293, 1295, 1305, 1345, 1414}}, 69 | {C: 10, Points: []int{27, 227, 250, 269, 294, 336, 393, 493, 526, 916, 965, 1053, 1068, 1081, 1092, 1215, 1269, 1290, 1321, 1322, 1373, 1403, 1418, 1324}}, 70 | {C: 11, Points: []int{29, 33, 79, 115, 118, 130, 146, 153, 156, 157, 228, 278, 366, 380, 387, 396, 412, 442, 449, 466, 470, 513, 557, 576, 592, 638, 639, 671, 688, 695, 729, 734, 795, 797, 842, 868, 870, 874, 910, 914, 915, 926, 929, 942, 951, 953, 980, 1001, 1013, 1016, 1041, 1061, 1093, 1101, 1125, 1174, 1186, 1198, 1199, 1217, 1240, 1262, 1278, 1298, 1299, 1308, 1317, 1319, 1333, 1363, 1376, 1387, 1413, 1461, 1465, 1492, 1500, 311, 675, 793, 237, 684, 1014}}, 71 | {C: 12, Points: []int{36, 64, 75, 215, 303, 306, 343, 348, 388, 405, 433, 454, 477, 516, 522, 532, 561, 563, 577, 579, 580, 602, 762, 773, 817, 837, 851, 871, 886, 897, 1003, 1019, 1021, 1044, 1103, 1205, 1259, 1261, 1329, 1336, 1342, 1446, 1470, 1472, 1489, 1490, 519, 239, 200, 717, 1294, 193, 924}}, 72 | {C: 13, Points: []int{37, 97, 120, 145, 231, 335, 467, 488, 490, 570, 582, 611, 659, 670, 700, 768, 772, 804, 844, 946, 950, 971, 981, 990, 999, 1010, 1029, 1036, 1122, 1219, 1230, 1263, 1280, 1332, 1340, 1424, 106, 1177, 347, 1189, 171}}, 73 | {C: 14, Points: []int{46, 434, 537, 559, 564, 565, 1020, 1031, 1033, 1035, 1039, 1140, 1339, 2, 1353}}, 74 | {C: 15, Points: []int{56, 102, 124, 283, 293, 553, 568, 665, 686, 827, 857, 933, 1272, 1372, 67, 575, 578, 987, 1226, 1444, 1497, 774}}, 75 | {C: 16, Points: []int{61, 367, 423, 431, 525, 678, 704, 716, 957, 1095, 1191, 1360, 1438}}, 76 | {C: 17, Points: []int{65, 82, 105, 131, 147, 170, 173, 223, 230, 316, 322, 365, 383, 410, 427, 464, 498, 508, 527, 589, 632, 664, 666, 707, 714, 741, 756, 786, 788, 812, 848, 862, 899, 920, 921, 983, 1002, 1006, 1027, 1037, 1129, 1182, 1218, 1244, 1245, 1267, 1270, 1287, 1314, 1315, 1323, 1337, 1346, 1388, 1396, 1409, 1484, 1166, 126, 1297}}, 77 | {C: 18, Points: []int{99, 214, 219, 304, 391, 506, 581, 614, 775, 890, 985, 988, 998, 1026, 1038, 1055, 1106, 1156, 1251, 1277, 1283, 1284, 1292, 1310, 1400, 1422, 1435, 1420, 712, 1209}}, 78 | {C: 19, Points: []int{103, 111, 113, 203, 212, 221, 226, 229, 232, 241, 244, 296, 298, 329, 357, 403, 421, 545, 571, 572, 641, 669, 680, 709, 736, 743, 755, 758, 770, 789, 960, 984, 1005, 1009, 1050, 1108, 1151, 1204, 1232, 1258, 1279, 1328, 1371, 1378, 1393, 1404, 1406, 1410, 1441, 1454, 1478, 1479, 339, 507, 824}}, 79 | {C: 20, Points: []int{128, 140, 199, 279, 401, 495, 497, 633, 873, 1130, 1165, 1171, 1212, 1268, 1275, 1304, 1448, 1483, 330, 1064, 1302, 947, 1494}}, 80 | {C: 21, Points: []int{135, 331, 381, 400, 407, 408, 699, 701, 825, 992, 1069, 1080, 1083, 1124, 1193, 1239}}, 81 | {C: 22, Points: []int{154, 160, 167, 402, 404, 583, 585, 598, 600, 685, 702, 744, 834, 836, 954, 1012, 1071, 1214, 1365, 1426, 1453, 1487}}, 82 | {C: 23, Points: []int{169, 204, 272, 351, 422, 759, 761, 896, 1121, 1145, 1175, 1266, 1370, 1391, 1429}}, 83 | {C: 24, Points: []int{317, 324, 547, 715, 781, 822, 1011, 1042, 1090, 1248, 1252, 45}}, 84 | {C: 25, Points: []int{373, 455, 463, 473, 540, 541, 543, 724, 725, 885, 905, 1257, 1301, 86, 1300, 866}}, 85 | {C: 26, Points: []int{395, 432, 636, 838, 1075, 1084, 1133, 1187, 1253, 1327}}, 86 | {C: 27, Points: []int{502, 55, 255, 266, 359, 360, 450, 624, 631, 1491}}, 87 | {C: 28, Points: []int{505, 142, 281, 369, 512, 517, 748, 750, 845, 907, 1152, 1194, 1311, 1347, 1482}}} 88 | 89 | matched := make([]bool, 29) 90 | 91 | c.Check(clusters, HasLen, 29) 92 | 93 | for _, good := range goodClusters { 94 | sort.Ints(good.Points) 95 | } 96 | 97 | for _, cluster := range clusters { 98 | sort.Ints(cluster.Points) 99 | 100 | for _, good := range goodClusters { 101 | if reflect.DeepEqual(cluster.Points, good.Points) { 102 | if matched[good.C] { 103 | c.Fatal("already matched") 104 | } 105 | matched[good.C] = true 106 | break 107 | } 108 | } 109 | 110 | } 111 | 112 | for _, b := range matched { 113 | c.Check(b, Equals, true) 114 | } 115 | 116 | // Verify that clusters + noise cover whole set of points 117 | allPoints := make([]bool, len(s.points)) 118 | for _, i := range noise { 119 | allPoints[i] = true 120 | } 121 | 122 | for _, cluster := range clusters { 123 | for _, i := range cluster.Points { 124 | allPoints[i] = true 125 | } 126 | } 127 | 128 | for _, b := range allPoints { 129 | c.Check(b, Equals, true) 130 | } 131 | } 132 | 133 | func (s *DBScanSuite) BenchmarkDBScan(c *C) { 134 | for i := 0; i < c.N; i++ { 135 | DBScan(s.points, 0.8, 10) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /distance.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | const ( 8 | // DegreeRad is coefficient to translate from degrees to radians 9 | DegreeRad = math.Pi / 180.0 10 | // EarthR is earth radius in km 11 | EarthR = 6371.0 12 | ) 13 | 14 | // DistanceSpherical is a spherical (optimized) distance between two points 15 | // 16 | // Result is distance in kilometers 17 | func DistanceSpherical(p1, p2 *Point) float64 { 18 | v1 := (p1[1] - p2[1]) * DegreeRad 19 | v1 = v1 * v1 20 | 21 | v2 := (p1[0] - p2[0]) * DegreeRad * math.Cos((p1[1]+p2[1])/2.0*DegreeRad) 22 | v2 = v2 * v2 23 | 24 | return EarthR * math.Sqrt(v1+v2) 25 | } 26 | 27 | // FastSine caclulates sinus approximated to parabola 28 | // 29 | // Taken from: http://forum.devmaster.net/t/fast-and-accurate-sine-cosine/9648 30 | func FastSine(x float64) float64 { 31 | const ( 32 | B = 4 / math.Pi 33 | C = -4 / (math.Pi * math.Pi) 34 | P = 0.225 35 | ) 36 | 37 | if x > math.Pi || x < -math.Pi { 38 | panic("out of range") 39 | } 40 | 41 | y := B*x + C*x*math.Abs(x) 42 | return P*(y*math.Abs(y)-y) + y 43 | } 44 | 45 | // FastCos calculates cosinus from sinus 46 | func FastCos(x float64) float64 { 47 | x += math.Pi / 2.0 48 | for x > math.Pi { 49 | x -= 2 * math.Pi 50 | } 51 | 52 | return FastSine(x) 53 | } 54 | 55 | // DistanceSphericalFast calculates spherical distance with fast cosine 56 | // without sqrt and normalization to Earth radius/radians 57 | // 58 | // To get real distance in km, take sqrt and multiply result by EarthR*DegreeRad 59 | // 60 | // In this library eps (distance) is adjusted so that we don't need 61 | // to do sqrt and multiplication 62 | func DistanceSphericalFast(p1, p2 *Point) float64 { 63 | v1 := (p1[1] - p2[1]) 64 | v2 := (p1[0] - p2[0]) * FastCos((p1[1]+p2[1])/2.0*DegreeRad) 65 | 66 | return v1*v1 + v2*v2 67 | } 68 | -------------------------------------------------------------------------------- /distance_test.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | import ( 4 | "math" 5 | 6 | . "gopkg.in/check.v1" 7 | ) 8 | 9 | type DistanceSuite struct { 10 | p1, p2 Point 11 | } 12 | 13 | var _ = Suite(&DistanceSuite{}) 14 | 15 | func (s *DistanceSuite) SetUpTest(c *C) { 16 | s.p1 = Point{30.244759, 59.955982} 17 | s.p2 = Point{30.24472, 59.955975} 18 | } 19 | 20 | func (s *DistanceSuite) TestFastCos(c *C) { 21 | c.Check(FastCos(0), Equals, math.Cos(0)) 22 | c.Check(math.Abs(FastCos(0.1)-math.Cos(0.1)) < 0.001, Equals, true) 23 | c.Check(math.Abs(FastCos(-0.1)-math.Cos(-0.1)) < 0.001, Equals, true) 24 | c.Check(math.Abs(FastCos(1.0)-math.Cos(1.0)) < 0.001, Equals, true) 25 | } 26 | 27 | func (s *DistanceSuite) TestDistanceSpherical(c *C) { 28 | c.Check(DistanceSpherical(&s.p1, &s.p2), Equals, 0.0023064907653812116) 29 | c.Check(DistanceSpherical(&s.p2, &s.p1), Equals, 0.0023064907653812116) 30 | c.Check(DistanceSpherical(&s.p1, &s.p1), Equals, 0.0) 31 | c.Check(DistanceSpherical(&s.p2, &s.p2), Equals, 0.0) 32 | } 33 | 34 | func (s *DistanceSuite) TestDistanceSphericalFast(c *C) { 35 | c.Check(DistanceSphericalFast(&s.p1, &s.p2), Equals, 4.3026720164084415e-10) 36 | c.Check(DistanceSphericalFast(&s.p2, &s.p1), Equals, 4.3026720164084415e-10) 37 | c.Check(DistanceSphericalFast(&s.p1, &s.p1), Equals, 0.0) 38 | c.Check(DistanceSphericalFast(&s.p2, &s.p2), Equals, 0.0) 39 | 40 | c.Check(math.Abs(math.Sqrt(DistanceSphericalFast(&s.p1, &s.p2))*DegreeRad*EarthR- 41 | DistanceSpherical(&s.p1, &s.p2)) < 0.000001, Equals, true) 42 | } 43 | 44 | func (s *DistanceSuite) BenchmarkDistanceSpherical(c *C) { 45 | for i := 0; i < c.N; i++ { 46 | DistanceSpherical(&s.p1, &s.p2) 47 | } 48 | } 49 | 50 | func (s *DistanceSuite) BenchmarkDistanceSphericalFast(c *C) { 51 | for i := 0; i < c.N; i++ { 52 | DistanceSphericalFast(&s.p1, &s.p2) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/smira/go-point-clustering 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/kr/pretty v0.1.0 // indirect 7 | github.com/willf/bitset v1.1.10 8 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 2 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 3 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 4 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 5 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 6 | github.com/willf/bitset v1.1.10 h1:NotGKqX0KwQ72NUzqrjZq5ipPNDQex9lo3WpaS8L2sc= 7 | github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= 8 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 9 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 10 | -------------------------------------------------------------------------------- /kdtree.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | // This code is heavily based on https://godoc.org/code.google.com/p/eaburns/kdtree 4 | // 5 | // Original code is under New BSD License. 6 | // Author: Ethan Burns 7 | 8 | import ( 9 | "sort" 10 | ) 11 | 12 | // KDTree is implementation of K-D Tree, with Points separated from 13 | // nodes. 14 | // 15 | // Nodes (T) hold only indices into Points slice 16 | type KDTree struct { 17 | Points PointList 18 | Root *T 19 | } 20 | 21 | // A T is a the node of a K-D tree. A *T is the root of a K-D tree, 22 | // and nil is an empty K-D tree. 23 | type T struct { 24 | // Point is the K-dimensional point associated with the 25 | // data of this node. 26 | PointID int 27 | EqualIDs []int 28 | 29 | split int 30 | left, right *T 31 | } 32 | 33 | // Insert returns a new K-D tree with the given node inserted. 34 | // Inserting a node that is already a member of a K-D tree 35 | // invalidates that tree. 36 | func (tree *KDTree) Insert(point Point) { 37 | tree.Points = append(tree.Points, point) 38 | tree.Root = tree.insert(tree.Root, 0, &T{PointID: len(tree.Points) - 1}) 39 | } 40 | 41 | func (tree *KDTree) insert(t *T, depth int, n *T) *T { 42 | if t == nil { 43 | n.split = depth % 2 44 | n.left, n.right = nil, nil 45 | return n 46 | } 47 | if tree.Points[n.PointID][t.split] < tree.Points[t.PointID][t.split] { 48 | t.left = tree.insert(t.left, depth+1, n) 49 | } else { 50 | t.right = tree.insert(t.right, depth+1, n) 51 | } 52 | return t 53 | } 54 | 55 | // InRange appends all nodes in the K-D tree that are within a given 56 | // distance from the given point to the given slice, which may be nil. 57 | // To avoid allocation, the slice can be pre-allocated with a larger 58 | // capacity and re-used across multiple calls to InRange. 59 | func (tree *KDTree) InRange(pt Point, dist float64, nodes []int) []int { 60 | if dist < 0 { 61 | return nodes 62 | } 63 | return tree.inRange(tree.Root, &pt, dist, nodes) 64 | } 65 | 66 | func (tree *KDTree) inRange(t *T, pt *Point, r float64, nodes []int) []int { 67 | if t == nil { 68 | return nodes 69 | } 70 | 71 | diff := pt[t.split] - tree.Points[t.PointID][t.split] 72 | 73 | thisSide, otherSide := t.right, t.left 74 | if diff < 0 { 75 | thisSide, otherSide = t.left, t.right 76 | } 77 | 78 | p1 := Point{} 79 | p1[1-t.split] = (pt[1-t.split] + tree.Points[t.PointID][1-t.split]) / 2 80 | p1[t.split] = pt[t.split] 81 | 82 | p2 := Point{} 83 | p2[1-t.split] = (pt[1-t.split] + tree.Points[t.PointID][1-t.split]) / 2 84 | p2[t.split] = tree.Points[t.PointID][t.split] 85 | 86 | dist := p1.sqDist(&p2) 87 | 88 | nodes = tree.inRange(thisSide, pt, r, nodes) 89 | if dist <= r*r { 90 | if tree.Points[t.PointID].sqDist(pt) < r*r { 91 | nodes = append(nodes, t.PointID) 92 | nodes = append(nodes, t.EqualIDs...) 93 | } 94 | nodes = tree.inRange(otherSide, pt, r, nodes) 95 | } 96 | 97 | return nodes 98 | } 99 | 100 | // Height returns the height of the K-D tree. 101 | func (tree *KDTree) Height() int { 102 | return tree.Root.height() 103 | } 104 | 105 | func (t *T) height() int { 106 | if t == nil { 107 | return 0 108 | } 109 | ht := t.left.height() 110 | if rht := t.right.height(); rht > ht { 111 | ht = rht 112 | } 113 | return ht + 1 114 | } 115 | 116 | // NewKDTree returns a new K-D tree built using the given nodes. 117 | func NewKDTree(points PointList) *KDTree { 118 | result := &KDTree{ 119 | Points: points, 120 | } 121 | 122 | if len(points) > 0 { 123 | result.Root = buildTree(0, preSort(result.Points)) 124 | } 125 | 126 | return result 127 | } 128 | 129 | // buildTree does iteration of node building: it finds median 130 | // point, and builds tree node with median (and all points equal to 131 | // median), calling itself recursively for left and right subtrees 132 | func buildTree(depth int, nodes *preSorted) *T { 133 | split := depth % 2 134 | switch len(nodes.cur[split]) { 135 | case 0: 136 | return nil 137 | case 1: 138 | return &T{ 139 | PointID: nodes.cur[split][0], 140 | split: split, 141 | } 142 | } 143 | med, equal, left, right := nodes.splitMed(split) 144 | return &T{ 145 | PointID: med, 146 | EqualIDs: equal, 147 | split: split, 148 | left: buildTree(depth+1, &left), 149 | right: buildTree(depth+1, &right), 150 | } 151 | } 152 | 153 | // preSorted holds the nodes pre-sorted on each dimension. 154 | type preSorted struct { 155 | points PointList 156 | 157 | // cur is the currently sorted set of point IDs by dimension 158 | cur [2][]int 159 | } 160 | 161 | // PreSort returns the nodes pre-sorted on each dimension. 162 | func preSort(points PointList) *preSorted { 163 | p := new(preSorted) 164 | p.points = points 165 | for i := range p.cur { 166 | p.cur[i] = make([]int, len(points)) 167 | for j := range p.cur[i] { 168 | p.cur[i][j] = j 169 | } 170 | sort.Sort(&nodeSorter{i, p.cur[i], points}) 171 | } 172 | return p 173 | } 174 | 175 | // SplitMed returns the median node on the split dimension and two 176 | // preSorted structs that contain the nodes (still sorted on each 177 | // dimension) that are less than and greater than or equal to the 178 | // median node value on the given splitting dimension. 179 | func (p *preSorted) splitMed(dim int) (med int, equal []int, left, right preSorted) { 180 | m := len(p.cur[dim]) / 2 181 | for m > 0 && p.points[p.cur[dim][m-1]][dim] == p.points[p.cur[dim][m]][dim] { 182 | m-- 183 | } 184 | mh := m 185 | for mh < len(p.cur[dim])-1 && p.points[p.cur[dim][mh+1]] == p.points[p.cur[dim][m]] { 186 | mh++ 187 | } 188 | med = p.cur[dim][m] 189 | equal = p.cur[dim][m+1 : mh+1] 190 | pivot := p.points[med][dim] 191 | 192 | left.points = p.points 193 | left.cur[dim] = p.cur[dim][:m] 194 | 195 | right.points = p.points 196 | right.cur[dim] = p.cur[dim][mh+1:] 197 | 198 | for d := range p.cur { 199 | if d == dim { 200 | continue 201 | } 202 | 203 | left.cur[d] = make([]int, 0, len(p.cur)) 204 | right.cur[d] = make([]int, 0, len(p.cur)) 205 | 206 | for _, n := range p.cur[d] { 207 | if n == med { 208 | continue 209 | } 210 | skip := false 211 | for _, x := range equal { 212 | if n == x { 213 | skip = true 214 | break 215 | } 216 | } 217 | if skip { 218 | continue 219 | } 220 | if p.points[n][dim] < pivot { 221 | left.cur[d] = append(left.cur[d], n) 222 | } else { 223 | right.cur[d] = append(right.cur[d], n) 224 | } 225 | } 226 | } 227 | 228 | return 229 | } 230 | 231 | // A nodeSorter implements sort.Interface, sorting the nodes 232 | // in ascending order of their point values on the split dimension. 233 | type nodeSorter struct { 234 | split int 235 | nodes []int 236 | points PointList 237 | } 238 | 239 | func (n *nodeSorter) Len() int { 240 | return len(n.nodes) 241 | } 242 | 243 | func (n *nodeSorter) Swap(i, j int) { 244 | n.nodes[i], n.nodes[j] = n.nodes[j], n.nodes[i] 245 | } 246 | 247 | func (n *nodeSorter) Less(i, j int) bool { 248 | a, b := n.points[n.nodes[i]][n.split], n.points[n.nodes[j]][n.split] 249 | if a == b { 250 | return n.points[n.nodes[i]][1-n.split] < n.points[n.nodes[j]][1-n.split] 251 | } 252 | return a < b 253 | } 254 | -------------------------------------------------------------------------------- /kdtree_bench_test.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | ) 7 | 8 | // RadiusMax is the maximum radius for InRange benchmarks. 9 | const radiusMax = 0.1 10 | 11 | // BenchmarkInsert benchmarks insertions into an initially empty tree. 12 | func BenchmarkInsert(b *testing.B) { 13 | b.StopTimer() 14 | pts := make([]Point, b.N) 15 | for i := range pts { 16 | for j := range pts[i] { 17 | pts[i][j] = rand.Float64() 18 | } 19 | } 20 | 21 | b.StartTimer() 22 | var tree = NewKDTree(nil) 23 | for _, pt := range pts { 24 | tree.Insert(pt) 25 | } 26 | } 27 | 28 | // BenchmarkInsert1000 benchmarks 1000 insertions into an empty tree. 29 | func BenchmarkInsert1000(b *testing.B) { 30 | insertSz(1000, b) 31 | } 32 | 33 | // BenchmarkInsert500 benchmarks 500 insertions into an empty tree. 34 | func BenchmarkInsert500(b *testing.B) { 35 | insertSz(500, b) 36 | } 37 | 38 | // InsertSz benchmarks inserting sz nodes into an empty tree. 39 | func insertSz(sz int, b *testing.B) { 40 | b.StopTimer() 41 | pts := make([]Point, sz) 42 | for i := range pts { 43 | for j := range pts[i] { 44 | pts[i][j] = rand.Float64() 45 | } 46 | } 47 | 48 | b.StartTimer() 49 | for i := 0; i < b.N; i++ { 50 | tree := NewKDTree(nil) 51 | for j := range pts { 52 | tree.Insert(pts[j]) 53 | } 54 | } 55 | 56 | } 57 | 58 | // BenchmarkMake1000 benchmarks Make with 1000 nodes. 59 | func BenchmarkMake1000(b *testing.B) { 60 | makeSz(1000, b) 61 | } 62 | 63 | // BenchmarkMake500 benchmarks Make with 500 nodes. 64 | func BenchmarkMake500(b *testing.B) { 65 | makeSz(500, b) 66 | } 67 | 68 | // MakeSz benchmarks Make with a given number of nodes. 69 | // The time includes allocating the nodes. 70 | func makeSz(sz int, b *testing.B) { 71 | b.StopTimer() 72 | pts := make([]Point, sz) 73 | for i := range pts { 74 | for j := range pts[i] { 75 | pts[i][j] = rand.Float64() 76 | } 77 | } 78 | 79 | b.StartTimer() 80 | 81 | for i := 0; i < b.N; i++ { 82 | NewKDTree(pts) 83 | } 84 | 85 | } 86 | 87 | func BenchmarkMakeInRange1000(b *testing.B) { 88 | newInRangeSz(1000, b) 89 | } 90 | 91 | func BenchmarkMakeInRange500(b *testing.B) { 92 | newInRangeSz(500, b) 93 | } 94 | 95 | // newInRangeSz benchmarks InRange function on a tree 96 | // created with New with the given number of nodes. 97 | func newInRangeSz(sz int, b *testing.B) { 98 | b.StopTimer() 99 | pts := make(PointList, sz) 100 | for i := range pts { 101 | for j := range pts[i] { 102 | pts[i][j] = rand.Float64() 103 | } 104 | } 105 | tree := NewKDTree(pts) 106 | 107 | points := make([]Point, b.N) 108 | for i := range points { 109 | for j := range points[i] { 110 | points[i][j] = rand.Float64() 111 | } 112 | } 113 | rs := make([]float64, b.N) 114 | for i := range rs { 115 | rs[i] = rand.Float64() 116 | } 117 | 118 | pool := make([]int, 0, sz) 119 | 120 | b.StartTimer() 121 | for i, pt := range points { 122 | tree.InRange(pt, rs[i]*radiusMax, pool[:0]) 123 | } 124 | } 125 | 126 | func BenchmarkInsertInRange1000(b *testing.B) { 127 | insertInRangeSz(1000, b) 128 | } 129 | 130 | func BenchmarkInsertInRange500(b *testing.B) { 131 | insertInRangeSz(500, b) 132 | } 133 | 134 | // insertInRangeSz benchmarks InRange function on a tree 135 | // created with repeated calls to Insert with the given number 136 | // of nodes. 137 | func insertInRangeSz(sz int, b *testing.B) { 138 | b.StopTimer() 139 | tree := NewKDTree(nil) 140 | 141 | for i := 0; i < sz; i++ { 142 | var pt Point 143 | for j := range pt { 144 | pt[j] = rand.Float64() 145 | } 146 | tree.Insert(pt) 147 | } 148 | 149 | points := make([]Point, b.N) 150 | for i := range points { 151 | for j := range points[i] { 152 | points[i][j] = rand.Float64() 153 | } 154 | } 155 | rs := make([]float64, b.N) 156 | for i := range rs { 157 | rs[i] = rand.Float64() 158 | } 159 | 160 | pool := make([]int, 0, sz) 161 | 162 | b.StartTimer() 163 | for i, pt := range points { 164 | tree.InRange(pt, rs[i]*radiusMax, pool[:0]) 165 | } 166 | } 167 | 168 | // BenchmarkInRangeLiner1000 benchmarks computing the in range 169 | // nodes via a linear scan. 170 | func BenchmarkInRangeLinear1000(b *testing.B) { 171 | inRangeLinearSz(1000, b) 172 | } 173 | 174 | // inRangeLinearSz benchmarks computing in range nodes using 175 | // a linear scan of the given number of nodes. 176 | func inRangeLinearSz(sz int, b *testing.B) { 177 | b.StopTimer() 178 | pts := make([]Point, sz) 179 | for i := range pts { 180 | for j := range pts[i] { 181 | pts[i][j] = rand.Float64() 182 | } 183 | } 184 | 185 | points := make([]Point, b.N) 186 | for i := range points { 187 | for j := range points[i] { 188 | points[i][j] = rand.Float64() 189 | } 190 | } 191 | rs := make([]float64, b.N) 192 | for i := range rs { 193 | rs[i] = rand.Float64() * radiusMax 194 | } 195 | 196 | local := make([]int, 0, sz) 197 | 198 | b.StartTimer() 199 | for i, pt := range points { 200 | local = local[:0] 201 | rr := rs[i] * rs[i] 202 | for j := range pts { 203 | if pts[j].sqDist(&pt) < rr { 204 | local = append(local, j) 205 | } 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /kdtree_test.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | "reflect" 7 | "testing" 8 | "testing/quick" 9 | ) 10 | 11 | // A pointSlice is a slice of points that implements the quick.Generator 12 | // interface, generating a random set of points on the unit square. 13 | type pointSlice []Point 14 | 15 | func (pointSlice) Generate(r *rand.Rand, size int) reflect.Value { 16 | ps := make([]Point, size) 17 | for i := range ps { 18 | for j := range ps[i] { 19 | ps[i][j] = r.Float64() 20 | } 21 | } 22 | return reflect.ValueOf(ps) 23 | } 24 | 25 | // Generate implements the Generator interface for Points 26 | func (p Point) Generate(r *rand.Rand, _ int) reflect.Value { 27 | for i := range p { 28 | p[i] = r.Float64() 29 | } 30 | return reflect.ValueOf(p) 31 | } 32 | 33 | // TestInsert tests the insert function, ensuring that random points 34 | // inserted into an empty tree maintain the K-D tree invariant. 35 | func TestInsert(t *testing.T) { 36 | if err := quick.Check(func(pts pointSlice) bool { 37 | var tree = NewKDTree(nil) 38 | for _, p := range pts { 39 | tree.Insert(p) 40 | } 41 | _, ok := tree.invariantHolds(tree.Root) 42 | return ok 43 | }, nil); err != nil { 44 | t.Error(err) 45 | } 46 | } 47 | 48 | // TestMake tests the Make function, ensuring that a tree built 49 | // using random points respects the K-D tree invariant. 50 | func TestMake(t *testing.T) { 51 | if err := quick.Check(func(pts pointSlice) bool { 52 | tree := NewKDTree(PointList(pts)) 53 | _, ok := tree.invariantHolds(tree.Root) 54 | return ok 55 | }, nil); err != nil { 56 | t.Error(err) 57 | } 58 | } 59 | 60 | // TestInRange tests the InRange function, ensuring that all points 61 | // in the range are reported, and all points reported are indeed in 62 | // the range. 63 | func TestInRange(t *testing.T) { 64 | if err := quick.Check(func(pts pointSlice, pt Point, r float64) bool { 65 | r = math.Abs(r) 66 | tree := NewKDTree(PointList(pts)) 67 | 68 | in := make(map[int]bool, len(pts)) 69 | for _, n := range tree.InRange(pt, r, nil) { 70 | in[n] = true 71 | } 72 | 73 | num := 0 74 | for i, p := range pts { 75 | if pt.sqDist(&p) <= r*r { 76 | num++ 77 | if !in[i] { 78 | return false 79 | } 80 | } 81 | } 82 | return num == len(in) 83 | }, nil); err != nil { 84 | t.Error(err) 85 | } 86 | } 87 | 88 | // InvariantHolds returns the points in this subtree, and a bool 89 | // that is true if the K-D tree invariant holds. The K-D tree invariant 90 | // states that all points in the left subtree have values less than that 91 | // of the current node on the splitting dimension, and the points 92 | // in the right subtree have values greater than or equal to that of 93 | // the current node. 94 | func (tree *KDTree) invariantHolds(t *T) ([]Point, bool) { 95 | if t == nil { 96 | return []Point{}, true 97 | } 98 | 99 | ok := true 100 | 101 | for _, i := range t.EqualIDs { 102 | if tree.Points[i] != tree.Points[t.PointID] { 103 | ok = false 104 | break 105 | } 106 | } 107 | 108 | left, leftOk := tree.invariantHolds(t.left) 109 | right, rightOk := tree.invariantHolds(t.right) 110 | 111 | ok = ok && leftOk && rightOk 112 | 113 | if ok { 114 | for _, l := range left { 115 | if l[t.split] >= tree.Points[t.PointID][t.split] { 116 | ok = false 117 | break 118 | } 119 | } 120 | } 121 | if ok { 122 | for _, r := range right { 123 | if r[t.split] < tree.Points[t.PointID][t.split] { 124 | ok = false 125 | break 126 | } 127 | } 128 | } 129 | 130 | return append(append(left, tree.Points[t.PointID]), right...), ok 131 | } 132 | 133 | func TestPreSort(t *testing.T) { 134 | if err := quick.Check(func(pts pointSlice) bool { 135 | p := preSort(PointList(pts)) 136 | for i := range p.cur { 137 | if !isSortedOnDim(i, p.cur[i], pts) || len(p.cur[i]) != len(pts) { 138 | return false 139 | } 140 | } 141 | return true 142 | }, nil); err != nil { 143 | t.Error(err) 144 | } 145 | } 146 | 147 | func TestPreSort_SplitMed(t *testing.T) { 148 | if err := quick.Check(func(pts pointSlice, dim int) bool { 149 | if len(pts) == 0 { 150 | return true 151 | } 152 | if dim < 0 { 153 | dim = -dim 154 | } 155 | dim %= 2 156 | 157 | sorted := preSort(PointList(pts)) 158 | med, equal, left, right := sorted.splitMed(dim) 159 | for _, p := range equal { 160 | if pts[p] != pts[med] { 161 | return false 162 | } 163 | } 164 | 165 | if len(left.cur[dim])+len(right.cur[dim])+1+len(equal) != len(pts) { 166 | return false 167 | } 168 | 169 | for i, p := range [2]*preSorted{&left, &right} { 170 | for d, ns := range p.cur { 171 | if len(ns) != len(p.cur[0]) { 172 | return false 173 | } 174 | if !isSortedOnDim(d, ns, pts) { 175 | return false 176 | } 177 | for _, n := range ns { 178 | if i == 0 && pts[n][dim] >= pts[med][dim] { 179 | return false 180 | } else if i == 1 && pts[n][dim] < pts[med][dim] { 181 | return false 182 | } 183 | } 184 | } 185 | } 186 | 187 | return true 188 | }, nil); err != nil { 189 | t.Error(err) 190 | } 191 | } 192 | 193 | // IsSortedOnDim returns true if the given slice is in sorted order 194 | // on the given dimension. 195 | func isSortedOnDim(dim int, nodes []int, pts pointSlice) bool { 196 | if len(nodes) == 0 { 197 | return true 198 | } 199 | prev := pts[nodes[0]][dim] 200 | for _, n := range nodes { 201 | if pts[n][dim] < prev { 202 | return false 203 | } 204 | prev = pts[n][dim] 205 | } 206 | return true 207 | } 208 | -------------------------------------------------------------------------------- /point.go: -------------------------------------------------------------------------------- 1 | // Package cluster implements DBScan clustering on (lat, lon) using K-D Tree 2 | package cluster 3 | 4 | // Point is longitue, latittude 5 | type Point [2]float64 6 | 7 | // PointList is a slice of Points 8 | type PointList []Point 9 | 10 | // Cluster is a result of DBScan work 11 | type Cluster struct { 12 | C int 13 | Points []int 14 | } 15 | 16 | // sqDist returns squared (w/o sqrt & normalization) distance between two points 17 | func (a *Point) sqDist(b *Point) float64 { 18 | return DistanceSphericalFast(a, b) 19 | } 20 | 21 | // LessEq - a <= b 22 | func (a *Point) LessEq(b *Point) bool { 23 | return a[0] <= b[0] && a[1] <= b[1] 24 | } 25 | 26 | // GreaterEq - a >= b 27 | func (a *Point) GreaterEq(b *Point) bool { 28 | return a[0] >= b[0] && a[1] >= b[1] 29 | } 30 | 31 | // CentroidAndBounds calculates center and cluster bounds 32 | func (c *Cluster) CentroidAndBounds(points PointList) (center, min, max Point) { 33 | if len(c.Points) == 0 { 34 | panic("empty cluster") 35 | } 36 | 37 | min = Point{180.0, 90.0} 38 | max = Point{-180.0, -90.0} 39 | 40 | for _, i := range c.Points { 41 | pt := points[i] 42 | 43 | for j := range pt { 44 | center[j] += pt[j] 45 | 46 | if pt[j] < min[j] { 47 | min[j] = pt[j] 48 | } 49 | if pt[j] > max[j] { 50 | max[j] = pt[j] 51 | } 52 | } 53 | } 54 | 55 | for j := range center { 56 | center[j] /= float64(len(c.Points)) 57 | } 58 | 59 | return 60 | } 61 | 62 | // Inside checks if (innerMin, innerMax) rectangle is inside (outerMin, outMax) rectangle 63 | func Inside(innerMin, innerMax, outerMin, outerMax *Point) bool { 64 | return innerMin.GreaterEq(outerMin) && innerMax.LessEq(outerMax) 65 | } 66 | -------------------------------------------------------------------------------- /point_test.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | import ( 4 | "testing" 5 | 6 | . "gopkg.in/check.v1" 7 | ) 8 | 9 | // Launch gocheck tests 10 | func Test(t *testing.T) { 11 | TestingT(t) 12 | } 13 | 14 | type PointSuite struct { 15 | points PointList 16 | } 17 | 18 | var _ = Suite(&PointSuite{}) 19 | 20 | func (s *PointSuite) SetUpTest(c *C) { 21 | s.points = PointList{Point{30.244759, 59.955982}, Point{30.24472, 59.955975}, Point{30.244358, 59.96698}} 22 | } 23 | 24 | func (s *PointSuite) TestCentroidAndBounds(c *C) { 25 | c1 := Cluster{C: 0, Points: []int{0, 1, 2}} 26 | 27 | center, min, max := c1.CentroidAndBounds(s.points) 28 | c.Check(center, DeepEquals, Point{30.244612333333333, 59.95964566666667}) 29 | c.Check(min, DeepEquals, Point{30.244358, 59.955975}) 30 | c.Check(max, DeepEquals, Point{30.244759, 59.96698}) 31 | } 32 | --------------------------------------------------------------------------------