diff --git a/integration/cmd/oasis/flags.go b/integration/cmd/oasis/flags.go index 3a16c804d92..525facab70f 100644 --- a/integration/cmd/oasis/flags.go +++ b/integration/cmd/oasis/flags.go @@ -5,11 +5,17 @@ import ( ) var flags struct { - listenPort int - osrmBackendEndpoint string + listenPort int + osrmBackendEndpoint string + tnSearchEndpoint string + tnSearchAPIKey string + tnSearchAPISignature string } func init() { flag.IntVar(&flags.listenPort, "p", 8090, "Listen port.") - flag.StringVar(&flags.osrmBackendEndpoint, "osrm", "", "Backend OSRM-backend endpoint") + flag.StringVar(&flags.osrmBackendEndpoint, "osrm", "", "OSRM-backend endpoint") + flag.StringVar(&flags.tnSearchEndpoint, "search", "", "TN-Search-backend endpoint") + flag.StringVar(&flags.tnSearchAPIKey, "searchApiKey", "", "API key for TN-Search-backend") + flag.StringVar(&flags.tnSearchAPISignature, "searchApiSignature", "", "API Signature for TN-Search-backend") } diff --git a/integration/cmd/oasis/main.go b/integration/cmd/oasis/main.go index 1e27eec6351..abae4fa057a 100644 --- a/integration/cmd/oasis/main.go +++ b/integration/cmd/oasis/main.go @@ -14,7 +14,7 @@ func main() { defer glog.Flush() mux := http.NewServeMux() - oasisService := oasis.New(flags.osrmBackendEndpoint) + oasisService := oasis.New(flags.osrmBackendEndpoint, flags.tnSearchEndpoint, flags.tnSearchAPIKey, flags.tnSearchAPISignature) mux.Handle("/oasis/v1/earliest/", oasisService) // listen diff --git a/integration/cmd/search-adhoc-request/main.go b/integration/cmd/search-adhoc-request/main.go index e8a802eec41..41e3d7a6b42 100644 --- a/integration/cmd/search-adhoc-request/main.go +++ b/integration/cmd/search-adhoc-request/main.go @@ -6,8 +6,8 @@ import ( "fmt" "net/http" - "github.com/Telenav/osrm-backend/integration/pkg/api/search/coordinate" "github.com/Telenav/osrm-backend/integration/pkg/api/search/nearbychargestation" + "github.com/Telenav/osrm-backend/integration/pkg/api/search/searchcoordinate" "github.com/Telenav/osrm-backend/integration/pkg/backend" ) @@ -30,7 +30,7 @@ func main() { req := nearbychargestation.NewRequest() req.APIKey = flags.apiKey req.APISignature = flags.apiSignature - req.Location = coordinate.Coordinate{Lat: 37.78509, Lon: -122.41988} + req.Location = searchcoordinate.Coordinate{Lat: 37.78509, Lon: -122.41988} clt := http.Client{Timeout: backend.Timeout()} requestURL := flags.entityEndpoint + req.RequestURI() diff --git a/integration/oasis/handler.go b/integration/oasis/handler.go index ed531cebea7..865e0718e74 100644 --- a/integration/oasis/handler.go +++ b/integration/oasis/handler.go @@ -6,6 +6,7 @@ import ( "net/http" "github.com/Telenav/osrm-backend/integration/oasis/osrmconnector" + "github.com/Telenav/osrm-backend/integration/oasis/searchconnector" "github.com/Telenav/osrm-backend/integration/pkg/api/oasis" "github.com/Telenav/osrm-backend/integration/pkg/api/osrm/route" "github.com/Telenav/osrm-backend/integration/pkg/api/search/nearbychargestation" @@ -14,13 +15,16 @@ import ( // Handler handles oasis request and provide response type Handler struct { - osrmConnector *osrmconnector.OSRMConnector + osrmConnector *osrmconnector.OSRMConnector + tnSearchConnector *searchconnector.TNSearchConnector } // New creates new Handler object -func New(osrmBackend string) *Handler { +func New(osrmBackend, searchEndpoint, apiKey, apiSignature string) *Handler { + // @todo: need make sure connectivity is on and continues available return &Handler{ - osrmConnector: osrmconnector.NewOSRMConnector(osrmBackend), + osrmConnector: osrmconnector.NewOSRMConnector(osrmBackend), + tnSearchConnector: searchconnector.NewTNSearchConnector(searchEndpoint, apiKey, apiSignature), } } @@ -36,7 +40,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - // check whether has enough energy route, err := h.requestRoute4InputOrigDest(oasisReq) if err != nil { glog.Error(err) @@ -45,6 +48,16 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } + // check whether orig and dest is reachable + if len(route.Routes) == 0 { + info := "Orig and destination is not reachable for request " + oasisReq.RequestURI() + "." + glog.Info(info) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, info) + return + } + + // check whether has enough energy b, remainRange, err := hasEnoughEnergy(oasisReq.CurrRange, oasisReq.SafeLevel, route) if err != nil { glog.Error(err) @@ -55,9 +68,17 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { if b { h.generateOASISResponse(w, route, remainRange) - } else { - generateFakeOASISResponse(w, oasisReq) + return } + + // check whether could achieve by single charge + overlap := getOverlapChargeStations4OrigDest(oasisReq, route.Routes[0].Distance, h.osrmConnector, h.tnSearchConnector) + if len(overlap) > 0 { + generateResponse4SingleChargeStation(w, oasisReq, overlap, h.osrmConnector) + return + } + + generateFakeOASISResponse(w, oasisReq) } func (h *Handler) requestRoute4InputOrigDest(oasisReq *oasis.Request) (*route.Response, error) { diff --git a/integration/oasis/has_enough_energy_test.go b/integration/oasis/has_enough_energy_test.go index 29ea6039ad5..fb62d27d2cb 100644 --- a/integration/oasis/has_enough_energy_test.go +++ b/integration/oasis/has_enough_energy_test.go @@ -7,7 +7,7 @@ import ( "github.com/Telenav/osrm-backend/integration/pkg/api/osrm/route" ) -func TesthasEnoughEnergyPositive1(t *testing.T) { +func TestHasEnoughEnergyPositive1(t *testing.T) { response := &route.Response{ Routes: []*route.Route{&route.Route{Distance: 10000.0}}, } @@ -26,7 +26,7 @@ func TesthasEnoughEnergyPositive1(t *testing.T) { } -func TesthasEnoughEnergyPositive2(t *testing.T) { +func TestHasEnoughEnergyPositive2(t *testing.T) { response := &route.Response{ Routes: []*route.Route{&route.Route{Distance: 10000.0}}, } diff --git a/integration/oasis/osrmhelper/helper.go b/integration/oasis/osrmhelper/helper.go new file mode 100644 index 00000000000..8044abd3273 --- /dev/null +++ b/integration/oasis/osrmhelper/helper.go @@ -0,0 +1,36 @@ +package osrmhelper + +import ( + "fmt" + "strconv" + + "github.com/Telenav/osrm-backend/integration/pkg/api" + "github.com/Telenav/osrm-backend/integration/pkg/api/osrm/coordinate" + "github.com/Telenav/osrm-backend/integration/pkg/api/osrm/route/options" + "github.com/Telenav/osrm-backend/integration/pkg/api/osrm/table" +) + +// GenerateTableReq4Points accept two group of points and generate osrm table request +func GenerateTableReq4Points(startPoints coordinate.Coordinates, endPoints coordinate.Coordinates) (*table.Request, error) { + if len(startPoints) == 0 || len(endPoints) == 0 { + return nil, fmt.Errorf("calling function with empty points") + } + + // generate table request + req := table.NewRequest() + req.Coordinates = append(startPoints, endPoints...) + + count := 0 + for i := range startPoints { + str := strconv.Itoa(i) + req.Sources = append(req.Sources, str) + count++ + } + for i := range endPoints { + str := strconv.Itoa(i + count) + req.Destinations = append(req.Destinations, str) + } + + req.Annotations = options.AnnotationsValueDistance + api.Comma + options.AnnotationsValueDuration + return req, nil +} diff --git a/integration/oasis/reachable_by_single_charge.go b/integration/oasis/reachable_by_single_charge.go new file mode 100644 index 00000000000..e1af76c9955 --- /dev/null +++ b/integration/oasis/reachable_by_single_charge.go @@ -0,0 +1,176 @@ +package oasis + +import ( + "encoding/json" + "fmt" + "net/http" + "sort" + + "github.com/Telenav/osrm-backend/integration/oasis/osrmconnector" + "github.com/Telenav/osrm-backend/integration/oasis/osrmhelper" + "github.com/Telenav/osrm-backend/integration/oasis/searchconnector" + "github.com/Telenav/osrm-backend/integration/oasis/stationfinder" + "github.com/Telenav/osrm-backend/integration/pkg/api/oasis" + "github.com/Telenav/osrm-backend/integration/pkg/api/osrm/coordinate" + "github.com/Telenav/osrm-backend/integration/pkg/api/osrm/table" + "github.com/Telenav/osrm-backend/integration/pkg/api/search/nearbychargestation" + "github.com/golang/glog" +) + +const maxOverlapPointsNum = 500 + +// Reachable chargestations from orig already be filterred by currage energy range as radius +// For destination, the filter is a dynamic value, depend on where is the nearest charge station. +// We want to make user has enough energy when reach destination +// The energy level is safeRange + nearest charge station's distance to destination +// If there is one or several charge stations could be found in both origStationsResp and destStationsResp +// We think the result is reachable by single charge station +func getOverlapChargeStations4OrigDest(req *oasis.Request, routedistance float64, osrmConnector *osrmconnector.OSRMConnector, tnSearchConnector *searchconnector.TNSearchConnector) coordinate.Coordinates { + // only possible when currRange + maxRange > distance + safeRange + if req.CurrRange+req.MaxRange < routedistance+req.SafeLevel { + return nil + } + + origStations := stationfinder.NewOrigStationFinder(osrmConnector, tnSearchConnector, req) + destStations := stationfinder.NewDestStationFinder(osrmConnector, tnSearchConnector, req) + overlap := stationfinder.FindOverlapBetweenStations(origStations, destStations) + + if len(overlap) == 0 { + return nil + } + + var overlapPoints coordinate.Coordinates + for i, item := range overlap { + overlapPoints = append(overlapPoints, + coordinate.Coordinate{ + Lat: item.Location.Lat, + Lon: item.Location.Lon, + }) + if i > maxOverlapPointsNum { + break + } + } + return overlapPoints +} + +type singleChargeStationCandidate struct { + location coordinate.Coordinate + distanceFromOrig float64 + durationFromOrig float64 + distanceToDest float64 + durationToDest float64 +} + +// @todo these logic might refactored later: charge station status calculation should be moved away +func generateResponse4SingleChargeStation(w http.ResponseWriter, req *oasis.Request, overlapPoints coordinate.Coordinates, osrmConnector *osrmconnector.OSRMConnector) bool { + candidate, err := pickChargeStationWithEarlistArrival(req, overlapPoints, osrmConnector) + + if err != nil { + w.WriteHeader(http.StatusOK) + r := new(oasis.Response) + r.Message = err.Error() + json.NewEncoder(w).Encode(r) + return false + } + + w.WriteHeader(http.StatusOK) + + station := new(oasis.ChargeStation) + station.WaitTime = 0.0 + station.ChargeTime = 7200.0 + station.ChargeRange = req.MaxRange + station.DetailURL = "url" + address := new(nearbychargestation.Address) + address.GeoCoordinate = nearbychargestation.Coordinate{Latitude: candidate.location.Lat, Longitude: candidate.location.Lon} + address.NavCoordinates = append(address.NavCoordinates, &nearbychargestation.Coordinate{Latitude: candidate.location.Lat, Longitude: candidate.location.Lon}) + station.Address = append(station.Address, address) + + solution := new(oasis.Solution) + solution.Distance = candidate.distanceFromOrig + candidate.distanceToDest + solution.Duration = candidate.durationFromOrig + candidate.durationToDest + station.ChargeTime + station.WaitTime + solution.RemainingRage = req.MaxRange + req.CurrRange - solution.Distance + solution.ChargeStations = append(solution.ChargeStations, station) + + r := new(oasis.Response) + r.Code = "200" + r.Message = "Success." + r.Solutions = append(r.Solutions, solution) + + json.NewEncoder(w).Encode(r) + return true +} + +func pickChargeStationWithEarlistArrival(req *oasis.Request, overlapPoints coordinate.Coordinates, osrmConnector *osrmconnector.OSRMConnector) (*singleChargeStationCandidate, error) { + if len(overlapPoints) == 0 { + err := fmt.Errorf("pickChargeStationWithEarlistArrival must be called with none empty overlapPoints") + glog.Fatalf("%v", err) + return nil, err + } + + // request table for orig->overlap stations + origPoint := coordinate.Coordinates{req.Coordinates[0]} + reqOrig, _ := osrmhelper.GenerateTableReq4Points(origPoint, overlapPoints) + respOrigC := osrmConnector.Request4Table(reqOrig) + + // request table for overlap stations -> dest + destPoint := coordinate.Coordinates{req.Coordinates[1]} + reqDest, _ := osrmhelper.GenerateTableReq4Points(overlapPoints, destPoint) + respDestC := osrmConnector.Request4Table(reqDest) + + respOrig := <-respOrigC + respDest := <-respDestC + + if respOrig.Err != nil { + glog.Warningf("Table request failed for url %s with error %v", reqOrig.RequestURI(), respOrig.Err) + return nil, respOrig.Err + } + if respDest.Err != nil { + glog.Warningf("Table request failed for url %s with error %v", reqDest.RequestURI(), respDest.Err) + return nil, respDest.Err + } + if len(respOrig.Resp.Durations[0]) != len(respDest.Resp.Durations) || len(overlapPoints) != len(respOrig.Resp.Durations[0]) { + err := fmt.Errorf("Incorrect table response, the dimension of array is not as expected. [orig2overlap, overlap2dest, overlap]= %d, %d, %d", + len(respOrig.Resp.Durations[0]), len(respDest.Resp.Durations), len(overlapPoints)) + glog.Errorf("%v", err) + return nil, err + } + + index, err := rankingSingleChargeStation(respOrig.Resp, respDest.Resp) + if err != nil { + return nil, err + } + return &singleChargeStationCandidate{ + location: overlapPoints[index], + distanceFromOrig: *respOrig.Resp.Distances[0][index], + durationFromOrig: *respOrig.Resp.Durations[0][index], + distanceToDest: *respDest.Resp.Distances[index][0], + durationToDest: *respDest.Resp.Durations[index][0], + }, nil +} + +type routePassSingleStation struct { + time float64 + index int +} + +func rankingSingleChargeStation(orig2Stations, stations2Dest *table.Response) (int, error) { + if len(orig2Stations.Durations) == 0 || len(orig2Stations.Durations[0]) != len(stations2Dest.Durations) { + err := fmt.Errorf("Incorrect table response for function rankingSingleChargeStation") + glog.Errorf("%v", err) + return -1, err + } + + size := len(orig2Stations.Durations[0]) + + var totalTimes []routePassSingleStation + for i := 0; i < size; i++ { + var route routePassSingleStation + route.time = *orig2Stations.Durations[0][i] + *stations2Dest.Durations[i][0] + route.index = i + totalTimes = append(totalTimes, route) + } + + sort.Slice(totalTimes, func(i, j int) bool { return totalTimes[i].time < totalTimes[j].time }) + + return totalTimes[0].index, nil +} diff --git a/integration/oasis/reachable_by_single_charge_test.go b/integration/oasis/reachable_by_single_charge_test.go new file mode 100644 index 00000000000..cddd9aaf877 --- /dev/null +++ b/integration/oasis/reachable_by_single_charge_test.go @@ -0,0 +1,40 @@ +package oasis + +import ( + "testing" + + "github.com/Telenav/osrm-backend/integration/pkg/api/osrm/table" +) + +var mockFloatArray1 []float64 = []float64{22.2, 11.1, 33.3, 44.4} +var mock1ToNTableResponse1 table.Response = table.Response{ + Durations: [][]*float64{ + []*float64{&mockFloatArray1[0], &mockFloatArray1[1], &mockFloatArray1[2], &mockFloatArray1[3]}, + }, + Distances: [][]*float64{ + []*float64{&mockFloatArray1[0], &mockFloatArray1[1], &mockFloatArray1[2], &mockFloatArray1[3]}, + }, +} + +var mockFloatArray2 []float64 = []float64{66.6, 11.1, 33.3, 33.3} +var mockNTo1TableResponse1 table.Response = table.Response{ + Durations: [][]*float64{ + []*float64{&mockFloatArray2[0]}, + []*float64{&mockFloatArray2[1]}, + []*float64{&mockFloatArray2[2]}, + []*float64{&mockFloatArray2[3]}, + }, + Distances: [][]*float64{ + []*float64{&mockFloatArray2[0]}, + []*float64{&mockFloatArray2[1]}, + []*float64{&mockFloatArray2[2]}, + []*float64{&mockFloatArray2[3]}, + }, +} + +func TestRankingSingleChargeStation(t *testing.T) { + index, err := rankingSingleChargeStation(&mock1ToNTableResponse1, &mockNTo1TableResponse1) + if err != nil || index != 1 { + t.Errorf("expect %v but got %v", 1, index) + } +} diff --git a/integration/oasis/charge_station_searcher.go b/integration/oasis/searchconnector/charge_station_searcher.go similarity index 95% rename from integration/oasis/charge_station_searcher.go rename to integration/oasis/searchconnector/charge_station_searcher.go index 6bf6c0fb1d3..69f602aa767 100644 --- a/integration/oasis/charge_station_searcher.go +++ b/integration/oasis/searchconnector/charge_station_searcher.go @@ -1,4 +1,4 @@ -package oasis +package searchconnector import "github.com/Telenav/osrm-backend/integration/pkg/api/search/nearbychargestation" diff --git a/integration/oasis/searchconnector/connector.go b/integration/oasis/searchconnector/connector.go index 131e6d16ce2..1691ccf7d7a 100644 --- a/integration/oasis/searchconnector/connector.go +++ b/integration/oasis/searchconnector/connector.go @@ -1,7 +1,6 @@ package searchconnector import ( - "github.com/Telenav/osrm-backend/integration/oasis" "github.com/Telenav/osrm-backend/integration/pkg/api/search/nearbychargestation" ) @@ -20,7 +19,7 @@ func NewTNSearchConnector(searchEndpoint, apiKey, apiSignature string) *TNSearch } // ChargeStationSearch returns a channel immediately. Response information could be retrieved from the channel when ready. -func (sc *TNSearchConnector) ChargeStationSearch(req *nearbychargestation.Request) <-chan oasis.ChargeStationsResponse { +func (sc *TNSearchConnector) ChargeStationSearch(req *nearbychargestation.Request) <-chan ChargeStationsResponse { return sc.searchClient.submitSearchReq(req) } diff --git a/integration/oasis/searchconnector/searchhttpclient.go b/integration/oasis/searchconnector/searchhttpclient.go index e740265b346..a333fdd5509 100644 --- a/integration/oasis/searchconnector/searchhttpclient.go +++ b/integration/oasis/searchconnector/searchhttpclient.go @@ -5,7 +5,6 @@ import ( "net/http" "strings" - "github.com/Telenav/osrm-backend/integration/oasis" "github.com/Telenav/osrm-backend/integration/pkg/api/search/nearbychargestation" "github.com/Telenav/osrm-backend/integration/pkg/backend" "github.com/golang/glog" @@ -29,7 +28,7 @@ func newTNSearchHTTPClient(endpoint, apiKey, apiSignature string) *tnSearchHTTPC } } -func (sc *tnSearchHTTPClient) submitSearchReq(req *nearbychargestation.Request) <-chan oasis.ChargeStationsResponse { +func (sc *tnSearchHTTPClient) submitSearchReq(req *nearbychargestation.Request) <-chan ChargeStationsResponse { var url string if !strings.HasPrefix(sc.tnSearchEndpoint, "http://") { url += "http://" @@ -63,7 +62,7 @@ func (sc *tnSearchHTTPClient) handle(req *request) { } defer resp.Body.Close() - var searchResp oasis.ChargeStationsResponse + var searchResp ChargeStationsResponse searchResp.Err = json.NewDecoder(resp.Body).Decode(&searchResp.Resp) req.searchRespC <- searchResp glog.Infof("[tnSearchHTTPClient]Response for request %s" + req.url + "is generated.") diff --git a/integration/oasis/searchconnector/searchrequest.go b/integration/oasis/searchconnector/searchrequest.go index d3d2bb3e503..711c18c21dc 100644 --- a/integration/oasis/searchconnector/searchrequest.go +++ b/integration/oasis/searchconnector/searchrequest.go @@ -1,15 +1,13 @@ package searchconnector -import "github.com/Telenav/osrm-backend/integration/oasis" - type request struct { url string - searchRespC chan oasis.ChargeStationsResponse + searchRespC chan ChargeStationsResponse } func newTNSearchRequest(url string) *request { return &request{ url: url, - searchRespC: make(chan oasis.ChargeStationsResponse), + searchRespC: make(chan ChargeStationsResponse), } } diff --git a/integration/oasis/searchhelper/helper.go b/integration/oasis/searchhelper/helper.go new file mode 100644 index 00000000000..842daaa0ef6 --- /dev/null +++ b/integration/oasis/searchhelper/helper.go @@ -0,0 +1,21 @@ +package searchhelper + +import ( + "github.com/Telenav/osrm-backend/integration/pkg/api/search/nearbychargestation" + "github.com/Telenav/osrm-backend/integration/pkg/api/search/searchcoordinate" +) + +// GenerateSearchRequest accepts center point and limitations and generate nearbychargestation.Request +func GenerateSearchRequest(location searchcoordinate.Coordinate, limit int, radius float64) (*nearbychargestation.Request, error) { + req := nearbychargestation.NewRequest() + req.Location = location + if limit > 0 { + req.Limit = limit + } + + if radius > 0 { + req.Radius = radius + } + + return req, nil +} diff --git a/integration/oasis/stationfinder/basic_finder.go b/integration/oasis/stationfinder/basic_finder.go new file mode 100644 index 00000000000..88c9a1fc970 --- /dev/null +++ b/integration/oasis/stationfinder/basic_finder.go @@ -0,0 +1,49 @@ +package stationfinder + +import ( + "sync" + + "github.com/Telenav/osrm-backend/integration/pkg/api/search/nearbychargestation" +) + +type basicFinder struct { +} + +func (bf *basicFinder) iterateNearbyStations(stations []*nearbychargestation.Result, respLock *sync.RWMutex) <-chan ChargeStationInfo { + if len(stations) == 0 { + c := make(chan ChargeStationInfo) + go func() { + defer close(c) + }() + return c + } + + if respLock != nil { + respLock.RLock() + } + size := len(stations) + results := make([]*nearbychargestation.Result, size) + copy(results, stations) + if respLock != nil { + respLock.RUnlock() + } + + c := make(chan ChargeStationInfo, size) + go func() { + defer close(c) + for _, result := range results { + if len(result.Place.Address) == 0 { + continue + } + station := ChargeStationInfo{ + ID: result.ID, + Location: StationCoordinate{ + Lat: result.Place.Address[0].GeoCoordinate.Latitude, + Lon: result.Place.Address[0].GeoCoordinate.Longitude}, + } + c <- station + } + }() + + return c +} diff --git a/integration/oasis/stationfinder/basic_finder_test.go b/integration/oasis/stationfinder/basic_finder_test.go new file mode 100644 index 00000000000..f4b569feaaf --- /dev/null +++ b/integration/oasis/stationfinder/basic_finder_test.go @@ -0,0 +1,284 @@ +package stationfinder + +import ( + "reflect" + "sync" + "testing" + + "github.com/Telenav/osrm-backend/integration/pkg/api/search/nearbychargestation" +) + +var mockSearchResponse1 *nearbychargestation.Response = &nearbychargestation.Response{ + Results: []*nearbychargestation.Result{ + &nearbychargestation.Result{ + ID: "station1", + Place: nearbychargestation.Place{ + Address: []*nearbychargestation.Address{ + &nearbychargestation.Address{ + GeoCoordinate: nearbychargestation.Coordinate{ + Latitude: 32.333, + Longitude: 122.333, + }, + NavCoordinates: []*nearbychargestation.Coordinate{ + &nearbychargestation.Coordinate{ + Latitude: 32.333, + Longitude: 122.333, + }, + }, + }, + }, + }, + }, + &nearbychargestation.Result{ + ID: "station2", + Place: nearbychargestation.Place{ + Address: []*nearbychargestation.Address{ + &nearbychargestation.Address{ + GeoCoordinate: nearbychargestation.Coordinate{ + Latitude: -32.333, + Longitude: -122.333, + }, + NavCoordinates: []*nearbychargestation.Coordinate{ + &nearbychargestation.Coordinate{ + Latitude: -32.333, + Longitude: -122.333, + }, + }, + }, + }, + }, + }, + &nearbychargestation.Result{ + ID: "station3", + Place: nearbychargestation.Place{ + Address: []*nearbychargestation.Address{ + &nearbychargestation.Address{ + GeoCoordinate: nearbychargestation.Coordinate{ + Latitude: 32.333, + Longitude: -122.333, + }, + NavCoordinates: []*nearbychargestation.Coordinate{ + &nearbychargestation.Coordinate{ + Latitude: 32.333, + Longitude: -122.333, + }, + }, + }, + }, + }, + }, + &nearbychargestation.Result{ + ID: "station4", + Place: nearbychargestation.Place{ + Address: []*nearbychargestation.Address{ + &nearbychargestation.Address{ + GeoCoordinate: nearbychargestation.Coordinate{ + Latitude: -32.333, + Longitude: 122.333, + }, + NavCoordinates: []*nearbychargestation.Coordinate{ + &nearbychargestation.Coordinate{ + Latitude: -32.333, + Longitude: 122.333, + }, + }, + }, + }, + }, + }, + }, +} + +var mockSearchResponse2 *nearbychargestation.Response = &nearbychargestation.Response{ + Results: []*nearbychargestation.Result{ + &nearbychargestation.Result{ + ID: "station1", + Place: nearbychargestation.Place{ + Address: []*nearbychargestation.Address{ + &nearbychargestation.Address{ + GeoCoordinate: nearbychargestation.Coordinate{ + Latitude: 32.333, + Longitude: 122.333, + }, + NavCoordinates: []*nearbychargestation.Coordinate{ + &nearbychargestation.Coordinate{ + Latitude: 32.333, + Longitude: 122.333, + }, + }, + }, + }, + }, + }, + &nearbychargestation.Result{ + ID: "station2", + Place: nearbychargestation.Place{ + Address: []*nearbychargestation.Address{ + &nearbychargestation.Address{ + GeoCoordinate: nearbychargestation.Coordinate{ + Latitude: -32.333, + Longitude: -122.333, + }, + NavCoordinates: []*nearbychargestation.Coordinate{ + &nearbychargestation.Coordinate{ + Latitude: -32.333, + Longitude: -122.333, + }, + }, + }, + }, + }, + }, + &nearbychargestation.Result{ + ID: "station5", + Place: nearbychargestation.Place{ + Address: []*nearbychargestation.Address{ + &nearbychargestation.Address{ + GeoCoordinate: nearbychargestation.Coordinate{ + Latitude: -12.333, + Longitude: 122.333, + }, + NavCoordinates: []*nearbychargestation.Coordinate{ + &nearbychargestation.Coordinate{ + Latitude: -12.333, + Longitude: 122.333, + }, + }, + }, + }, + }, + }, + }, +} + +var mockChargeStationInfo1 []ChargeStationInfo = []ChargeStationInfo{ + ChargeStationInfo{ + ID: "station1", + Location: StationCoordinate{ + Lat: 32.333, + Lon: 122.333, + }, + }, + ChargeStationInfo{ + ID: "station2", + Location: StationCoordinate{ + Lat: -32.333, + Lon: -122.333, + }, + }, + ChargeStationInfo{ + ID: "station3", + Location: StationCoordinate{ + Lat: 32.333, + Lon: -122.333, + }, + }, + ChargeStationInfo{ + ID: "station4", + Location: StationCoordinate{ + Lat: -32.333, + Lon: 122.333, + }, + }, +} + +var mockChargeStationInfo2 []ChargeStationInfo = []ChargeStationInfo{ + ChargeStationInfo{ + ID: "station1", + Location: StationCoordinate{ + Lat: 32.333, + Lon: 122.333, + }, + }, + ChargeStationInfo{ + ID: "station2", + Location: StationCoordinate{ + Lat: -32.333, + Lon: -122.333, + }, + }, + ChargeStationInfo{ + ID: "station5", + Location: StationCoordinate{ + Lat: -12.333, + Lon: 122.333, + }, + }, +} + +func TestBasicFinderCorrectness(t *testing.T) { + cases := []struct { + input []*nearbychargestation.Result + expect []ChargeStationInfo + }{ + { + mockSearchResponse1.Results, + mockChargeStationInfo1, + }, + } + + for _, b := range cases { + input := b.input + expect := b.expect + var bf basicFinder + c := bf.iterateNearbyStations(input, nil) + + var wg sync.WaitGroup + go func(wg *sync.WaitGroup) { + wg.Add(1) + defer wg.Done() + + var r []ChargeStationInfo + for item := range c { + r = append(r, item) + } + + if !reflect.DeepEqual(r, expect) { + t.Errorf("parse %v expect %v but got %v", b.input, b.expect, r) + } + }(&wg) + wg.Wait() + } +} + +func TestBasicFinderAsync(t *testing.T) { + cases := []struct { + input []*nearbychargestation.Result + inputLock *sync.RWMutex + expect []ChargeStationInfo + }{ + { + mockSearchResponse1.Results, + &sync.RWMutex{}, + mockChargeStationInfo1, + }, + } + + for _, b := range cases { + input := b.input + expect := b.expect + var bf basicFinder + + num := 20 + var wg sync.WaitGroup + for i := 0; i < num; i++ { + go func(wg *sync.WaitGroup) { + wg.Add(1) + + c := bf.iterateNearbyStations(input, b.inputLock) + go func(wg *sync.WaitGroup) { + defer wg.Done() + var r []ChargeStationInfo + for item := range c { + r = append(r, item) + } + if !reflect.DeepEqual(r, expect) { + t.Errorf("parse %v expect %v but got %v", b.input, b.expect, r) + } + }(wg) + }(&wg) + } + wg.Wait() + + } +} diff --git a/integration/oasis/stationfinder/dest_station_finder.go b/integration/oasis/stationfinder/dest_station_finder.go new file mode 100644 index 00000000000..a8244ba0206 --- /dev/null +++ b/integration/oasis/stationfinder/dest_station_finder.go @@ -0,0 +1,63 @@ +package stationfinder + +import ( + "sync" + + "github.com/Telenav/osrm-backend/integration/oasis/osrmconnector" + "github.com/Telenav/osrm-backend/integration/oasis/searchconnector" + "github.com/Telenav/osrm-backend/integration/oasis/searchhelper" + "github.com/Telenav/osrm-backend/integration/pkg/api/oasis" + "github.com/Telenav/osrm-backend/integration/pkg/api/search/nearbychargestation" + "github.com/Telenav/osrm-backend/integration/pkg/api/search/searchcoordinate" + "github.com/golang/glog" +) + +//@todo: This number need to be adjusted based on charge station profile +const destMaxSearchCandidateNumber = 999 + +type destStationFinder struct { + osrmConnector *osrmconnector.OSRMConnector + tnSearchConnector *searchconnector.TNSearchConnector + oasisReq *oasis.Request + searchResp *nearbychargestation.Response + searchRespLock *sync.RWMutex + bf *basicFinder +} + +func NewDestStationFinder(oc *osrmconnector.OSRMConnector, sc *searchconnector.TNSearchConnector, oasisReq *oasis.Request) *destStationFinder { + obj := &destStationFinder{ + osrmConnector: oc, + tnSearchConnector: sc, + oasisReq: oasisReq, + searchResp: nil, + searchRespLock: &sync.RWMutex{}, + bf: &basicFinder{}, + } + obj.prepare() + return obj +} + +func (sf *destStationFinder) prepare() { + req, _ := searchhelper.GenerateSearchRequest( + searchcoordinate.Coordinate{ + Lat: sf.oasisReq.Coordinates[1].Lat, + Lon: sf.oasisReq.Coordinates[1].Lon}, + destMaxSearchCandidateNumber, + sf.oasisReq.MaxRange-sf.oasisReq.SafeLevel) + + respC := sf.tnSearchConnector.ChargeStationSearch(req) + resp := <-respC + if resp.Err != nil { + glog.Warningf("Search failed during prepare orig search for url: %s", req.RequestURI()) + return + } + + sf.searchRespLock.Lock() + sf.searchResp = resp.Resp + sf.searchRespLock.Unlock() + return +} + +func (sf *destStationFinder) iterateNearbyStations() <-chan ChargeStationInfo { + return sf.bf.iterateNearbyStations(sf.searchResp.Results, sf.searchRespLock) +} diff --git a/integration/oasis/stationfinder/dest_station_finder_test.go b/integration/oasis/stationfinder/dest_station_finder_test.go new file mode 100644 index 00000000000..63d9dd49740 --- /dev/null +++ b/integration/oasis/stationfinder/dest_station_finder_test.go @@ -0,0 +1,45 @@ +package stationfinder + +import ( + "reflect" + "sync" + "testing" +) + +func createMockDestStationFinder1() *destStationFinder { + obj := &destStationFinder{ + osrmConnector: nil, + tnSearchConnector: nil, + oasisReq: nil, + searchResp: mockSearchResponse1, + searchRespLock: &sync.RWMutex{}, + bf: &basicFinder{}, + } + return obj +} + +func createMockDestStationFinder2() *destStationFinder { + obj := &destStationFinder{ + osrmConnector: nil, + tnSearchConnector: nil, + oasisReq: nil, + searchResp: mockSearchResponse2, + searchRespLock: &sync.RWMutex{}, + bf: &basicFinder{}, + } + return obj +} + +func TestDestStationFinderIterator(t *testing.T) { + sf := createMockDestStationFinder1() + c := sf.iterateNearbyStations() + var r []ChargeStationInfo + go func() { + for item := range c { + r = append(r, item) + } + if !reflect.DeepEqual(r, mockChargeStationInfo1) { + t.Errorf("expect %v but got %v", mockChargeStationInfo1, r) + } + }() +} diff --git a/integration/oasis/stationfinder/doc.go b/integration/oasis/stationfinder/doc.go new file mode 100644 index 00000000000..e62f14f8d1e --- /dev/null +++ b/integration/oasis/stationfinder/doc.go @@ -0,0 +1,19 @@ +// Package stationfinder provide functionality to find nearby charge stations and +// related algorithm. +// Finders: +// - origStationFinder holds logic for how to find reachable charge stations +// based on current energy level. +// - destStationFinder holds logic for how to find reachable charge stations +// based on safe energy level and distance to nearest charge station(todo). +// - lowEnergyLocationStationFinder holds logic for how to find reachable +// charge station near certain location. + +// Algorithm: +// - Each finder provide iterator to iterate charge station candidates. +// - The choice of channel as response makes algorithm could be asynchronous func. +// - FindOverlapBetweenStations provide functionality to find overlap +// between two iterator. +// - CalcCostBetweenChargeStationsPair provide functionality to calculate +// cost between two group of charge stations which could construct a new +// graph as edges. +package stationfinder diff --git a/integration/oasis/stationfinder/low_energy_location_station_finder.go b/integration/oasis/stationfinder/low_energy_location_station_finder.go new file mode 100644 index 00000000000..cd06331a041 --- /dev/null +++ b/integration/oasis/stationfinder/low_energy_location_station_finder.go @@ -0,0 +1 @@ +package stationfinder diff --git a/integration/oasis/stationfinder/orig_station_finder.go b/integration/oasis/stationfinder/orig_station_finder.go new file mode 100644 index 00000000000..ba4853d80fd --- /dev/null +++ b/integration/oasis/stationfinder/orig_station_finder.go @@ -0,0 +1,63 @@ +package stationfinder + +import ( + "sync" + + "github.com/Telenav/osrm-backend/integration/oasis/osrmconnector" + "github.com/Telenav/osrm-backend/integration/oasis/searchconnector" + "github.com/Telenav/osrm-backend/integration/oasis/searchhelper" + "github.com/Telenav/osrm-backend/integration/pkg/api/oasis" + "github.com/Telenav/osrm-backend/integration/pkg/api/search/nearbychargestation" + "github.com/Telenav/osrm-backend/integration/pkg/api/search/searchcoordinate" + "github.com/golang/glog" +) + +//@todo: This number need to be adjusted based on charge station profile +const origMaxSearchCandidateNumber = 999 + +type origStationFinder struct { + osrmConnector *osrmconnector.OSRMConnector + tnSearchConnector *searchconnector.TNSearchConnector + oasisReq *oasis.Request + searchResp *nearbychargestation.Response + searchRespLock *sync.RWMutex + bf *basicFinder +} + +func NewOrigStationFinder(oc *osrmconnector.OSRMConnector, sc *searchconnector.TNSearchConnector, oasisReq *oasis.Request) *origStationFinder { + obj := &origStationFinder{ + osrmConnector: oc, + tnSearchConnector: sc, + oasisReq: oasisReq, + searchResp: nil, + searchRespLock: &sync.RWMutex{}, + bf: &basicFinder{}, + } + obj.prepare() + return obj +} + +func (sf *origStationFinder) prepare() { + req, _ := searchhelper.GenerateSearchRequest( + searchcoordinate.Coordinate{ + Lat: sf.oasisReq.Coordinates[0].Lat, + Lon: sf.oasisReq.Coordinates[0].Lon}, + origMaxSearchCandidateNumber, + sf.oasisReq.CurrRange) + + respC := sf.tnSearchConnector.ChargeStationSearch(req) + resp := <-respC + if resp.Err != nil { + glog.Warningf("Search failed during prepare orig search for url: %s", req.RequestURI()) + return + } + + sf.searchRespLock.Lock() + sf.searchResp = resp.Resp + sf.searchRespLock.Unlock() + return +} + +func (sf *origStationFinder) iterateNearbyStations() <-chan ChargeStationInfo { + return sf.bf.iterateNearbyStations(sf.searchResp.Results, sf.searchRespLock) +} diff --git a/integration/oasis/stationfinder/orig_station_finder_test.go b/integration/oasis/stationfinder/orig_station_finder_test.go new file mode 100644 index 00000000000..ed9056f3593 --- /dev/null +++ b/integration/oasis/stationfinder/orig_station_finder_test.go @@ -0,0 +1,45 @@ +package stationfinder + +import ( + "reflect" + "sync" + "testing" +) + +func createMockOrigStationFinder1() *origStationFinder { + obj := &origStationFinder{ + osrmConnector: nil, + tnSearchConnector: nil, + oasisReq: nil, + searchResp: mockSearchResponse1, + searchRespLock: &sync.RWMutex{}, + bf: &basicFinder{}, + } + return obj +} + +func createMockOrigStationFinder2() *origStationFinder { + obj := &origStationFinder{ + osrmConnector: nil, + tnSearchConnector: nil, + oasisReq: nil, + searchResp: mockSearchResponse2, + searchRespLock: &sync.RWMutex{}, + bf: &basicFinder{}, + } + return obj +} + +func TestOrigStationFinderIterator(t *testing.T) { + sf := createMockOrigStationFinder1() + c := sf.iterateNearbyStations() + var r []ChargeStationInfo + go func() { + for item := range c { + r = append(r, item) + } + if !reflect.DeepEqual(r, mockChargeStationInfo1) { + t.Errorf("expect %v but got %v", mockChargeStationInfo1, r) + } + }() +} diff --git a/integration/oasis/stationfinder/stations_iterator.go b/integration/oasis/stationfinder/stations_iterator.go new file mode 100644 index 00000000000..c2b7eaeda3a --- /dev/null +++ b/integration/oasis/stationfinder/stations_iterator.go @@ -0,0 +1,5 @@ +package stationfinder + +type nearbyStationsIterator interface { + iterateNearbyStations() <-chan ChargeStationInfo +} diff --git a/integration/oasis/stationfinder/stations_iterator_alg.go b/integration/oasis/stationfinder/stations_iterator_alg.go new file mode 100644 index 00000000000..a5a3f17aad3 --- /dev/null +++ b/integration/oasis/stationfinder/stations_iterator_alg.go @@ -0,0 +1,38 @@ +package stationfinder + +// FindOverlapBetweenStations finds overlap charge stations based on two iterator +func FindOverlapBetweenStations(iterF nearbyStationsIterator, iterS nearbyStationsIterator) []ChargeStationInfo { + var overlap []ChargeStationInfo + dict := buildChargeStationInfoDict(iterF) + c := iterS.iterateNearbyStations() + for item := range c { + if _, has := dict[item.ID]; has { + overlap = append(overlap, item) + } + } + + return overlap +} + +// ChargeStationInfo defines charge station information +type ChargeStationInfo struct { + ID string + Location StationCoordinate + err error +} + +// StationCoordinate represents location information +type StationCoordinate struct { + Lat float64 + Lon float64 +} + +func buildChargeStationInfoDict(iter nearbyStationsIterator) map[string]bool { + dict := make(map[string]bool) + c := iter.iterateNearbyStations() + for item := range c { + dict[item.ID] = true + } + + return dict +} diff --git a/integration/oasis/stationfinder/stations_iterator_alg_test.go b/integration/oasis/stationfinder/stations_iterator_alg_test.go new file mode 100644 index 00000000000..9d04731d9d3 --- /dev/null +++ b/integration/oasis/stationfinder/stations_iterator_alg_test.go @@ -0,0 +1,48 @@ +package stationfinder + +import ( + "reflect" + "testing" +) + +var mockDict1 map[string]bool = map[string]bool{ + "station1": true, + "station2": true, + "station3": true, + "station4": true, +} + +func TestBuildChargeStationInfoDict1(t *testing.T) { + sf := createMockOrigStationFinder1() + m := buildChargeStationInfoDict(sf) + if !reflect.DeepEqual(m, mockDict1) { + t.Errorf("expect %v but got %v", mockDict1, m) + } +} + +var overlapChargeStationInfo1 []ChargeStationInfo = []ChargeStationInfo{ + ChargeStationInfo{ + ID: "station1", + Location: StationCoordinate{ + Lat: 32.333, + Lon: 122.333, + }, + }, + ChargeStationInfo{ + ID: "station2", + Location: StationCoordinate{ + Lat: -32.333, + Lon: -122.333, + }, + }, +} + +func TestFindOverlapBetweenStations1(t *testing.T) { + sf1 := createMockOrigStationFinder2() + sf2 := createMockDestStationFinder1() + r := FindOverlapBetweenStations(sf1, sf2) + + if !reflect.DeepEqual(r, overlapChargeStationInfo1) { + t.Errorf("expect %v but got %v", overlapChargeStationInfo1, r) + } +} diff --git a/integration/pkg/api/search/nearbychargestation/request.go b/integration/pkg/api/search/nearbychargestation/request.go index c070a498f40..c162cbc9843 100644 --- a/integration/pkg/api/search/nearbychargestation/request.go +++ b/integration/pkg/api/search/nearbychargestation/request.go @@ -5,8 +5,8 @@ import ( "strconv" "github.com/Telenav/osrm-backend/integration/pkg/api" - "github.com/Telenav/osrm-backend/integration/pkg/api/search/coordinate" "github.com/Telenav/osrm-backend/integration/pkg/api/search/options" + "github.com/Telenav/osrm-backend/integration/pkg/api/search/searchcoordinate" "github.com/golang/glog" ) @@ -20,7 +20,7 @@ type Request struct { APIKey string APISignature string Category string - Location coordinate.Coordinate + Location searchcoordinate.Coordinate Intent string Locale string Limit int @@ -40,7 +40,7 @@ func NewRequest() *Request { APIKey: "", APISignature: "", Category: options.ChargeStationCategory, - Location: coordinate.Coordinate{}, + Location: searchcoordinate.Coordinate{}, Intent: options.AroundIntent, Locale: options.ENUSLocale, Limit: options.DefaultLimitValue, diff --git a/integration/pkg/api/search/coordinate/coordinate.go b/integration/pkg/api/search/searchcoordinate/coordinate.go similarity index 97% rename from integration/pkg/api/search/coordinate/coordinate.go rename to integration/pkg/api/search/searchcoordinate/coordinate.go index 87067e07d1d..6684432e073 100644 --- a/integration/pkg/api/search/coordinate/coordinate.go +++ b/integration/pkg/api/search/searchcoordinate/coordinate.go @@ -1,4 +1,4 @@ -package coordinate +package searchcoordinate import ( "fmt"