penguin/golifehk

@golifehk bot in Telegram

commit 6e94adbed4f942fa9c9c54ca1f6b066d53355013

author斟酌 鵬兄 <tgckpg@gmail.com>
date2025-12-16T03:10:27Z
subjectHandle new special route listing for mtr buses
commit 6e94adbed4f942fa9c9c54ca1f6b066d53355013
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date:   2025-12-16T03:10:27Z

    Handle new special route listing for mtr buses
---
 datasources/mtr/bus/BusStop.go     |  87 ++++++-------
 datasources/mtr/bus/busschedule.go | 244 ++++++++++++++++++-------------------
 datasources/mtr/bus/busstops.go    | 232 ++++++++++++++++++-----------------
 3 files changed, 287 insertions(+), 276 deletions(-)

diff --git a/datasources/mtr/bus/BusStop.go b/datasources/mtr/bus/BusStop.go
index cd8a400..8e6ea31 100644
--- a/datasources/mtr/bus/BusStop.go
+++ b/datasources/mtr/bus/BusStop.go
@@ -1,62 +1,63 @@
 package bus
 
 import (
-    i18n "github.com/tgckpg/golifehk/i18n"
+	i18n "github.com/tgckpg/golifehk/i18n"
 )
 
 type BusStop struct {
-    RouteId string
-    Direction string
-    StationSeq int
-    StationId string
-    Latitude float64
-    Longtitude float64
-    Name_zh string
-    Name_en string
-
-    // RouteStops[ StationSeq ] = BusStop
-    RouteStops *map[int] *BusStop
-
-    i18n.Generics
+	RouteId     string
+	ReferenceId string
+	Direction   string
+	StationSeq  int
+	StationId   string
+	Latitude    float64
+	Longtitude  float64
+	Name_zh     string
+	Name_en     string
+
+	// RouteStops[ StationSeq ] = BusStop
+	RouteStops *map[int]*BusStop
+
+	i18n.Generics
 }
 
