diff options
Diffstat (limited to 'web/ride-filters.go')
| -rw-r--r-- | web/ride-filters.go | 307 |
1 files changed, 307 insertions, 0 deletions
diff --git a/web/ride-filters.go b/web/ride-filters.go new file mode 100644 index 0000000..189a72e --- /dev/null +++ b/web/ride-filters.go @@ -0,0 +1,307 @@ +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 + } +} |