Files
golifehk/datasources/mtr/bus/QueryResult.go
2026-03-10 15:18:34 +08:00

317 lines
7.0 KiB
Go

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
}