package handlers import ( "fmt" "io" "math" "net/http" "otaupdate/services" "strings" "github.com/fiskerinc/cloud-services/pkg/clickhouse" "github.com/fiskerinc/cloud-services/pkg/common" "github.com/fiskerinc/cloud-services/pkg/logger" "github.com/fiskerinc/cloud-services/pkg/utils" "github.com/fiskerinc/cloud-services/pkg/validator" "github.com/gorilla/schema" "github.com/intel-go/fastjson" "github.com/pkg/errors" "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" ) // HandleVehiclePathsPost godoc // @Summary Gets paths of vehicles // @Description Returns vehicle paths // @Accept json // @Produce json // @Param Authorization header string false "Bearer " // @Param Api-Key header string false "" // @Param vins body vehicleBodyParams true "List of VINs" // @Param lookback_hours query int false "Data lookback window in hours. Default is 24 if not set" // @Success 200 {object} common.GpsPaths // @Failure 400 {object} common.JSONError "Bad request" // @Failure 401 {object} common.JSONError "Unauthorized" // @Failure 503 {object} common.JSONError "Service unavailable" // @Router /vehicle_paths [post] func HandleVehiclePathsPost(w http.ResponseWriter, r *http.Request) { query, body, err := parseVehiclePathsParams(r) if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { return } conn, err := services.GetClickhouseClient() if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { logger.Error().Err(err).Msg("cannot get clickhouse client") return } paths, err := selectVehiclePaths(conn, body.VINs, query.LookbackHours) if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { logger.Error().Err(err).Msg("failed to select vehicle paths from feature_table") return } utils.RespJSON(w, http.StatusOK, paths) } func parseVehiclePathsParams(r *http.Request) (vehiclePathQuery, vehicleBodyParams, error) { sch := schema.NewDecoder() sch.SetAliasTag("json") queryParams := vehiclePathQuery{} bodyParams := vehicleBodyParams{} //process query params err := sch.Decode(&queryParams, r.URL.Query()) if err != nil { return vehiclePathQuery{}, vehicleBodyParams{}, errors.WithStack(err) } err = validator.GetValidator().Struct(queryParams) if err != nil { return vehiclePathQuery{}, vehicleBodyParams{}, errors.WithStack(err) } //process body params err = fastjson.NewDecoder(r.Body).Decode(&bodyParams) if err != nil && err != io.EOF { return vehiclePathQuery{}, vehicleBodyParams{}, errors.WithStack(err) } err = validator.GetValidator().Struct(bodyParams) if err != nil { return vehiclePathQuery{}, vehicleBodyParams{}, errors.WithStack(err) } //set default value for queryParams if queryParams.LookbackHours == 0 { queryParams.LookbackHours = 24 } return queryParams, bodyParams, nil } func selectVehiclePaths(conn clickhouse.ClientInterface, vinList []string, lookbackHours int64) (common.GpsPaths, error) { var vinListSb strings.Builder //serialize vinList for query vinListSb.WriteRune('[') for i, vin := range vinList { vinListSb.WriteRune('\'') vinListSb.WriteString(vin) vinListSb.WriteRune('\'') if i < len(vinList)-1 { vinListSb.WriteRune(',') } } vinListSb.WriteRune(']') //query gps routes queryText := fmt.Sprintf("SELECT VIN, TBOX_GPSLati, TBOX_GPSLongi FROM feature_table WHERE Timestamp > now() - (interval %d hour) AND VIN in %s ORDER BY Timestamp ASC", lookbackHours, vinListSb.String()) logger.Info().Msgf("query: %s", queryText) var results []ChGpsRow err := conn.Select(&results, queryText) if err != nil { return nil, err } //transform query results var pathsMap common.GpsPaths = make(common.GpsPaths, len(vinList)) //populate pathsMap for _, vin := range vinList { pathsMap[vin] = common.GpsPath{} } for _, row := range results { _, ok := pathsMap[row.VIN] if !ok { pathsMap[row.VIN] = common.GpsPath{} } //ignore invalid GPS points if math.IsNaN(row.TBOX_GPSLati) || math.IsNaN(row.TBOX_GPSLongi) { continue } if row.TBOX_GPSLati == 0 && row.TBOX_GPSLongi == 0 { continue } if row.TBOX_GPSLati > 90 || row.TBOX_GPSLati < -90 { continue } if row.TBOX_GPSLongi > 180 || row.TBOX_GPSLongi < -180 { continue } pathsMap[row.VIN] = append(pathsMap[row.VIN], common.GpsPoint{row.TBOX_GPSLati, row.TBOX_GPSLongi}, ) } return pathsMap, nil } type ChGpsRow struct { VIN string `ch:"VIN" json:"VIN"` TBOX_GPSLati float64 `ch:"TBOX_GPSLati" json:"TBOX_GPSLati"` TBOX_GPSLongi float64 `ch:"TBOX_GPSLongi" json:"TBOX_GPSLongi"` } type vehiclePathQuery struct { LookbackHours int64 `json:"lookback_hours"` } type vehicleBodyParams struct { VINs []string `json:"vins" validate:"required"` }