171 lines
4.7 KiB
Go
171 lines
4.7 KiB
Go
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 <ID token>"
|
|
// @Param Api-Key header string false "<API token>"
|
|
// @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"`
|
|
}
|