package web import ( "net/url" "strconv" "time" "github.com/martinlehoux/biking_home/ride" "github.com/martinlehoux/biking_home/rides" ) const ( rideFilterDistanceMinKm = 10 rideFilterDistanceMaxKm = 300 rideFilterDistanceStepKm = 10 rideFilterDurationMinMinutes = 0 rideFilterDurationMaxMinutes = 24 * 60 rideFilterDurationStepMinutes = 30 rideFilterCotacolMinPoints = 0 rideFilterCotacolMaxPoints = 5000 rideFilterCotacolStepPoints = 50 ) const ( rideFilterDistanceMinParam = "distance_min" rideFilterDistanceMaxParam = "distance_max" rideFilterDurationMinParam = "duration_min" rideFilterDurationMaxParam = "duration_max" rideFilterDateFromParam = "date_from" rideFilterDateToParam = "date_to" rideFilterCotacolMinParam = "cotacol_min" rideFilterCotacolMaxParam = "cotacol_max" rideFilterRouteParam = "route" ) const ( rideRouteFilterAny = "" rideRouteFilterAvailable = "available" rideRouteFilterMissing = "missing" ) type RideFilters struct { DistanceMinKm *int DistanceMaxKm *int DurationMinMinutes *int DurationMaxMinutes *int DateFrom *time.Time DateToExclusive *time.Time CotacolMinPoints *int CotacolMaxPoints *int Route string } type RideFilterView struct { Active bool DistanceMinKm int DistanceMaxKm int DurationMinMinutes int DurationMaxMinutes int DateFrom string DateTo string CotacolMinPoints int CotacolMaxPoints int Route string ResetURL string } func parseRideFilters(query url.Values) RideFilters { filters := RideFilters{ DistanceMinKm: parseRideFilterInt(query.Get(rideFilterDistanceMinParam), rideFilterDistanceMinKm, rideFilterDistanceMaxKm, rideFilterDistanceStepKm), DistanceMaxKm: parseRideFilterInt(query.Get(rideFilterDistanceMaxParam), rideFilterDistanceMinKm, rideFilterDistanceMaxKm, rideFilterDistanceStepKm), DurationMinMinutes: parseRideFilterInt(query.Get(rideFilterDurationMinParam), rideFilterDurationMinMinutes, rideFilterDurationMaxMinutes, rideFilterDurationStepMinutes), DurationMaxMinutes: parseRideFilterInt(query.Get(rideFilterDurationMaxParam), rideFilterDurationMinMinutes, rideFilterDurationMaxMinutes, rideFilterDurationStepMinutes), CotacolMinPoints: parseRideFilterInt(query.Get(rideFilterCotacolMinParam), rideFilterCotacolMinPoints, rideFilterCotacolMaxPoints, rideFilterCotacolStepPoints), CotacolMaxPoints: parseRideFilterInt(query.Get(rideFilterCotacolMaxParam), rideFilterCotacolMinPoints, rideFilterCotacolMaxPoints, rideFilterCotacolStepPoints), Route: parseRideRouteFilter(query.Get(rideFilterRouteParam)), } filters.DateFrom = parseRideFilterDate(query.Get(rideFilterDateFromParam), false) filters.DateToExclusive = parseRideFilterDate(query.Get(rideFilterDateToParam), true) filters.DistanceMinKm = clearRideFilterBound(filters.DistanceMinKm, rideFilterDistanceMinKm) filters.DistanceMaxKm = clearRideFilterBound(filters.DistanceMaxKm, rideFilterDistanceMaxKm) filters.DurationMinMinutes = clearRideFilterBound(filters.DurationMinMinutes, rideFilterDurationMinMinutes) filters.DurationMaxMinutes = clearRideFilterBound(filters.DurationMaxMinutes, rideFilterDurationMaxMinutes) filters.CotacolMinPoints = clearRideFilterBound(filters.CotacolMinPoints, rideFilterCotacolMinPoints) filters.CotacolMaxPoints = clearRideFilterBound(filters.CotacolMaxPoints, rideFilterCotacolMaxPoints) if filters.DateFrom != nil && filters.DateToExclusive != nil && !filters.DateFrom.Before(*filters.DateToExclusive) { filters.DateFrom = nil filters.DateToExclusive = nil } if filters.DistanceMinKm != nil && filters.DistanceMaxKm != nil && *filters.DistanceMinKm > *filters.DistanceMaxKm { filters.DistanceMinKm = nil filters.DistanceMaxKm = nil } if filters.DurationMinMinutes != nil && filters.DurationMaxMinutes != nil && *filters.DurationMinMinutes > *filters.DurationMaxMinutes { filters.DurationMinMinutes = nil filters.DurationMaxMinutes = nil } if filters.CotacolMinPoints != nil && filters.CotacolMaxPoints != nil && *filters.CotacolMinPoints > *filters.CotacolMaxPoints { filters.CotacolMinPoints = nil filters.CotacolMaxPoints = nil } return filters } func clearRideFilterBound(value *int, bound int) *int { if value != nil && *value == bound { return nil } return value } func parseRideFilterInt(value string, min, max, step int) *int { if value == "" { return nil } parsed, err := strconv.Atoi(value) if err != nil || parsed < min || parsed > max || (parsed-min)%step != 0 { return nil } return &parsed } func parseRideFilterDate(value string, end bool) *time.Time { if value == "" { return nil } parsed, err := time.Parse(dateFormat, value) if err != nil { return nil } if end { parsed = parsed.AddDate(0, 0, 1) } return &parsed } func parseRideRouteFilter(value string) string { if value == rideRouteFilterAvailable || value == rideRouteFilterMissing { return value } return rideRouteFilterAny } func buildRideFilterView(filters RideFilters, sort RideSort) RideFilterView { dateFrom := "" if filters.DateFrom != nil { dateFrom = filters.DateFrom.Format(dateFormat) } dateTo := "" if filters.DateToExclusive != nil { dateTo = filters.DateToExclusive.AddDate(0, 0, -1).Format(dateFormat) } return RideFilterView{ Active: filters.Active(), DistanceMinKm: rideFilterValue(filters.DistanceMinKm, rideFilterDistanceMinKm), DistanceMaxKm: rideFilterValue(filters.DistanceMaxKm, rideFilterDistanceMaxKm), DurationMinMinutes: rideFilterValue(filters.DurationMinMinutes, rideFilterDurationMinMinutes), DurationMaxMinutes: rideFilterValue(filters.DurationMaxMinutes, rideFilterDurationMaxMinutes), DateFrom: dateFrom, DateTo: dateTo, CotacolMinPoints: rideFilterValue(filters.CotacolMinPoints, rideFilterCotacolMinPoints), CotacolMaxPoints: rideFilterValue(filters.CotacolMaxPoints, rideFilterCotacolMaxPoints), Route: filters.Route, ResetURL: rideFiltersResetURL(sort), } } func rideFilterValue(value *int, fallback int) int { if value == nil { return fallback } return *value } func formatRideFilterInt(value int) string { return strconv.Itoa(value) } func formatRideFilterDistance(value int) string { return strconv.Itoa(value) + " km" } func formatRideFilterDuration(value int) string { hours := value / 60 minutes := value % 60 return strconv.Itoa(hours) + "h " + formatTwoDigitNumber(minutes) + "m" } func formatRideFilterCotacol(value int) string { return strconv.Itoa(value) + " points" } func formatTwoDigitNumber(value int) string { if value < 10 { return "0" + strconv.Itoa(value) } return strconv.Itoa(value) } func rideFilterQuery(filters RideFilters) url.Values { query := url.Values{} if filters.DistanceMinKm != nil { query.Set(rideFilterDistanceMinParam, formatRideFilterInt(*filters.DistanceMinKm)) } if filters.DistanceMaxKm != nil { query.Set(rideFilterDistanceMaxParam, formatRideFilterInt(*filters.DistanceMaxKm)) } if filters.DurationMinMinutes != nil { query.Set(rideFilterDurationMinParam, formatRideFilterInt(*filters.DurationMinMinutes)) } if filters.DurationMaxMinutes != nil { query.Set(rideFilterDurationMaxParam, formatRideFilterInt(*filters.DurationMaxMinutes)) } if filters.DateFrom != nil { query.Set(rideFilterDateFromParam, filters.DateFrom.Format(dateFormat)) } if filters.DateToExclusive != nil { query.Set(rideFilterDateToParam, filters.DateToExclusive.AddDate(0, 0, -1).Format(dateFormat)) } if filters.CotacolMinPoints != nil { query.Set(rideFilterCotacolMinParam, formatRideFilterInt(*filters.CotacolMinPoints)) } if filters.CotacolMaxPoints != nil { query.Set(rideFilterCotacolMaxParam, formatRideFilterInt(*filters.CotacolMaxPoints)) } if filters.Route != rideRouteFilterAny { query.Set(rideFilterRouteParam, filters.Route) } return query } func rideFiltersResetURL(sort RideSort) string { if sort.Column == rideSortStarted && sort.Descending { return "/" } query := url.Values{} query.Set("sort", sort.Column) query.Set("dir", rideSortDirection(sort)) return "/?" + query.Encode() } func (filters RideFilters) Active() bool { return filters.DistanceMinKm != nil || filters.DistanceMaxKm != nil || filters.DurationMinMinutes != nil || filters.DurationMaxMinutes != nil || filters.DateFrom != nil || filters.DateToExclusive != nil || filters.CotacolMinPoints != nil || filters.CotacolMaxPoints != nil || filters.Route != rideRouteFilterAny } func filterRides(items []rides.Ride, filters RideFilters) []rides.Ride { if !filters.Active() { return items } filtered := make([]rides.Ride, 0, len(items)) for _, item := range items { if rideMatchesFilters(item, filters) { filtered = append(filtered, item) } } return filtered } func rideMatchesFilters(item rides.Ride, filters RideFilters) bool { if filters.DistanceMinKm != nil && item.DistanceM < float64(*filters.DistanceMinKm)*1000 { return false } if filters.DistanceMaxKm != nil && item.DistanceM > float64(*filters.DistanceMaxKm)*1000 { return false } if filters.DurationMinMinutes != nil && item.MovingTimeS < int64(*filters.DurationMinMinutes)*60 { return false } if filters.DurationMaxMinutes != nil && item.MovingTimeS > int64(*filters.DurationMaxMinutes)*60 { return false } startDate := item.StartDate.UTC() if filters.DateFrom != nil && startDate.Before(*filters.DateFrom) { return false } if filters.DateToExclusive != nil && !startDate.Before(*filters.DateToExclusive) { return false } if filters.CotacolMinPoints != nil || filters.CotacolMaxPoints != nil { cotacol, ready := item.CotacolScore() if !ready || item.CotacolAlgorithmVersion() != ride.CotacolAlgorithmVersion || item.DistanceM <= 0 { return false } if filters.CotacolMinPoints != nil && cotacol < float64(*filters.CotacolMinPoints) { return false } if filters.CotacolMaxPoints != nil && cotacol > float64(*filters.CotacolMaxPoints) { return false } } switch filters.Route { case rideRouteFilterAvailable: return item.GPXPath != "" case rideRouteFilterMissing: return item.GPXPath == "" default: return true } }