summaryrefslogtreecommitdiff
path: root/web/ride-filters.go
diff options
context:
space:
mode:
Diffstat (limited to 'web/ride-filters.go')
-rw-r--r--web/ride-filters.go307
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
+ }
+}