diff --git a/integration/service/connectivitymap/builder.go b/integration/service/connectivitymap/builder.go new file mode 100644 index 00000000000..90cb6c534d4 --- /dev/null +++ b/integration/service/connectivitymap/builder.go @@ -0,0 +1,156 @@ +package connectivitymap + +import ( + "sync" + + "github.com/Telenav/osrm-backend/integration/service/spatialindexer" + "github.com/golang/glog" +) + +type connectivityMapBuilder struct { + iterator spatialindexer.PointsIterator + finder spatialindexer.Finder + ranker spatialindexer.Ranker + distanceLimit float64 + id2NearbyIDs ID2NearByIDsMap + + numOfWorker int + workerWaitGroup *sync.WaitGroup + aggregatorWaitGroup *sync.WaitGroup + aggregatorC chan placeIDWithNearByPlaceIDs +} + +func newConnectivityMapBuilder(iterator spatialindexer.PointsIterator, finder spatialindexer.Finder, + ranker spatialindexer.Ranker, distanceLimit float64, numOfWorker int) *connectivityMapBuilder { + builder := &connectivityMapBuilder{ + iterator: iterator, + finder: finder, + ranker: ranker, + distanceLimit: distanceLimit, + id2NearbyIDs: make(ID2NearByIDsMap), + + numOfWorker: numOfWorker, + workerWaitGroup: &sync.WaitGroup{}, + aggregatorWaitGroup: &sync.WaitGroup{}, + aggregatorC: make(chan placeIDWithNearByPlaceIDs, 10000), + } + + if numOfWorker < 1 { + glog.Fatal("numOfWorker should never be smaller than 1, recommend using NumCPU()\n") + } + + return builder +} + +/* + -> worker (fetch task -> find -> rank) + / \ + / \ + Input Iterator(channel) ---> worker (fetch task -> find -> rank) ---> aggregatorChannel -> feed to map + \ / + \ / + -> worker (fetch task -> find -> rank) +*/ + +func (builder *connectivityMapBuilder) build() ID2NearByIDsMap { + builder.process() + builder.aggregate() + builder.wait() + + return builder.id2NearbyIDs +} + +func (builder *connectivityMapBuilder) process() { + inputC := builder.iterator.IteratePoints() + + for i := 0; i < builder.numOfWorker; i++ { + builder.workerWaitGroup.Add(1) + go builder.work(i, inputC, builder.aggregatorC) + } + + glog.Infof("builder start number of %d workers.\n", builder.numOfWorker) +} + +func (builder *connectivityMapBuilder) work(workerID int, source <-chan spatialindexer.PointInfo, sink chan<- placeIDWithNearByPlaceIDs) { + defer builder.workerWaitGroup.Done() + + counter := 0 + for p := range source { + counter += 1 + nearbyIDs := builder.finder.FindNearByPointIDs(p.Location, builder.distanceLimit, spatialindexer.UnlimitedCount) + rankedResults := builder.ranker.RankPointIDsByGreatCircleDistance(p.Location, nearbyIDs) + + ids := make([]IDAndDistance, 0, len(rankedResults)) + for _, r := range rankedResults { + ids = append(ids, IDAndDistance{ + ID: r.ID, + Distance: r.Distance, + }) + } + + sink <- placeIDWithNearByPlaceIDs{ + id: p.ID, + ids: ids, + } + } + + glog.Infof("Worker_%d finished handling %d tasks.\n", workerID, counter) +} + +func (builder *connectivityMapBuilder) aggregate() { + builder.aggregatorWaitGroup.Add(1) + + go func() { + counter := 0 + for item := range builder.aggregatorC { + counter += 1 + builder.id2NearbyIDs[item.id] = item.ids + } + + glog.Infof("Aggregation is finished with handling %d items.\n", counter) + builder.aggregatorWaitGroup.Done() + }() +} + +func (builder *connectivityMapBuilder) wait() { + builder.workerWaitGroup.Wait() + close(builder.aggregatorC) + builder.aggregatorWaitGroup.Wait() +} + +type placeIDWithNearByPlaceIDs struct { + id spatialindexer.PointID + ids []IDAndDistance +} + +func (builder *connectivityMapBuilder) buildInSerial() ID2NearByIDsMap { + glog.Warning("This function is only used for compare result of worker's build().\n`") + internalResult := make(chan placeIDWithNearByPlaceIDs, 10000) + m := make(ID2NearByIDsMap) + + go func() { + for p := range builder.iterator.IteratePoints() { + nearbyIDs := builder.finder.FindNearByPointIDs(p.Location, builder.distanceLimit, spatialindexer.UnlimitedCount) + rankedResults := builder.ranker.RankPointIDsByGreatCircleDistance(p.Location, nearbyIDs) + + ids := make([]IDAndDistance, 0, len(rankedResults)) + for _, r := range rankedResults { + ids = append(ids, IDAndDistance{ + ID: r.ID, + Distance: r.Distance, + }) + } + internalResult <- placeIDWithNearByPlaceIDs{ + id: p.ID, + ids: ids, + } + } + close(internalResult) + }() + + for item := range internalResult { + m[item.id] = item.ids + } + + return m +} diff --git a/integration/service/connectivitymap/builder_test.go b/integration/service/connectivitymap/builder_test.go new file mode 100644 index 00000000000..35c55b8619c --- /dev/null +++ b/integration/service/connectivitymap/builder_test.go @@ -0,0 +1,101 @@ +package connectivitymap + +import ( + "reflect" + "runtime" + "testing" + + "github.com/Telenav/osrm-backend/integration/service/spatialindexer" + "github.com/Telenav/osrm-backend/integration/service/spatialindexer/ranker" +) + +// Mock env: +// Iterator returns 100 of fixed location point(IDs are 1000, 1001, 1002, ...) +// Finder returns fixed array result(10 points, IDs are 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ) +// Ranker use great circle distance to calculate +// Expect result: +// build() generate same result as pre-calculated map +// map[1000] = {results of 10 points} +// map[1001] = {results of 10 points} +// ... +// map[1099] = {results of 10 points} +func TestBuilderWithMockIteratorAndFinder(t *testing.T) { + builder := newConnectivityMapBuilder(&spatialindexer.MockOneHundredPointsIterator{}, + &spatialindexer.MockFinder{}, + ranker.CreateRanker(ranker.SimpleRanker, nil), + 100, + runtime.NumCPU()) + + actual := builder.build() + + // construct expect map + expect := make(ID2NearByIDsMap) + + var idAndDistanceArray = []IDAndDistance{ + IDAndDistance{ + ID: 3, + Distance: 345.220003472554, + }, + IDAndDistance{ + ID: 2, + Distance: 402.8536530341791, + }, + IDAndDistance{ + ID: 4, + Distance: 1627.1858848458571, + }, + IDAndDistance{ + ID: 5, + Distance: 4615.586636153461, + }, + IDAndDistance{ + ID: 1, + Distance: 5257.70008125706, + }, + IDAndDistance{ + ID: 6, + Distance: 6888.7486674247, + }, + IDAndDistance{ + ID: 7, + Distance: 7041.893747628621, + }, + IDAndDistance{ + ID: 10, + Distance: 8622.213424347745, + }, + IDAndDistance{ + ID: 9, + Distance: 9438.804320070916, + }, + IDAndDistance{ + ID: 8, + Distance: 9897.44482638937, + }, + } + + for i := 0; i < 100; i++ { + index := i + 1000 + expect[(spatialindexer.PointID(index))] = idAndDistanceArray + } + + if !reflect.DeepEqual(actual, expect) { + t.Errorf("Failed to pass TestBuilder with mock data, \nexpect \n%+v\n but got \n%v\n", expect, actual) + } +} + +func TestBuilderWithSingleWorker(t *testing.T) { + builder := newConnectivityMapBuilder(&spatialindexer.MockOneHundredPointsIterator{}, + &spatialindexer.MockFinder{}, + ranker.CreateRanker(ranker.SimpleRanker, nil), + 100, + runtime.NumCPU()) + + actual := builder.build() + + expect := builder.buildInSerial() + + if !reflect.DeepEqual(actual, expect) { + t.Errorf("Failed to pass TestBuilder with mock data, \nexpect \n%+v\n but got \n%v\n", expect, actual) + } +} diff --git a/integration/service/connectivitymap/connectivity_map.go b/integration/service/connectivitymap/connectivity_map.go new file mode 100644 index 00000000000..02497a77c61 --- /dev/null +++ b/integration/service/connectivitymap/connectivity_map.go @@ -0,0 +1,54 @@ +package connectivitymap + +import ( + "github.com/Telenav/osrm-backend/integration/service/spatialindexer" + "github.com/golang/glog" +) + +// IDAndDistance wraps ID and distance information +type IDAndDistance struct { + ID spatialindexer.PointID + Distance float64 +} + +// ID2NearByIDsMap is a mapping between ID and its nearby IDs +type ID2NearByIDsMap map[spatialindexer.PointID][]IDAndDistance + +// Connectivity Map used to query connectivity for given placeID +type ConnectivityMap struct { + id2nearByIDs ID2NearByIDsMap + maxRange float64 + statistic *statistic +} + +// NewConnectivityMap creates ConnectivityMap +func NewConnectivityMap(maxRange float64) *ConnectivityMap { + return &ConnectivityMap{ + maxRange: maxRange, + statistic: newStatistic(), + } +} + +// Build creates ConnectivityMap +func (cm *ConnectivityMap) Build() { + glog.Info("Successfully finished GenerateConnectivityMap\n") +} + +// Dump dump ConnectivityMap's content to given folderPath +func (cm *ConnectivityMap) Dump(folderPath string) { +} + +// Load rebuild ConnectivityMap from dumpped data in given folderPath +func (cm *ConnectivityMap) Load(folderPath string) { +} + +// QueryConnectivity answers connectivity query for given placeInfo +func (cm *ConnectivityMap) QueryConnectivity(placeInfo spatialindexer.PointInfo, limitDistance float64) { + // for each everything recorded in data, apply limit option on that +} + +// MaxRange tells the value used to pre-process place data. +// MaxRange means the maximum distance in meters could be reached from current location. +func (cm *ConnectivityMap) MaxRange() float64 { + return cm.maxRange +} diff --git a/integration/service/connectivitymap/connectivity_map_mock.go b/integration/service/connectivitymap/connectivity_map_mock.go new file mode 100644 index 00000000000..0bea5c8cb3e --- /dev/null +++ b/integration/service/connectivitymap/connectivity_map_mock.go @@ -0,0 +1,66 @@ +package connectivitymap + +var fakeID2NearByIDsMap1 = ID2NearByIDsMap{ + 1: []IDAndDistance{ + IDAndDistance{ + ID: 2, + Distance: 3, + }, + IDAndDistance{ + ID: 5, + Distance: 4, + }, + IDAndDistance{ + ID: 7, + Distance: 6, + }, + IDAndDistance{ + ID: 8, + Distance: 12, + }, + }, + + 2: []IDAndDistance{ + IDAndDistance{ + ID: 1, + Distance: 3, + }, + IDAndDistance{ + ID: 7, + Distance: 23, + }, + }, + + 5: []IDAndDistance{ + IDAndDistance{ + ID: 1, + Distance: 4, + }, + IDAndDistance{ + ID: 8, + Distance: 5, + }, + }, + + 7: []IDAndDistance{ + IDAndDistance{ + ID: 1, + Distance: 6, + }, + IDAndDistance{ + ID: 2, + Distance: 23, + }, + }, + + 8: []IDAndDistance{ + IDAndDistance{ + ID: 5, + Distance: 5, + }, + IDAndDistance{ + ID: 1, + Distance: 12, + }, + }, +} diff --git a/integration/service/connectivitymap/doc.go b/integration/service/connectivitymap/doc.go new file mode 100644 index 00000000000..ffb4f3a6bb1 --- /dev/null +++ b/integration/service/connectivitymap/doc.go @@ -0,0 +1,32 @@ +/* +Package connectivitymap provides connectivity information for place graphs. + +For example, given following graph + + - - - - 4 +| | | | | + - 2 - - - +| | | | | + - - - 3 - +| | | | | +1 - - - - + +Place connectivity map will pre-process each points and generate following result: +While query connectivity for place 1, will return + (place 2, 3), // the shortest path between place 1 and place 2 is 3 + (place 3, 4), // the shortest path between place 1 and place 2 is 4 + (place 4, 7), // the shortest path between place 1 and place 2 is 7 +The result is sorted by shortest path distance(or other user defined strategy) + +When query for connectivity, user could also pass in limitation option, such as distance limitation. +For example, when query connectivity for place 3 +With limitation = -1, it will return + (place 2, 3), // the shortest path between place 3 and place 2 is 3 + (place 4, 3), // the shortest path between place 3 and place 4 is 3 + (place 1, 4), // the shortest path between place 3 and place 1 is 4 +With limitation = 3, it will return + (place 2, 3), // the shortest path between place 3 and place 2 is 3 + (place 4, 3), // the shortest path between place 3 and place 4 is 3 + +*/ +package connectivitymap diff --git a/integration/service/connectivitymap/dumper.go b/integration/service/connectivitymap/dumper.go new file mode 100644 index 00000000000..d377c21cc6a --- /dev/null +++ b/integration/service/connectivitymap/dumper.go @@ -0,0 +1,118 @@ +package connectivitymap + +import ( + "bytes" + "encoding/gob" + "io/ioutil" + "os" + "strings" + + "github.com/Telenav/osrm-backend/integration/pkg/api" + "github.com/golang/glog" +) + +const id2NearByIDsMapFileName = "id2nearbyidsmap.gob" + +func serializeConnectivityMap(cm *ConnectivityMap, folderPath string) error { + if !strings.HasSuffix(folderPath, api.Slash) { + folderPath += api.Slash + } + + if err := serializeID2NearByIDsMap(cm, folderPath); err != nil { + return err + } + + if err := cm.statistic.dump(folderPath); err != nil { + return err + } + + glog.Infof("Successfully deserialize ConnectivityMap from folder %s.\n", folderPath) + + return nil +} + +func deSerializeConnectivityMap(cm *ConnectivityMap, folderPath string) error { + if !strings.HasSuffix(folderPath, api.Slash) { + folderPath += api.Slash + } + + if err := deSerializeID2NearByIDsMap(cm, folderPath); err != nil { + return err + } + + if err := cm.statistic.load(folderPath); err != nil { + return err + } + cm.maxRange = cm.statistic.MaxRange + + return nil +} + +func removeAllDumpFiles(folderPath string) error { + if !strings.HasSuffix(folderPath, api.Slash) { + folderPath += "/" + } + + _, err := os.Stat(folderPath + id2NearByIDsMapFileName) + if !os.IsNotExist(err) { + err = os.Remove(folderPath + id2NearByIDsMapFileName) + if err != nil { + glog.Errorf("Remove file failed %s\n", folderPath+id2NearByIDsMapFileName) + return err + } + } else { + glog.Warningf("There is no %s file in folder %s\n", id2NearByIDsMapFileName, folderPath) + } + + _, err = os.Stat(folderPath + statisticFileName) + if !os.IsNotExist(err) { + err = os.Remove(folderPath + statisticFileName) + if err != nil { + glog.Errorf("Remove file failed %s\n", folderPath+statisticFileName) + return err + } + } else { + glog.Warningf("There is no %s file in folder %s\n", statisticFileName, folderPath) + } + + return nil +} + +func serializeID2NearByIDsMap(cm *ConnectivityMap, folderPath string) error { + buf := new(bytes.Buffer) + encoder := gob.NewEncoder(buf) + err := encoder.Encode(cm.id2nearByIDs) + + if err != nil { + glog.Errorf("During encode ConnectivityMap's ID2NearByIDsMap met error %v", err) + return err + } + + if err = ioutil.WriteFile(folderPath+id2NearByIDsMapFileName, buf.Bytes(), 0644); err != nil { + glog.Errorf("During dump ConnectivityMap's ID2NearByIDsMap to %s met error %v", folderPath+id2NearByIDsMapFileName, err) + return err + } + + return nil +} + +func deSerializeID2NearByIDsMap(cm *ConnectivityMap, folderPath string) error { + byteArray, err := ioutil.ReadFile(folderPath + id2NearByIDsMapFileName) + if err != nil { + glog.Errorf("During load ConnectivityMap's ID2NearByIDsMap from %s met error %v", + folderPath+id2NearByIDsMapFileName, err) + return err + } + + buf := bytes.NewBuffer(byteArray) + decoder := gob.NewDecoder(buf) + err = decoder.Decode(&cm.id2nearByIDs) + + if err != nil { + glog.Errorf("During decode ConnectivityMap's ID2NearByIDsMap from %s met error %v", + folderPath+id2NearByIDsMapFileName, err) + return err + } + + return nil +} diff --git a/integration/service/connectivitymap/dumper_test.go b/integration/service/connectivitymap/dumper_test.go new file mode 100644 index 00000000000..bce177a0400 --- /dev/null +++ b/integration/service/connectivitymap/dumper_test.go @@ -0,0 +1,60 @@ +package connectivitymap + +import ( + "os" + "reflect" + "testing" + + "github.com/golang/glog" +) + +func TestDumpGivenObjectThenLoadAndThenCompareWithOriginalObject(t *testing.T) { + cases := []ConnectivityMap{ + ConnectivityMap{ + id2nearByIDs: fakeID2NearByIDsMap1, + maxRange: fakeDistanceLimit, + statistic: &fakeStatisticResult1, + }, + } + + // check whether curent folder is writeable + path, _ := os.Getwd() + _, err := os.OpenFile(path+"/tmp", os.O_RDONLY|os.O_CREATE, 0644) + if err != nil { + return + } + if err := os.Remove(path + "/tmp"); err != nil { + glog.Errorf("During dumper test, remove path %s failed.\n", path+"/tmp") + return + } + + if err := removeAllDumpFiles(path); err != nil { + t.Errorf("During running removeAllDumpFiles met error %v", err) + } + for _, c := range cases { + if err := serializeConnectivityMap(&c, path); err != nil { + t.Errorf("During running serializeConnectivityMap for case %v, met error %v", c, err) + } + + actual := NewConnectivityMap(0.0) + if err := deSerializeConnectivityMap(actual, path); err != nil { + t.Errorf("During running deSerializeConnectivityMap for case %v, met error %v", c, err) + } + + if !reflect.DeepEqual(actual.id2nearByIDs, c.id2nearByIDs) { + t.Errorf("Expect result \n%+v but got \n%+v\n", c.id2nearByIDs, actual.id2nearByIDs) + } + + if !reflect.DeepEqual(actual.maxRange, c.maxRange) { + t.Errorf("Expect result \n%+v but got \n%+v\n", c.maxRange, actual.maxRange) + } + + if !reflect.DeepEqual(actual.statistic, c.statistic) { + t.Errorf("Expect result \n%+v but got \n%+v\n", c.statistic, actual.statistic) + } + + if err := removeAllDumpFiles(path); err != nil { + t.Errorf("During running removeAllDumpFiles met error %v", err) + } + } +} diff --git a/integration/service/connectivitymap/statistic.go b/integration/service/connectivitymap/statistic.go new file mode 100644 index 00000000000..aa2a850a6c2 --- /dev/null +++ b/integration/service/connectivitymap/statistic.go @@ -0,0 +1,152 @@ +package connectivitymap + +import ( + "encoding/json" + "io/ioutil" + "math" + "strings" + + "github.com/Telenav/osrm-backend/integration/pkg/api" + "github.com/golang/glog" +) + +const statisticFileName = "connectivity_map_statistic.json" + +type statistic struct { + Count int `json:"count"` + ValidCount int `json:"valid_count"` + AverageNearByIDsCount int `json:"average_nearby_ids_count"` + MaxNearByIDsCount int `json:"max_nearby_ids_count"` + MinNearByIDsCount int `json:"min_nearby_ids_count"` + AverageMaxDistance float64 `json:"average_value_max_distance"` + MaxOfMaxDistance float64 `json:"max_value_max_distance"` + MinOfMaxDistance float64 `json:"min_value_max_distance"` + MaxRange float64 `json:"maxrange_set_by_preprocessing"` +} + +func newStatistic() *statistic { + return &statistic{ + Count: 0, + ValidCount: 0, + AverageNearByIDsCount: 0, + MaxNearByIDsCount: math.MinInt32, + MinNearByIDsCount: math.MaxInt32, + AverageMaxDistance: 0.0, + MaxOfMaxDistance: 0.0, + MinOfMaxDistance: math.MaxFloat64, + MaxRange: 0.0, + } +} + +func (s *statistic) init() { + s.Count = 0 + s.ValidCount = 0 + s.AverageNearByIDsCount = 0 + s.MaxNearByIDsCount = math.MinInt32 + s.MinNearByIDsCount = math.MaxInt32 + s.AverageMaxDistance = 0.0 + s.MaxOfMaxDistance = 0.0 + s.MinOfMaxDistance = math.MaxFloat64 + s.MaxRange = 0.0 +} + +func (s *statistic) build(m ID2NearByIDsMap, MaxRange float64) *statistic { + s.init() + s.MaxRange = MaxRange + + totalNearByIDsCount := 0 + totalMaxDistance := 0.0 + for _, idAndDistanceArray := range m { + s.Count += 1 + if len(idAndDistanceArray) == 0 { + continue + } + s.ValidCount += 1 + + prevTotalNearByIDsCount := totalNearByIDsCount + totalNearByIDsCount += len(idAndDistanceArray) + if prevTotalNearByIDsCount > totalNearByIDsCount { + glog.Fatalf("Overflow during accumulate totalNearByIDsCount, before accumulate value = %v, after add %v new value is %v\n", + prevTotalNearByIDsCount, len(idAndDistanceArray), totalNearByIDsCount) + } + s.MaxNearByIDsCount = max(s.MaxNearByIDsCount, len(idAndDistanceArray)) + s.MinNearByIDsCount = min(s.MinNearByIDsCount, len(idAndDistanceArray)) + + prevTotalMaxDistance := totalMaxDistance + + maxDistance := 0.0 + for _, item := range idAndDistanceArray { + maxDistance = math.Max(maxDistance, item.Distance) + } + + totalMaxDistance += maxDistance + if prevTotalMaxDistance > totalMaxDistance { + glog.Fatalf("Overflow during accumulate totalMaxDistance, before accumulate value = %#v, after add %v new value is %#v\n", + prevTotalMaxDistance, maxDistance, totalMaxDistance) + } + s.MaxOfMaxDistance = math.Max(s.MaxOfMaxDistance, maxDistance) + s.MinOfMaxDistance = math.Min(s.MinOfMaxDistance, maxDistance) + } + + s.AverageNearByIDsCount = totalNearByIDsCount / s.ValidCount + s.AverageMaxDistance = totalMaxDistance / (float64)(s.ValidCount) + + glog.Infof("Build statistic for ID2NearByIDsMap finished. %+v\n", s) + return s +} + +func (s *statistic) dump(folderPath string) error { + if !strings.HasSuffix(folderPath, api.Slash) { + folderPath += api.Slash + } + + file, err := json.Marshal(*s) + if err != nil { + glog.Errorf("Marshal object %+v failed with error %+v\n", s, err) + return err + } + + if err := ioutil.WriteFile(folderPath+statisticFileName, file, 0644); err != nil { + glog.Errorf("Dump %s to %s failed with error %+v\n", statisticFileName, folderPath, err) + return err + } + + glog.Infof("Successfully dump statistic file of %s to %s\n", statisticFileName, folderPath) + return nil +} + +func (s *statistic) load(folderPath string) error { + if !strings.HasSuffix(folderPath, api.Slash) { + folderPath += api.Slash + } + + file, err := ioutil.ReadFile(folderPath + statisticFileName) + if err != nil { + glog.Errorf("Load %s from %s failed with error %+v\n", statisticFileName, folderPath, err) + return err + } + + err = json.Unmarshal([]byte(file), s) + if err != nil { + glog.Errorf("Unmarshal statistic file %s from %s failed with error %+v\n", statisticFileName, folderPath, err) + return err + } + + return nil +} + +// max returns the larger of x or y. +func max(x, y int) int { + if x < y { + return y + } + return x +} + +// min returns the smaller of x or y. +func min(x, y int) int { + if x > y { + return y + } + return x +} diff --git a/integration/service/connectivitymap/statistic_test.go b/integration/service/connectivitymap/statistic_test.go new file mode 100644 index 00000000000..957d1d966f2 --- /dev/null +++ b/integration/service/connectivitymap/statistic_test.go @@ -0,0 +1,41 @@ +package connectivitymap + +import ( + "reflect" + "testing" +) + +var fakeDistanceLimit float64 = 123 +var fakeStatisticResult1 = statistic{ + Count: 5, + ValidCount: 5, + AverageNearByIDsCount: 2, + MaxNearByIDsCount: 4, + MinNearByIDsCount: 2, + AverageMaxDistance: 15, + MaxOfMaxDistance: 23, + MinOfMaxDistance: 5, + MaxRange: 123, +} + +func TestStatisticBuild(t *testing.T) { + cases := []struct { + id2NearByIDsMap ID2NearByIDsMap + distanceLimit float64 + expect *statistic + }{ + { + id2NearByIDsMap: fakeID2NearByIDsMap1, + distanceLimit: fakeDistanceLimit, + expect: &fakeStatisticResult1, + }, + } + + for _, c := range cases { + actual := newStatistic().build(c.id2NearByIDsMap, c.distanceLimit) + if !reflect.DeepEqual(actual, c.expect) { + t.Errorf("Incorrect statistic build() result, expect \n%+v \nbut got \n%+v\n", c.expect, actual) + } + } + +} diff --git a/integration/service/spatialindexer/interface.go b/integration/service/spatialindexer/interface.go index 1f4b885c9e7..9ca5c11cec6 100644 --- a/integration/service/spatialindexer/interface.go +++ b/integration/service/spatialindexer/interface.go @@ -32,7 +32,7 @@ const UnlimitedCount = math.MaxInt32 type Finder interface { // FindNearByPointIDs returns a group of points near to given center location - FindNearByPointIDs(center Location, radius float64, limitCount int) []PointInfo + FindNearByPointIDs(center Location, radius float64, limitCount int) []*PointInfo } // Ranker used to ranking a group of points diff --git a/integration/service/spatialindexer/interface_mock.go b/integration/service/spatialindexer/interface_mock.go new file mode 100644 index 00000000000..654edda288c --- /dev/null +++ b/integration/service/spatialindexer/interface_mock.go @@ -0,0 +1,130 @@ +package spatialindexer + +var mockPlaceInfo1 = []*PointInfo{ + &PointInfo{ + ID: 1, + Location: Location{ + Lat: 37.355204, + Lon: -121.953901, + }, + }, + &PointInfo{ + ID: 2, + Location: Location{ + Lat: 37.399331, + Lon: -121.981193, + }, + }, + &PointInfo{ + ID: 3, + Location: Location{ + Lat: 37.401948, + Lon: -121.977384, + }, + }, + &PointInfo{ + ID: 4, + Location: Location{ + Lat: 37.407082, + Lon: -121.991937, + }, + }, + &PointInfo{ + ID: 5, + Location: Location{ + Lat: 37.407277, + Lon: -121.925482, + }, + }, + &PointInfo{ + ID: 6, + Location: Location{ + Lat: 37.375024, + Lon: -121.904706, + }, + }, + &PointInfo{ + ID: 7, + Location: Location{ + Lat: 37.359592, + Lon: -121.914164, + }, + }, + &PointInfo{ + ID: 8, + Location: Location{ + Lat: 37.366023, + Lon: -122.080777, + }, + }, + &PointInfo{ + ID: 9, + Location: Location{ + Lat: 37.368453, + Lon: -122.076400, + }, + }, + &PointInfo{ + ID: 10, + Location: Location{ + Lat: 37.373546, + Lon: -122.068904, + }, + }, +} + +// MockPointsIterator implements Finder's interface +type MockFinder struct { +} + +// FindNearByPointIDs returns mock result +// It returns 10 places defined in mockPlaceInfo1 +func (finder *MockFinder) FindNearByPointIDs(center Location, radius float64, limitCount int) []*PointInfo { + return mockPlaceInfo1 +} + +// MockPointsIterator implements PointsIterator's interface +type MockPointsIterator struct { +} + +// IteratePoints() iterate places with mock data +func (iterator *MockPointsIterator) IteratePoints() <-chan PointInfo { + pointInfoC := make(chan PointInfo, len(mockPlaceInfo1)) + + go func() { + for _, item := range mockPlaceInfo1 { + pointInfoC <- *item + } + + close(pointInfoC) + }() + + return pointInfoC +} + +// MockOneHundredPointsIterator implements PointsIterator's interface +type MockOneHundredPointsIterator struct { +} + +// IteratePoints() iterate places with mock data. +// It returns {ID:1000, fixed location}, {ID:1001, fixed location}, ... {ID:1099, fixed location} +func (iterator *MockOneHundredPointsIterator) IteratePoints() <-chan PointInfo { + pointInfoC := make(chan PointInfo, 100) + + go func() { + for i := 0; i < 100; i++ { + id := (PointID)(i + 1000) + pointInfoC <- PointInfo{ + ID: id, + Location: Location{ + Lat: 37.398896, + Lon: -121.976665, + }, + } + } + + close(pointInfoC) + }() + + return pointInfoC +} diff --git a/integration/service/spatialindexer/ranker/ranker_factory.go b/integration/service/spatialindexer/ranker/ranker_factory.go index a6bd0776975..4ae51344b98 100644 --- a/integration/service/spatialindexer/ranker/ranker_factory.go +++ b/integration/service/spatialindexer/ranker/ranker_factory.go @@ -6,10 +6,13 @@ import ( ) const ( - SimpleRanker = "SimpleRanker" + // SimpleRanker implements Raner's interface based on great circle distance + SimpleRanker = "SimpleRanker" + // OSRMBasedRanker implements Raner's interface based on OSRM OSRMBasedRanker = "OSRMBasedRanker" ) +// CreateRanker creates implementations of interface Ranker func CreateRanker(rankerType string, oc *osrmconnector.OSRMConnector) spatialindexer.Ranker { switch rankerType { case SimpleRanker: diff --git a/integration/service/spatialindexer/s2indexer/indexer.go b/integration/service/spatialindexer/s2indexer/indexer.go index 2abee9646b6..24ea20ddaea 100644 --- a/integration/service/spatialindexer/s2indexer/indexer.go +++ b/integration/service/spatialindexer/s2indexer/indexer.go @@ -80,8 +80,9 @@ func (indexer *S2Indexer) IteratePoints() <-chan spatialindexer.PointInfo { return pointsC } -func (indexer *S2Indexer) FindNearByPointIDs(center spatialindexer.Location, radius float64, limitCount int) []spatialindexer.PointInfo { - if !indexer.isInit() { +// FindNearByPointIDs returns nearby points for given center and conditions +func (indexer *S2Indexer) FindNearByPointIDs(center spatialindexer.Location, radius float64, limitCount int) []*spatialindexer.PointInfo { + if !indexer.isInitialized() { glog.Warning("S2Indexer is empty, try to Build() with correct input file first.\n") return nil } @@ -104,7 +105,7 @@ func (indexer S2Indexer) getPointIDsByS2CellID(cellid s2.CellID) ([]spatialindex return pointIDs, ok } -func (indexer S2Indexer) isInit() bool { +func (indexer S2Indexer) isInitialized() bool { return len(indexer.cellID2PointIDs) != 0 && len(indexer.pointID2Location) != 0 } diff --git a/integration/service/spatialindexer/s2indexer/spatial_query.go b/integration/service/spatialindexer/s2indexer/spatial_query.go index d64b997d284..9c5c1638dfd 100644 --- a/integration/service/spatialindexer/s2indexer/spatial_query.go +++ b/integration/service/spatialindexer/s2indexer/spatial_query.go @@ -24,8 +24,8 @@ func queryNearByS2Cells(point spatialindexer.Location, radiusInMeters float64) [ return ([]s2.CellID)(cellUnion) } -func queryNearByPoints(indexer *S2Indexer, point spatialindexer.Location, radius float64) []spatialindexer.PointInfo { - var result []spatialindexer.PointInfo +func queryNearByPoints(indexer *S2Indexer, point spatialindexer.Location, radius float64) []*spatialindexer.PointInfo { + var result []*spatialindexer.PointInfo cellIDs := queryNearByS2Cells(point, radius) @@ -42,7 +42,7 @@ func queryNearByPoints(indexer *S2Indexer, point spatialindexer.Location, radius continue } - result = append(result, spatialindexer.PointInfo{ + result = append(result, &spatialindexer.PointInfo{ ID: pointID, Location: location, }) diff --git a/integration/service/spatialindexer/s2indexer/spatial_query_test.go b/integration/service/spatialindexer/s2indexer/spatial_query_test.go index c444a8f0d8a..191fa503b9f 100644 --- a/integration/service/spatialindexer/s2indexer/spatial_query_test.go +++ b/integration/service/spatialindexer/s2indexer/spatial_query_test.go @@ -74,15 +74,15 @@ func TestSpatialIndexQuery1(t *testing.T) { Lon: -121.969861, } - expect := []spatialindexer.PointInfo{ - spatialindexer.PointInfo{ + expect := []*spatialindexer.PointInfo{ + &spatialindexer.PointInfo{ ID: 1, Location: spatialindexer.Location{ Lat: 37.402701, Lon: -121.974096, }, }, - spatialindexer.PointInfo{ + &spatialindexer.PointInfo{ ID: 2, Location: spatialindexer.Location{ Lat: 37.40353,