penguin/golifehk

@golifehk bot in Telegram

datasources/mtr/bus/QueryResult.go

raw · 7157 bytes

package bus

import (
	"fmt"
	"sort"
	"strconv"
	"strings"

	i18n "github.com/tgckpg/golifehk/i18n"
	query "github.com/tgckpg/golifehk/query"
	utils "github.com/tgckpg/golifehk/utils"
)

type QueryResult struct {
	Schedules *map[*BusStop]*BusStopBuses
	Lang      string
	Error     error
	Source    *query.QueryMessage
	Query     *query.QueryObject

	isConsumed bool
	dataType   string
	tableData  [][]query.TableCell

	FallbackNearest int
	NearestRange    float64
}

func writeShortRoute(lang *string, sb *strings.Builder, b *BusStop) {
	if b.PrevStop() != nil {
		utils.WriteMDv2Text(sb, (*b.PrevStop().Name)[*lang])
		sb.WriteString(" \\> ")
	}

	sb.WriteString("*")
	utils.WriteMDv2Text(sb, (*b.Name)[*lang])
	sb.WriteString("*")

	if b.NextStop() != nil {
		sb.WriteString(" \\> ")
		utils.WriteMDv2Text(sb, (*b.NextStop().Name)[*lang])
	}

	sb.WriteString("\n")
}

func (this QueryResult) DataType() string                  { return this.dataType }
func (this QueryResult) Consumed() bool                    { return this.isConsumed }
func (this QueryResult) GetTableData() [][]query.TableCell { return this.tableData }