-func ( this *BusStop ) PrevStop() *BusStop {
-    if v, hasKey := (*this.RouteStops)[ this.StationSeq - 1 ]; hasKey {
-        return v
-    }
-    return nil
+func (this *BusStop) PrevStop() *BusStop {
+	if v, hasKey := (*this.RouteStops)[this.StationSeq-1]; hasKey {
+		return v
+	}
+	return nil
 }
 
-func ( this *BusStop ) NextStop() *BusStop {
-    if v, hasKey := (*this.RouteStops)[ this.StationSeq + 1 ]; hasKey {
-        return v
-    }
-    return nil
+func (this *BusStop) NextStop() *BusStop {
+	if v, hasKey := (*this.RouteStops)[this.StationSeq+1]; hasKey {
+		return v
+	}
+	return nil
 }
 
-func ( this *BusStop ) Reload() {
-    i18n_Name := map[string] string{}
-    i18n_Name["en"] = this.Name_en
-    i18n_Name["zh-Hant"] = this.Name_zh
+func (this *BusStop) Reload() {
+	i18n_Name := map[string]string{}
+	i18n_Name["en"] = this.Name_en
+	i18n_Name["zh-Hant"] = this.Name_zh
 
-    searchData := [] *string{}
-    searchData = append( searchData, &this.Name_en )
-    searchData = append( searchData, &this.Name_zh )
+	searchData := []*string{}
+	searchData = append(searchData, &this.Name_en)
+	searchData = append(searchData, &this.Name_zh)
 
-    this.Name = &i18n_Name
-    this.Key = &this.RouteId
-    this.SearchData = &searchData
+	this.Name = &i18n_Name
+	this.Key = &this.RouteId
+	this.SearchData = &searchData
 }
 
-type ByRoute [] *BusStop
+type ByRoute []*BusStop
 
-func (a ByRoute) Len() int           { return len(a) }
-func (a ByRoute) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
+func (a ByRoute) Len() int      { return len(a) }
+func (a ByRoute) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
 func (a ByRoute) Less(i, j int) bool {
-    _a := *a[i]
-    _b := *a[j]
-    if _a.RouteId == _b.RouteId {
-        return _a.Direction < _b.Direction
-    }
-    return _a.RouteId < _b.RouteId
+	_a := *a[i]
+	_b := *a[j]
+	if _a.RouteId == _b.RouteId {
+		return _a.Direction < _b.Direction
+	}
+	return _a.RouteId < _b.RouteId
 }
diff --git a/datasources/mtr/bus/busschedule.go b/datasources/mtr/bus/busschedule.go
index 12f3704..cdd3d58 100644
--- a/datasources/mtr/bus/busschedule.go
+++ b/datasources/mtr/bus/busschedule.go
@@ -1,146 +1,146 @@
 package bus
 
 import (
-    "fmt"
-    "bytes"
-    "encoding/json"
-    "io"
-    "log"
-    "net/http"
-    "path/filepath"
-    "time"
-
-    "github.com/tgckpg/golifehk/utils"
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"log"
+	"net/http"
+	"path/filepath"
+	"time"
+
+	"github.com/tgckpg/golifehk/utils"
 )
 
 type Location struct {
-    latitude float32
-    longitude float32
+	latitude  float32
+	longitude float32
 }
 
 type Bus struct {
-    ETA int `json:"arrivalTimeInSecond,string"`
-    ETAText string `json:"arrivalTimeText"`
-    BusId string `json:"busId"`
-    BusLocation Location `json:"busLocation"`
-    ETD string `json:"departureTimeInSecond"`
-    ETDText string `json:"departureTimeText"`
-    Delayed string `json:"isDelayed"`
-    Scheduled string `json:"isScheduled"`
-    LineRef string `json:"lineRef"`
+	ETA         int      `json:"arrivalTimeInSecond,string"`
+	ETAText     string   `json:"arrivalTimeText"`
+	BusId       string   `json:"busId"`
+	BusLocation Location `json:"busLocation"`
+	ETD         string   `json:"departureTimeInSecond"`
+	ETDText     string   `json:"departureTimeText"`
+	Delayed     string   `json:"isDelayed"`
+	Scheduled   string   `json:"isScheduled"`
+	LineRef     string   `json:"lineRef"`
 }
 
 type BusStopBuses struct {
-    Buses [] Bus `json:"bus"`
-    BusStopId string `json:"busStopId"`
-    Suspended string `json:"isSuspended"`
+	Buses     []Bus  `json:"bus"`
+	BusStopId string `json:"busStopId"`
+	Suspended string `json:"isSuspended"`
 }
 
 type ScheduleStatusTime struct {
-    time.Time
+	time.Time
 }
 
 type BusSchedule struct {
-    RefreshTime int `json:"appRefreshTimeInSecond,string"`
-    BusStops [] BusStopBuses `json:"busStop"`
-    StatusTime ScheduleStatusTime `json:"routeStatusTime"`
-    RemarksTitle string `json:"routeStatusRemarkTitle"`
-    // 0   = OK
-    // 100 = Rate limit ( Unconfirmed. Not in spec )
-    Status int `json:"status,string"`
+	RefreshTime  int                `json:"appRefreshTimeInSecond,string"`
+	BusStops     []BusStopBuses     `json:"busStop"`
+	StatusTime   ScheduleStatusTime `json:"routeStatusTime"`
+	RemarksTitle string             `json:"routeStatusRemarkTitle"`
+	// 0   = OK
+	// 100 = Rate limit ( Unconfirmed. Not in spec )
+	Status int `json:"status,string"`
 }
 
 func (t *ScheduleStatusTime) UnmarshalJSON(b []byte) (err error) {
-    date, err := time.Parse(`"2006\/01\/02 15:04"`, string(b))
-    if err != nil {
-        return err
-    }
-    t.Time = date
-    return
+	date, err := time.Parse(`"2006\/01\/02 15:04"`, string(b))
+	if err != nil {
+		return err
+	}
+	t.Time = date
+	return
 }
 
-func getSchedule( lang string, routeName string ) ( *BusSchedule, error ) {
-
-    CACHE_PATH := filepath.Join(
-        utils.WORKDIR,
-        "mtr_bsch" + "-" + lang + "-" + routeName + ".json",
-    )
-
-    postLang := "en"
-    if lang == "zh-Hant" {
-        postLang = "zh"
-    }
-
-    QUERY_FUNC := func() ( io.ReadCloser, error ) {
-        // Query Remote
-        values := map[string]string { "language": postLang, "routeName": routeName }
-        jsonValue, _ := json.Marshal(values)
-        resp, err := http.Post(
-            "https://rt.data.gov.hk/v1/transport/mtr/bus/getSchedule",
-            "application/json",
-            bytes.NewBuffer( jsonValue ),
-        )
-
-        if err != nil {
-            return nil, err
-        }
-
-        return resp.Body, nil
-    }
-
-    cs, err := utils.CacheStreamEx( CACHE_PATH, QUERY_FUNC )
-    if err != nil {
-        return nil, err
-    }
-
-    oldSch := BusSchedule{
-        Status: -1,
-    }
-
-    if cs.Local != nil {
-        err = json.Unmarshal( cs.Local.Bytes(), &oldSch )
-        if err != nil {
-            return nil, err
-        }
-    }
-
-    newSch := BusSchedule{
-        Status: -1,
-    }
-
-    for i := 0; i < 3; i ++ {
-
-        if cs.Remote != nil {
-            err = json.Unmarshal( cs.Remote.Bytes(), &newSch )
-            if err != nil {
-                return nil, err
-            }
-        }
-
-        if newSch.Status == 0 {
-            cs.Commit()
-            return &newSch, nil
-        }
-
-        if oldSch.Status == 0 {
-            if cs.NotExpired( 60 ) || oldSch.StatusTime.Time == newSch.StatusTime.Time {
-                log.Printf( "Using cache: %s", CACHE_PATH )
-                return &oldSch, nil
-            }
-        }
-
-        // First time + try again i times
-        err = cs.Reload()
-        log.Printf( "Reloading (%d): %s", i, CACHE_PATH )
-        if err != nil {
-            err = fmt.Errorf( "Error retrieving data: %s", err )
-            return nil, err
-        }
-    }
-
-    if newSch.Status != 0 {
-        err = fmt.Errorf( "%s (%d)", newSch.RemarksTitle, newSch.Status )
-    }
-
-    return &newSch, err
+func getSchedule(lang string, routeName string) (*BusSchedule, error) {
+
+	CACHE_PATH := filepath.Join(
+		utils.WORKDIR,
+		"mtr_bsch"+"-"+lang+"-"+routeName+".json",
+	)
+
+	postLang := "en"
+	if lang == "zh-Hant" {
+		postLang = "zh"
+	}
+
+	QUERY_FUNC := func() (io.ReadCloser, error) {
+		// Query Remote
+		values := map[string]string{"language": postLang, "routeName": routeName}
+		jsonValue, _ := json.Marshal(values)
+		resp, err := http.Post(
+			"https://rt.data.gov.hk/v1/transport/mtr/bus/getSchedule",
+			"application/json",
+			bytes.NewBuffer(jsonValue),
+		)
+
+		if err != nil {
+			return nil, err
+		}
+
+		return resp.Body, nil
+	}
+
+	cs, err := utils.CacheStreamEx(CACHE_PATH, QUERY_FUNC)
+	if err != nil {
+		return nil, err
+	}
+
+	oldSch := BusSchedule{
+		Status: -1,
+	}
+
+	if cs.Local != nil {
+		err = json.Unmarshal(cs.Local.Bytes(), &oldSch)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	newSch := BusSchedule{
+		Status: -1,
+	}
+
+	for i := 0; i < 3; i++ {
+
+		if cs.Remote != nil {
+			err = json.Unmarshal(cs.Remote.Bytes(), &newSch)
+			if err != nil {
+				return nil, err
+			}
+		}
+
+		if newSch.Status == 0 {
+			cs.Commit()
+			return &newSch, nil
+		}
+
+		if oldSch.Status == 0 {
+			if cs.NotExpired(60) || oldSch.StatusTime.Time == newSch.StatusTime.Time {
+				log.Printf("Using cache: %s", CACHE_PATH)
+				return &oldSch, nil
+			}
+		}
+
+		// First time + try again i times
+		err = cs.Reload()
+		log.Printf("Reloading (%d): %s", i, CACHE_PATH)
+		if err != nil {
+			err = fmt.Errorf("Error retrieving data: %s", err)
+			return nil, err
+		}
+	}
+
+	if newSch.Status != 0 {
+		err = fmt.Errorf("%s (%d)", newSch.RemarksTitle, newSch.Status)
+	}
+
+	return &newSch, err
 }
diff --git a/datasources/mtr/bus/busstops.go b/datasources/mtr/bus/busstops.go
index f3fa7d8..aa0410b 100644
--- a/datasources/mtr/bus/busstops.go
+++ b/datasources/mtr/bus/busstops.go
@@ -1,120 +1,130 @@
 package bus
 
 import (
-    "encoding/csv"
-    "fmt"
-    "io"
-    "net/http"
-    "path/filepath"
-    "strconv"
-
-    query "github.com/tgckpg/golifehk/query"
-    "github.com/tgckpg/golifehk/utils"
+	"encoding/csv"
+	"fmt"
+	"io"
+	"log"
+	"net/http"
+	"path/filepath"
+	"strconv"
+
+	query "github.com/tgckpg/golifehk/query"
+	"github.com/tgckpg/golifehk/utils"
 )
 
-var CSV_BUSSTOPS string = filepath.Join( utils.WORKDIR, "mtr_bus_stops.csv" )
-
-func readBusStopData( r io.Reader ) ( *map[string] *BusStop, error ) {
-
-    reader := csv.NewReader( r )
-    entries, err := reader.ReadAll()
-    if err != nil {
-        return nil, err
-    }
-
-    busStops := map[string] *BusStop{}
-    routeStops := map[string] map[string] map[int] *BusStop{}
-
-    var headers []string
-    for i, line := range entries {
-        if i == 0 {
-            headers = line
-            continue
-        }
-
-        var entry BusStop
-
-        for j, value := range line {
-            switch headers[j] {
-                case "ROUTE_ID":
-                    entry.RouteId = value
-                case "DIRECTION":
-                    entry.Direction = value
-                case "STATION_SEQNO":
-                    v, _ := strconv.ParseFloat( value, 64 )
-                    entry.StationSeq = int( v )
-                case "STATION_ID":
-                    entry.StationId = value
-                case "STATION_LATITUDE":
-                    v, _ := strconv.ParseFloat( value, 64 )
-                    entry.Latitude = v
-                case "STATION_LONGITUDE":
-                    v, _ := strconv.ParseFloat( value, 64 )
-                    entry.Longtitude = v
-                case "STATION_NAME_CHI":
-                    entry.Name_zh = value
-                case "STATION_NAME_ENG":
-                    entry.Name_en = value
-                default:
-                    return nil, fmt.Errorf( "Unknown header \"%s\"", headers[j] )
-            }
-        }
-
-        if busStops[ entry.StationId ] != nil {
-            return nil, fmt.Errorf( "Duplicated entry %+v", entry )
-        }
-
-        routeDir, hasKey := routeStops[ entry.RouteId ]
-        if !hasKey {
-            routeStops[ entry.RouteId ] = map[string] map[int] *BusStop{}
-            routeDir = routeStops[ entry.RouteId ]
-        }
-
-        route, hasKey := routeDir[ entry.Direction ]
-        if !hasKey {
-            routeDir[ entry.Direction ] = map[int] *BusStop{}
-            route = routeDir[ entry.Direction ]
-        }
-
-        _, hasKey = route[ entry.StationSeq ]
-        if !hasKey {
-            route[ entry.StationSeq ] = &entry
-        }
-
-        entry.RouteStops = &route
-        entry.Reload()
-
-        busStops[ entry.StationId ] = &entry
-    }
-    return &busStops, nil
+var CSV_BUSSTOPS string = filepath.Join(utils.WORKDIR, "mtr_bus_stops.csv")
+
+func readBusStopData(r io.Reader) (*map[string]*BusStop, error) {
+
+	reader := csv.NewReader(r)
+	entries, err := reader.ReadAll()
+	if err != nil {
+		return nil, err
+	}
+
+	busStops := map[string]*BusStop{}
+	routeStops := map[string]map[string]map[int]*BusStop{}
+
+	var headers []string
+	for i, line := range entries {
+		if i == 0 {
+			headers = line
+			continue
+		}
+
+		var entry BusStop
+
+		for j, value := range line {
+			switch headers[j] {
+			case "ROUTE_ID":
+				entry.RouteId = value
+			case "DIRECTION":
+				entry.Direction = value
+			case "STATION_SEQNO":
+				v, _ := strconv.ParseFloat(value, 64)
+				entry.StationSeq = int(v)
+			case "STATION_ID":
+				entry.StationId = value
+			case "STATION_LATITUDE":
+				v, _ := strconv.ParseFloat(value, 64)
+				entry.Latitude = v
+			case "STATION_LONGITUDE":
+				v, _ := strconv.ParseFloat(value, 64)
+				entry.Longtitude = v
+			case "STATION_NAME_CHI":
+				entry.Name_zh = value
+			case "STATION_NAME_ENG":
+				entry.Name_en = value
+			case "REFERENCE_ID":
+				entry.ReferenceId = value
+			default:
+				return nil, fmt.Errorf("Unknown header \"%s\"", headers[j])
+			}
+		}
+
+		// Ignoring special route for now as getSchedule does not reflect
+		// which bus is a special route
+		if entry.ReferenceId != entry.RouteId {
+			log.Printf("Ignoring special Route: %s", entry.ReferenceId)
+			continue
+		}
+
+		if busStops[entry.StationId] != nil {
+			return nil, fmt.Errorf("Duplicated entry %+v", entry)
+		}
+
+		routeDir, hasKey := routeStops[entry.RouteId]
+		if !hasKey {
+			routeStops[entry.RouteId] = map[string]map[int]*BusStop{}
+			routeDir = routeStops[entry.RouteId]
+		}
+
+		route, hasKey := routeDir[entry.Direction]
+		if !hasKey {
+			routeDir[entry.Direction] = map[int]*BusStop{}
+			route = routeDir[entry.Direction]
+		}
+
+		_, hasKey = route[entry.StationSeq]
+		if !hasKey {
+			route[entry.StationSeq] = &entry
+		}
+
+		entry.RouteStops = &route
+		entry.Reload()
+
+		busStops[entry.StationId] = &entry
+	}
+	return &busStops, nil
 }
 
-func getBusStops() (*[] query.ISearchable, error) {
-
-    QUERY_FUNC := func() ( io.ReadCloser, error ) {
-        resp, err := http.Get( "https://opendata.mtr.com.hk/data/mtr_bus_stops.csv" )
-        if err != nil {
-            return nil, err
-        }
-        return resp.Body, nil
-    }
-
-    buff, err := utils.CacheStream( CSV_BUSSTOPS, QUERY_FUNC, 7 * 24 * 3600 )
-    if err != nil {
-        return nil, err
-    }
-
-    utils.ReadBOM( buff )
-    busStopMap, err := readBusStopData( buff )
-    if err != nil {
-        return nil, err
-    }
-
-    searchables := [] query.ISearchable{}
-    for _, busStop := range *busStopMap {
-        searchables = append( searchables, busStop )
-    }
-
-    return &searchables, nil
+func getBusStops() (*[]query.ISearchable, error) {
+
+	QUERY_FUNC := func() (io.ReadCloser, error) {
+		resp, err := http.Get("https://opendata.mtr.com.hk/data/mtr_bus_stops.csv")
+		if err != nil {
+			return nil, err
+		}
+		return resp.Body, nil
+	}
+
+	buff, err := utils.CacheStream(CSV_BUSSTOPS, QUERY_FUNC, 7*24*3600)
+	if err != nil {
+		return nil, err
+	}
+
+	utils.ReadBOM(buff)
+	busStopMap, err := readBusStopData(buff)
+	if err != nil {
+		return nil, err
+	}
+
+	searchables := []query.ISearchable{}
+	for _, busStop := range *busStopMap {
+		searchables = append(searchables, busStop)
+	}
+
+	return &searchables, nil
 
 }