From a4c42e7b0762429036e538cf52cb36d97cd0fe86 Mon Sep 17 00:00:00 2001 From: codebear801 Date: Mon, 4 May 2020 16:39:29 -0700 Subject: [PATCH] feat: implement station connectivity graph based on pre-build data issue: https://github.com/Telenav/osrm-backend/issues/242 --- .../oasis/connectivitymap/connectivity_map.go | 10 +- .../connectivitymap/connectivity_map_mock.go | 20 ++ .../service/oasis/spatialindexer/interface.go | 18 +- .../oasis/spatialindexer/s2indexer/indexer.go | 21 ++ .../service/oasis/stationconnquerier/doc.go | 9 + .../station_conn_querier.go | 151 +++++++++++ .../station_conn_querier_test.go | 239 ++++++++++++++++++ 7 files changed, 464 insertions(+), 4 deletions(-) create mode 100644 integration/service/oasis/stationconnquerier/doc.go create mode 100644 integration/service/oasis/stationconnquerier/station_conn_querier.go create mode 100644 integration/service/oasis/stationconnquerier/station_conn_querier_test.go diff --git a/integration/service/oasis/connectivitymap/connectivity_map.go b/integration/service/oasis/connectivitymap/connectivity_map.go index 8e2d0d37cde..6c10257ed12 100644 --- a/integration/service/oasis/connectivitymap/connectivity_map.go +++ b/integration/service/oasis/connectivitymap/connectivity_map.go @@ -68,9 +68,13 @@ func (cm *ConnectivityMap) Load(folderPath string) *ConnectivityMap { return cm } -// 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 +// QueryConnectivity answers connectivity query for given placeID +// Return true and IDAndDistance array for given placeID, otherwise false and nil +func (cm *ConnectivityMap) QueryConnectivity(placeID spatialindexer.PointID) ([]IDAndDistance, bool) { + if result, ok := cm.id2nearByIDs[placeID]; ok { + return result, true + } + return nil, false } // MaxRange tells the value used to pre-process place data. diff --git a/integration/service/oasis/connectivitymap/connectivity_map_mock.go b/integration/service/oasis/connectivitymap/connectivity_map_mock.go index 0bea5c8cb3e..5d01ada8092 100644 --- a/integration/service/oasis/connectivitymap/connectivity_map_mock.go +++ b/integration/service/oasis/connectivitymap/connectivity_map_mock.go @@ -64,3 +64,23 @@ var fakeID2NearByIDsMap1 = ID2NearByIDsMap{ }, }, } + +var fakeID2NearByIDsMap2 = ID2NearByIDsMap{ + 1: []IDAndDistance{ + { + ID: 2, + Distance: 1, + }, + }, + 2: []IDAndDistance{ + { + ID: 3, + Distance: 2, + }, + }, +} + +// MockConnectivityMap constructs simple connectivity map for integration test +var MockConnectivityMap = ConnectivityMap{ + id2nearByIDs: fakeID2NearByIDsMap2, +} diff --git a/integration/service/oasis/spatialindexer/interface.go b/integration/service/oasis/spatialindexer/interface.go index 97e80fc057b..d587762a3d5 100644 --- a/integration/service/oasis/spatialindexer/interface.go +++ b/integration/service/oasis/spatialindexer/interface.go @@ -2,10 +2,13 @@ package spatialindexer import ( "math" + "strconv" + + "github.com/Telenav/osrm-backend/integration/api/nav" ) // Location for poi point -// @todo: will be replaced by the one in map +// todo codebear801 will be replaced by the one in nav type Location struct { Lat float64 Lon float64 @@ -27,6 +30,11 @@ type RankedPointInfo struct { // Only the data used for pre-processing contains valid PointID type PointID int64 +// String converts PointID to string +func (p PointID) String() string { + return strconv.FormatInt((int64)(p), 10) +} + // UnlimitedCount means all spatial search result will be returned const UnlimitedCount = math.MaxInt32 @@ -47,6 +55,14 @@ type Ranker interface { RankPointIDsByShortestDistance(center Location, targets []*PointInfo) []*RankedPointInfo } +// PlaceLocationQuerier returns *nav.location for given location +type PlaceLocationQuerier interface { + + // GetLocation returns *nav.Location for given placeID + // Returns nil if given placeID is not found + GetLocation(placeID string) *nav.Location +} + // PointsIterator provides iterateability for PointInfo type PointsIterator interface { diff --git a/integration/service/oasis/spatialindexer/s2indexer/indexer.go b/integration/service/oasis/spatialindexer/s2indexer/indexer.go index a3e3563d275..893f85c6b9a 100644 --- a/integration/service/oasis/spatialindexer/s2indexer/indexer.go +++ b/integration/service/oasis/spatialindexer/s2indexer/indexer.go @@ -1,8 +1,10 @@ package s2indexer import ( + "strconv" "time" + "github.com/Telenav/osrm-backend/integration/api/nav" "github.com/Telenav/osrm-backend/integration/service/oasis/spatialindexer" "github.com/Telenav/osrm-backend/integration/service/oasis/spatialindexer/poiloader" "github.com/golang/geo/s2" @@ -118,6 +120,25 @@ func (indexer *S2Indexer) FindNearByPointIDs(center spatialindexer.Location, rad return results } +// GetLocation returns *nav.Location for given placeID +// Returns nil if given placeID is not found +func (indexer *S2Indexer) GetLocation(placeID string) *nav.Location { + id, err := strconv.Atoi(placeID) + if err != nil { + glog.Errorf("Incorrect station ID passed to NearByStationQuery %+v, got error %#v", placeID, err) + return nil + } + if location, ok := indexer.pointID2Location[(spatialindexer.PointID)(id)]; ok { + return &nav.Location{ + Lat: location.Lat, + Lon: location.Lon, + } + } + + return nil +} + +//TODO codebear801 This function should be replaced by GetLocation func (indexer S2Indexer) getPointLocationByPointID(id spatialindexer.PointID) (spatialindexer.Location, bool) { location, ok := indexer.pointID2Location[id] return location, ok diff --git a/integration/service/oasis/stationconnquerier/doc.go b/integration/service/oasis/stationconnquerier/doc.go new file mode 100644 index 00000000000..2d7f60b8dcf --- /dev/null +++ b/integration/service/oasis/stationconnquerier/doc.go @@ -0,0 +1,9 @@ +/* +Package stationconnquerier provides connectivitymap.Querier interface based on pre-build connectivity data. +It needs to connect orig and destination point into station graph. + +For orig/start point, based on electric vehicle's current energy level, it queries all possible reachable stations take start point as center. +For destination/end point, based on electric vehicle's max energy level, it queries all possible stations which could reach destination with maximum amount of charge. +For charge stations, it retrieves connectivity from pre-build data. If a charge station is reachable to destination/end point, it must connects that into graph. +*/ +package stationconnquerier diff --git a/integration/service/oasis/stationconnquerier/station_conn_querier.go b/integration/service/oasis/stationconnquerier/station_conn_querier.go new file mode 100644 index 00000000000..d3af5aee011 --- /dev/null +++ b/integration/service/oasis/stationconnquerier/station_conn_querier.go @@ -0,0 +1,151 @@ +package stationconnquerier + +import ( + "strconv" + + "github.com/Telenav/osrm-backend/integration/api/nav" + "github.com/Telenav/osrm-backend/integration/service/oasis/connectivitymap" + "github.com/Telenav/osrm-backend/integration/service/oasis/spatialindexer" + "github.com/Telenav/osrm-backend/integration/service/oasis/stationfinder/stationfindertype" + "github.com/golang/glog" +) + +type StationConnectivityQuerier struct { + stationLocationQuerier spatialindexer.PlaceLocationQuerier + stationConnectivity *connectivitymap.ConnectivityMap + reachableStationsByStart []*connectivitymap.QueryResult + reachableStationToEnd map[string]*connectivitymap.QueryResult + startLocation *nav.Location + endLocation *nav.Location +} + +func New(stationFinder spatialindexer.Finder, stationRanker spatialindexer.Ranker, + stationLocationQuerier spatialindexer.PlaceLocationQuerier, + stationConnectivity *connectivitymap.ConnectivityMap, + start, end *nav.Location, + currEnergyLevel, maxEnergyLevel float64) connectivitymap.Querier { + + querier := &StationConnectivityQuerier{ + stationLocationQuerier: stationLocationQuerier, + stationConnectivity: stationConnectivity, + startLocation: start, + endLocation: end, + } + querier.connectStartIntoStationGraph(stationFinder, stationRanker, start, currEnergyLevel) + querier.connectEndIntoStationGraph(stationFinder, stationRanker, end, maxEnergyLevel) + + return querier +} + +func (querier *StationConnectivityQuerier) connectStartIntoStationGraph(stationFinder spatialindexer.Finder, stationRanker spatialindexer.Ranker, + start *nav.Location, currEnergyLevel float64) { + center := spatialindexer.Location{Lat: start.Lat, Lon: start.Lon} + nearByPoints := stationFinder.FindNearByPointIDs(center, currEnergyLevel, spatialindexer.UnlimitedCount) + rankedPoints := stationRanker.RankPointIDsByShortestDistance(center, nearByPoints) + + reachableStationsByStart := make([]*connectivitymap.QueryResult, 0, len(rankedPoints)) + for _, rankedPointInfo := range rankedPoints { + tmp := &connectivitymap.QueryResult{ + StationID: rankedPointInfo.ID.String(), + StationLocation: &nav.Location{Lat: rankedPointInfo.Location.Lat, Lon: rankedPointInfo.Location.Lon}, + Distance: rankedPointInfo.Distance, + // TODO codebear801 Replace with pre-calculate duration https://github.com/Telenav/osrm-backend/issues/321 + Duration: rankedPointInfo.Distance, + } + reachableStationsByStart = append(reachableStationsByStart, tmp) + } + + querier.reachableStationsByStart = reachableStationsByStart +} + +func (querier *StationConnectivityQuerier) connectEndIntoStationGraph(stationFinder spatialindexer.Finder, stationRanker spatialindexer.Ranker, + end *nav.Location, maxEnergyLevel float64) { + center := spatialindexer.Location{Lat: end.Lat, Lon: end.Lon} + nearByPoints := stationFinder.FindNearByPointIDs(center, maxEnergyLevel, spatialindexer.UnlimitedCount) + rankedPoints := stationRanker.RankPointIDsByShortestDistance(center, nearByPoints) + + reachableStationToEnd := make(map[string]*connectivitymap.QueryResult) + for _, rankedPointInfo := range rankedPoints { + reachableStationToEnd[rankedPointInfo.ID.String()] = &connectivitymap.QueryResult{ + StationID: stationfindertype.DestLocationID, + StationLocation: end, + Distance: rankedPointInfo.Distance, + //TODO codebear801 https://github.com/Telenav/osrm-backend/issues/321 + Duration: rankedPointInfo.Distance, + } + } + + querier.reachableStationToEnd = reachableStationToEnd +} + +// NearByStationQuery finds near by stations by given stationID and return them in recorded sequence +// Returns nil if given stationID is not found or no connectivity +func (querier *StationConnectivityQuerier) NearByStationQuery(stationID string) []*connectivitymap.QueryResult { + + if stationID == stationfindertype.OrigLocationID { + return querier.reachableStationsByStart + } + + if stationID == stationfindertype.DestLocationID { + return nil + } + + placeID, err := strconv.Atoi(stationID) + if err != nil { + glog.Errorf("Incorrect station ID passed to NearByStationQuery %+v, got error %#v", stationID, err) + return nil + } + if connectivityResults, ok := querier.stationConnectivity.QueryConnectivity((spatialindexer.PointID)(placeID)); ok { + size := len(connectivityResults) + if querier.isStationConnectsToEnd(stationID) { + size += 1 + } + + results := make([]*connectivitymap.QueryResult, 0, size) + for _, idAndWeight := range connectivityResults { + tmp := &connectivitymap.QueryResult{ + StationID: idAndWeight.ID.String(), + StationLocation: querier.GetLocation(idAndWeight.ID.String()), + Distance: idAndWeight.Distance, + //TODO codebear801 https://github.com/Telenav/osrm-backend/issues/321 + Duration: idAndWeight.Distance, + } + results = append(results, tmp) + } + + return querier.connectEndIntoGraph(stationID, results) + } else { + if querier.isStationConnectsToEnd(stationID) { + results := make([]*connectivitymap.QueryResult, 0, 1) + return querier.connectEndIntoGraph(stationID, results) + } + } + + return nil +} + +// GetLocation returns location of given station id +// Returns nil if given stationID is not found +func (querier *StationConnectivityQuerier) GetLocation(stationID string) *nav.Location { + switch stationID { + case stationfindertype.OrigLocationID: + return querier.startLocation + case stationfindertype.DestLocationID: + return querier.endLocation + default: + return querier.stationLocationQuerier.GetLocation(stationID) + } +} + +func (querier *StationConnectivityQuerier) isStationConnectsToEnd(stationID string) bool { + _, ok := querier.reachableStationToEnd[stationID] + return ok +} + +func (querier *StationConnectivityQuerier) connectEndIntoGraph(stationID string, results []*connectivitymap.QueryResult) []*connectivitymap.QueryResult { + if queryResult4End, ok := querier.reachableStationToEnd[stationID]; ok { + results = append(results, queryResult4End) + } + + return results +} diff --git a/integration/service/oasis/stationconnquerier/station_conn_querier_test.go b/integration/service/oasis/stationconnquerier/station_conn_querier_test.go new file mode 100644 index 00000000000..bf7b7552088 --- /dev/null +++ b/integration/service/oasis/stationconnquerier/station_conn_querier_test.go @@ -0,0 +1,239 @@ +package stationconnquerier + +import ( + "reflect" + "testing" + + "github.com/Telenav/osrm-backend/integration/api/nav" + "github.com/Telenav/osrm-backend/integration/service/oasis/connectivitymap" + "github.com/Telenav/osrm-backend/integration/service/oasis/spatialindexer" + "github.com/Telenav/osrm-backend/integration/service/oasis/spatialindexer/ranker" + "github.com/Telenav/osrm-backend/integration/service/oasis/stationfinder/stationfindertype" +) + +/* +Construct graph as follows + + station_1 + / | \ + / | \ + orig --- station_2 --- dest + \ | / + \ | / + station_3 + +Expects for connectivity: +orig: station_1, station_2, station_3 +station_1: station_2, dest +station_2: station_3, dest +station_3: dest +dest: nil +*/ +func TestStationConnQuerier(t *testing.T) { + querier := New( + &mockFinder{}, + ranker.CreateRanker(ranker.SimpleRanker, nil), + &mockPlaceLocationQuerier{}, + &connectivitymap.MockConnectivityMap, + mockOrigLocation, + mockDestLocation, + 10, + 30, + ) + + // verify location + locationCases := []struct { + queryStr string + expectLocation *nav.Location + }{ + { + stationfindertype.OrigLocationID, + mockOrigLocation, + }, + { + stationfindertype.DestLocationID, + mockDestLocation, + }, + { + "1", + mockStation1Location, + }, + { + "2", + mockStation2Location, + }, + { + "3", + mockStation3Location, + }, + { + "incorrect_station_id", + nil, + }, + } + + for _, c := range locationCases { + actualLocation := querier.GetLocation(c.queryStr) + if !reflect.DeepEqual(actualLocation, c.expectLocation) { + t.Errorf("Incorrect result for connectivitymap.Querier.GetLocation, expect %+v but got %+v\n", c.expectLocation, actualLocation) + } + } + + // verify connectivity + connectivityCases := []struct { + stationID string + expectQueryResult []*connectivitymap.QueryResult + }{ + { + stationfindertype.OrigLocationID, + []*connectivitymap.QueryResult{ + { + StationID: "3", + StationLocation: mockStation3Location, + Distance: 4622.08948420977, + Duration: 4622.08948420977, + }, + { + StationID: "2", + StationLocation: mockStation2Location, + Distance: 4999.134247893073, + Duration: 4999.134247893073, + }, + { + StationID: "1", + StationLocation: mockStation1Location, + Distance: 6310.598332634715, + Duration: 6310.598332634715, + }, + }, + }, + { + stationfindertype.DestLocationID, + nil, + }, + { + "1", + []*connectivitymap.QueryResult{ + { + StationID: "2", + StationLocation: mockStation2Location, + Distance: 1, + Duration: 1, + }, + { + StationID: stationfindertype.DestLocationID, + StationLocation: mockDestLocation, + Distance: 4873.817197753869, + Duration: 4873.817197753869, + }, + }, + }, + { + "2", + []*connectivitymap.QueryResult{ + { + StationID: "3", + StationLocation: mockStation3Location, + Distance: 2, + Duration: 2, + }, + { + StationID: stationfindertype.DestLocationID, + StationLocation: mockDestLocation, + Distance: 7277.313067724465, + Duration: 7277.313067724465, + }, + }, + }, + { + "3", + []*connectivitymap.QueryResult{ + { + StationID: stationfindertype.DestLocationID, + StationLocation: mockDestLocation, + Distance: 7083.8672907090095, + Duration: 7083.8672907090095, + }, + }, + }, + } + + for _, c := range connectivityCases { + actualQueryResult := querier.NearByStationQuery(c.stationID) + if !reflect.DeepEqual(actualQueryResult, c.expectQueryResult) { + t.Errorf("Incorrect result for connectivitymap.Querier.NearByStationQuery, expect %+v but got %+v\n", c.expectQueryResult, actualQueryResult) + } + } +} + +var mockPlaceInfo = []*spatialindexer.PointInfo{ + { + ID: 1, + Location: spatialindexer.Location{ + Lat: 37.355204, + Lon: -121.953901, + }, + }, + { + ID: 2, + Location: spatialindexer.Location{ + Lat: 37.399331, + Lon: -121.981193, + }, + }, + { + ID: 3, + Location: spatialindexer.Location{ + Lat: 37.401948, + Lon: -121.977384, + }, + }, +} + +type mockFinder struct { +} + +// FindNearByPointIDs returns mock result +func (finder *mockFinder) FindNearByPointIDs(center spatialindexer.Location, radius float64, limitCount int) []*spatialindexer.PointInfo { + return mockPlaceInfo +} + +type mockPlaceLocationQuerier struct { +} + +var mockOrigLocation = &nav.Location{ + Lat: 37.407277, + Lon: -121.925482, +} + +var mockDestLocation = &nav.Location{ + Lat: 37.375024, + Lon: -121.904706, +} + +var mockStation1Location = &nav.Location{ + Lat: 37.355204, + Lon: -121.953901, +} + +var mockStation2Location = &nav.Location{ + Lat: 37.399331, + Lon: -121.981193, +} + +var mockStation3Location = &nav.Location{ + Lat: 37.401948, + Lon: -121.977384, +} + +func (querier *mockPlaceLocationQuerier) GetLocation(placeID string) *nav.Location { + switch placeID { + case "1": + return mockStation1Location + case "2": + return mockStation2Location + case "3": + return mockStation3Location + } + return nil +}