func (this *QueryResult) Message() (string, error) {

	this.dataType = "PlainText"

	if this.Error != nil {
		return "", this.Error
	}

	langPack, err := i18n.LoadKeys(this.Lang)
	if err != nil {
		return "", err
	}

	sb := strings.Builder{}

	if this.Schedules == nil {

		q := *this.Query

		if len(*q.Results) == 0 {
			terms := make([]string, len(*q.SearchTerms), len(*q.SearchTerms))
			for i, term := range *q.SearchTerms {
				terms[i] = term.Org
			}
			return "", fmt.Errorf("Not Found: \"%s\"", strings.Join(terms, "\", \""))
		}

		if q.Key == "" {

			loc := q.Message.Location
			if loc != nil {

				this.dataType = "Table"

				table := [][]query.TableCell{}

				// Group by Station Name first
				bGroups := map[string]*[]*BusStop{}

				for _, entry := range *q.Results {
					busStop := any(entry).(*BusStop)
					bName := (*busStop.Name)[this.Lang]
					bGroup, ok := bGroups[bName]
					if !ok {
						bGroup = &[]*BusStop{}
						bGroups[bName] = bGroup
					}
					*bGroup = append(*bGroup, busStop)
				}

				for bName, bGroup := range bGroups {

					row := []query.TableCell{
						query.TableCell{Name: bName, Value: bName},
					}
					gRow := row

					var minDist float64
					var maxDist float64

					for colIndex, busStop := range *bGroup {
						if colIndex%6 == 0 {
							table = append(table, row)
							row = []query.TableCell{}
						}

						sb_i := strings.Builder{}
						sb_i.WriteString(busStop.RouteId)
						d := busStop.Direction
						if d == "O" {
							sb_i.WriteString("↑")
						} else if d == "I" {
							sb_i.WriteString("↓")
						} else {
							sb_i.WriteString("\\?")
						}

						cell := query.TableCell{
							Name:  sb_i.String(),
							Value: fmt.Sprintf("%s %s", busStop.RouteId, bName),
						}

						// Data are already sorted by shortest dist
						// So the first one must be min dist
						if minDist == 0 {
							minDist = busStop.Dist(loc.Lat(), loc.Lon())
						}

						if colIndex+1 == len(*bGroup) {
							maxDist = busStop.Dist(loc.Lat(), loc.Lon())
						}

						row = append(row, cell)
					}

					if minDist == maxDist {
						gRow[0].Name = fmt.Sprintf("%s (%s)", bName, i18n.FormatDistance(langPack, minDist))
					} else {
						gRow[0].Name = fmt.Sprintf(
							"%s (%s~%s)",
							bName, i18n.FormatDistance(langPack, minDist),
							bName, i18n.FormatDistance(langPack, maxDist),
						)
					}

					rangeText := i18n.FormatDistance(langPack, this.NearestRange)
					if maxDist < this.NearestRange {
						utils.WriteMDv2Text(&sb, i18n.DS_MTR_NEAREST_STOPS.Text(langPack, rangeText))
					} else if this.NearestRange < minDist {
						utils.WriteMDv2Text(&sb, i18n.DS_MTR_NO_NEAREST_STOPS.Text(langPack, rangeText, this.FallbackNearest))
					}

					table = append(table, row)
				}

				this.tableData = table
			} else {
				sort.Sort(query.ByKey(*q.Results))
				for _, entry := range *q.Results {
					busStop := any(entry).(*BusStop)
					utils.WriteMDv2Text(&sb, busStop.RouteId)
					sb.WriteString(" ")
					writeShortRoute(&this.Lang, &sb, busStop)
				}
			}
		} else if 1 == len(*q.SearchTerms) {

			// Route listing
			busLines := map[string]map[string]*BusStop{}

			for _, entry := range *q.Results {

				busStop := any(entry).(*BusStop)
				busRoutes := busLines[busStop.ReferenceId]

				if busRoutes == nil {
					busRoutes = make(map[string]*BusStop)
					busLines[busStop.ReferenceId] = busRoutes
				}

				if _, ok := busRoutes[busStop.Direction]; ok {
					continue
				}

				busRoutes[busStop.Direction] = busStop

				for busRoutes[busStop.Direction].PrevStop() != nil {
					busRoutes[busStop.Direction] = busRoutes[busStop.Direction].PrevStop()
				}
			}

			// Sort Bus Lines
			lineKeys := make([]string, 0, len(busLines))
			for k := range busLines {
				lineKeys = append(lineKeys, k)
			}

			sort.Slice(lineKeys, func(i, j int) bool {
				ai, an := splitKey(lineKeys[i])
				bi, bn := splitKey(lineKeys[j])

				// different base → normal string compare
				if ai != bi {
					return ai < bi
				}

				// same base: base key comes first
				if an == -1 && bn != -1 {
					return true
				}
				if an != -1 && bn == -1 {
					return false
				}

				// both have suffix → numeric compare
				return an < bn
			})

			for _, lineKey := range lineKeys {
				busRoutes := busLines[lineKey]

				// Sort route directions
				dirKeys := make([]string, 0, len(busRoutes))
				for k := range busRoutes {
					dirKeys = append(dirKeys, k)
				}

				sort.Strings(dirKeys)

				for _, d := range dirKeys {
					b := busRoutes[d]

					utils.WriteMDv2Text(&sb, lineKey)

					if d == "O" {
						sb.WriteString("↑")
					} else if d == "I" {
						sb.WriteString("↓")
					} else {
						sb.WriteString("\\?")
					}
					sb.WriteString("\n ")

					for {
						utils.WriteMDv2Text(&sb, (*b.Name)[this.Lang])
						b = b.NextStop()
						if b == nil {
							break
						}

						sb.WriteString(" \\> ")
					}
					sb.WriteString("\n")
				}
			}

		} else {
			return "", fmt.Errorf("%s", "Unreachable condition occured!?")
		}
	} else {
		if 0 < len(*this.Schedules) {

			busStops := []*BusStop{}

			for b, _ := range *this.Schedules {
				busStops = append(busStops, b)
			}

			sort.Sort(ByRoute(busStops))

			for _, busStop := range busStops {
				buses := (*this.Schedules)[busStop]

				writeShortRoute(&this.Lang, &sb, busStop)
				for _, bus := range buses.Buses {
					sb.WriteString("  \\* ")
					if bus.ETAText == "" {
						utils.WriteMDv2Text(&sb, bus.ETDText)
					} else {
						utils.WriteMDv2Text(&sb, bus.ETAText)
					}
					sb.WriteString("\n")
				}
				sb.WriteString("\n")
			}
		} else {
			utils.WriteMDv2Text(&sb, i18n.DS_MTR_NO_SCHEDULES.Text(langPack))
		}
	}

	return sb.String(), nil
}

func splitKey(s string) (base string, num int) {
	parts := strings.SplitN(s, "-", 2)
	if len(parts) == 1 {
		return s, -1 // base key, no suffix
	}

	n, err := strconv.Atoi(parts[1])
	if err != nil {
		return s, -1 // fallback: treat as base
	}

	return parts[0], n
}