Added kmb and refactored query.Parse
This commit is contained in:
parent
292665c49b
commit
3376d9eb96
40
datasources/kmb/BusStop.go
Normal file
40
datasources/kmb/BusStop.go
Normal file
@ -0,0 +1,40 @@
|
||||
package kmb
|
||||
|
||||
import (
|
||||
i18n "github.com/tgckpg/golifehk/i18n"
|
||||
)
|
||||
|
||||
type BusStop struct {
|
||||
BusStopId string `json:"stop"`
|
||||
Latitude float64 `json:"lat,string"`
|
||||
Longtitude float64 `json:"long,string"`
|
||||
Name_en string `json:"name_en"`
|
||||
Name_tc string `json:"name_tc"`
|
||||
Name_sc string `json:"name_sc"`
|
||||
|
||||
// Routes[ Route ][ Direction ]
|
||||
Routes *[] *RouteStop
|
||||
|
||||
i18n.Generics
|
||||
}
|
||||
|
||||
type BusStops struct {
|
||||
Type string `json:"type"`
|
||||
Version string `json:"version"`
|
||||
DateCreated string `json:"generated_timestamp"`
|
||||
BusStops [] *BusStop `json:"data"`
|
||||
}
|
||||
|
||||
func ( this *BusStop ) Reload() {
|
||||
i18n_Name := map[string] string{}
|
||||
i18n_Name["en"] = this.Name_en
|
||||
i18n_Name["zh-Hant"] = this.Name_tc
|
||||
|
||||
searchData := [] *string{}
|
||||
searchData = append( searchData, &this.Name_en )
|
||||
searchData = append( searchData, &this.Name_tc )
|
||||
|
||||
this.Name = &i18n_Name
|
||||
this.Key = &this.BusStopId
|
||||
this.SearchData = &searchData
|
||||
}
|
47
datasources/kmb/QueryResult.go
Normal file
47
datasources/kmb/QueryResult.go
Normal file
@ -0,0 +1,47 @@
|
||||
package kmb
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
query "github.com/tgckpg/golifehk/query"
|
||||
utils "github.com/tgckpg/golifehk/utils"
|
||||
)
|
||||
|
||||
type QueryResult struct {
|
||||
Lang string
|
||||
Error error
|
||||
Query *query.QueryObject
|
||||
}
|
||||
|
||||
func ( this *QueryResult ) Message() ( string, error ) {
|
||||
|
||||
if this.Error != nil {
|
||||
return "", this.Error
|
||||
}
|
||||
|
||||
sb := strings.Builder{}
|
||||
|
||||
if 0 < len( *this.Query.Results ) {
|
||||
// Print Stop Name, the print the list of routes
|
||||
if this.Query.Key == "" {
|
||||
for _, item := range *this.Query.Results {
|
||||
var b *BusStop
|
||||
b = any( item ).( *BusStop )
|
||||
|
||||
if b.Routes == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
utils.WriteMDv2Text( &sb, (*b.Name)[ this.Lang ] )
|
||||
sb.WriteString( "\n " )
|
||||
for _, route := range *b.Routes {
|
||||
utils.WriteMDv2Text( &sb, route.RouteId )
|
||||
sb.WriteString( " " )
|
||||
}
|
||||
sb.WriteString( "\n" )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String(), nil
|
||||
}
|
44
datasources/kmb/RouteStop.go
Normal file
44
datasources/kmb/RouteStop.go
Normal file
@ -0,0 +1,44 @@
|
||||
package kmb
|
||||
|
||||
import (
|
||||
query "github.com/tgckpg/golifehk/query"
|
||||
)
|
||||
|
||||
type RouteStop struct {
|
||||
BusStop *BusStop
|
||||
RouteId string `json:"route"`
|
||||
ServiceType string `json:"service_type"`
|
||||
Direction string `json:"bound"`
|
||||
StationSeq int `json:"seq,string"`
|
||||
StationId string `json:"stop"`
|
||||
|
||||
RouteStops *map[int] *RouteStop
|
||||
query.Searchable
|
||||
}
|
||||
|
||||
type RouteStops struct {
|
||||
Type string `json:"type"`
|
||||
Version string `json:"version"`
|
||||
DateCreated string `json:"generated_timestamp"`
|
||||
RouteStops [] *RouteStop `json:"data"`
|
||||
}
|
||||
|
||||
func ( routeStop RouteStop ) PrevStop() *RouteStop {
|
||||
if v, hasKey := (*routeStop.RouteStops)[ routeStop.StationSeq - 1 ]; hasKey {
|
||||
return v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ( routeStop RouteStop ) NextStop() *RouteStop {
|
||||
if v, hasKey := (*routeStop.RouteStops)[ routeStop.StationSeq + 1 ]; hasKey {
|
||||
return v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ByRouteId []RouteStop
|
||||
|
||||
func (a ByRouteId) Len() int { return len(a) }
|
||||
func (a ByRouteId) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByRouteId) Less(i, j int) bool { return a[i].RouteId < a[j].RouteId }
|
32
datasources/kmb/query.go
Normal file
32
datasources/kmb/query.go
Normal file
@ -0,0 +1,32 @@
|
||||
package kmb
|
||||
|
||||
import (
|
||||
"strings"
|
||||
query "github.com/tgckpg/golifehk/query"
|
||||
)
|
||||
|
||||
func Query( lang string, message string ) query.IQueryResult {
|
||||
|
||||
var qo *query.QueryObject
|
||||
var err error
|
||||
|
||||
qr := QueryResult{ Lang: lang }
|
||||
busStops, err := getBusStops()
|
||||
if err != nil {
|
||||
qr.Error = err
|
||||
goto qrReturn
|
||||
}
|
||||
|
||||
qo, err = query.Parse( strings.ToUpper( message ), busStops )
|
||||
if err != nil {
|
||||
qr.Error = err
|
||||
goto qrReturn
|
||||
}
|
||||
|
||||
qr.Query = qo
|
||||
|
||||
qrReturn:
|
||||
var iqr query.IQueryResult
|
||||
iqr = &qr
|
||||
return iqr
|
||||
}
|
10
datasources/kmb/query_test.go
Normal file
10
datasources/kmb/query_test.go
Normal file
@ -0,0 +1,10 @@
|
||||
package kmb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestQuerySchedule( t *testing.T ) {
|
||||
fmt.Print( Query( "zh", "大欖" ).Message() )
|
||||
}
|
143
datasources/kmb/routestops.go
Normal file
143
datasources/kmb/routestops.go
Normal file
@ -0,0 +1,143 @@
|
||||
package kmb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
// "strings"
|
||||
|
||||
query "github.com/tgckpg/golifehk/query"
|
||||
"github.com/tgckpg/golifehk/utils"
|
||||
)
|
||||
|
||||
var JSON_ROUTESTOPS string = filepath.Join( utils.WORKDIR, "kmb-routestops.json" )
|
||||
var JSON_BUSSTOPS string = filepath.Join( utils.WORKDIR, "kmb-busstops.json" )
|
||||
|
||||
func readRouteStopsData( busStops *map[string] *BusStop, buff *bytes.Buffer ) error {
|
||||
|
||||
routeStopsData := RouteStops{}
|
||||
err := json.Unmarshal( buff.Bytes(), &routeStopsData )
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// routeStops[ Route ][ ServiceType ][ Direction ][ Seq ] = RouteStop
|
||||
routeStops := map[string] *map[string] *map[ string ] *map[ int ] *RouteStop{}
|
||||
for _, entry := range routeStopsData.RouteStops {
|
||||
|
||||
busStop := (*busStops)[ entry.StationId ]
|
||||
if busStop == nil {
|
||||
busStop = &BusStop {
|
||||
BusStopId: entry.StationId,
|
||||
Name_en: "???", Name_tc: "???", Name_sc: "???",
|
||||
}
|
||||
busStop.Reload()
|
||||
|
||||
(*busStops)[ entry.StationId ] = busStop
|
||||
}
|
||||
|
||||
if busStop.Routes == nil {
|
||||
busStopRoutes := [] *RouteStop{}
|
||||
busStop.Routes = &busStopRoutes
|
||||
}
|
||||
|
||||
(*busStop.Routes) = append( (*busStop.Routes), entry )
|
||||
entry.BusStop = busStop
|
||||
|
||||
route := routeStops[ entry.RouteId ]
|
||||
if route == nil {
|
||||
route = &map[string] *map[ string ] *map[ int ] *RouteStop{}
|
||||
routeStops[ entry.RouteId ] = route
|
||||
}
|
||||
|
||||
service := (*route)[ entry.ServiceType ]
|
||||
if service == nil {
|
||||
service = &map[ string ] *map[ int ] *RouteStop{}
|
||||
(*route)[ entry.ServiceType ] = service
|
||||
}
|
||||
|
||||
direction := (*service)[ entry.Direction ]
|
||||
if direction == nil {
|
||||
direction = &map[ int ] *RouteStop{}
|
||||
(*service)[ entry.Direction ] = direction
|
||||
entry.RouteStops = direction
|
||||
}
|
||||
|
||||
seq := (*direction)[ entry.StationSeq ]
|
||||
if seq == nil {
|
||||
(*direction)[ entry.StationSeq ] = entry
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readBusStopsData( buff *bytes.Buffer ) ( *map[string]*BusStop, error ) {
|
||||
busStopsData := BusStops{}
|
||||
err := json.Unmarshal( buff.Bytes(), &busStopsData )
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
busStopMap := map[string] *BusStop{}
|
||||
for _, entry := range busStopsData.BusStops {
|
||||
|
||||
entry.Reload()
|
||||
|
||||
if _, ok := busStopMap[ entry.BusStopId ]; ok {
|
||||
return nil, fmt.Errorf( "Duplicated BusStop: %s", entry.BusStopId )
|
||||
}
|
||||
|
||||
busStopMap[ entry.BusStopId ] = entry
|
||||
}
|
||||
return &busStopMap, nil
|
||||
}
|
||||
|
||||
func getBusStops() (*[] query.ISearchable, error) {
|
||||
|
||||
QUERY_FUNC := func() ( io.ReadCloser, error ) {
|
||||
resp, err := http.Get( "https://data.etabus.gov.hk/v1/transport/kmb/stop" )
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
buff, err := utils.CacheStream( JSON_BUSSTOPS, QUERY_FUNC, 7 * 24 * 3600 )
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
busStopMap, err := readBusStopsData( buff )
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
QUERY_FUNC = func() ( io.ReadCloser, error ) {
|
||||
resp, err := http.Get( "https://data.etabus.gov.hk/v1/transport/kmb/route-stop" )
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
buff, err = utils.CacheStream( JSON_ROUTESTOPS, QUERY_FUNC, 7 * 24 * 3600 )
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = readRouteStopsData( busStopMap, buff )
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
searchables := [] query.ISearchable{}
|
||||
for _, busStop := range *busStopMap {
|
||||
searchables = append( searchables, busStop )
|
||||
}
|
||||
|
||||
return &searchables, nil
|
||||
}
|
12
datasources/kmb/routestops_test.go
Normal file
12
datasources/kmb/routestops_test.go
Normal file
@ -0,0 +1,12 @@
|
||||
package kmb
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRouteStops(t *testing.T) {
|
||||
_, err := getBusStops()
|
||||
if err != nil {
|
||||
t.Error( err )
|
||||
}
|
||||
}
|
@ -1,5 +1,9 @@
|
||||
package bus
|
||||
|
||||
import (
|
||||
i18n "github.com/tgckpg/golifehk/i18n"
|
||||
)
|
||||
|
||||
type BusStop struct {
|
||||
RouteId string
|
||||
Direction string
|
||||
@ -7,27 +11,39 @@ type BusStop struct {
|
||||
StationId string
|
||||
Latitude float64
|
||||
Longtitude float64
|
||||
Name_zh string
|
||||
Name_en string
|
||||
|
||||
Name *map[string] string
|
||||
// RouteStops[ StationSeq ] = BusStop
|
||||
RouteStops *map[int] *BusStop
|
||||
|
||||
i18n.Generics
|
||||
}
|
||||
|
||||
func ( busStop BusStop ) PrevStop() *BusStop {
|
||||
if v, hasKey := (*busStop.RouteStops)[ busStop.StationSeq - 1 ]; hasKey {
|
||||
func ( this *BusStop ) PrevStop() *BusStop {
|
||||
if v, hasKey := (*this.RouteStops)[ this.StationSeq - 1 ]; hasKey {
|
||||
return v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ( busStop BusStop ) NextStop() *BusStop {
|
||||
if v, hasKey := (*busStop.RouteStops)[ busStop.StationSeq + 1 ]; hasKey {
|
||||
func ( this *BusStop ) NextStop() *BusStop {
|
||||
if v, hasKey := (*this.RouteStops)[ this.StationSeq + 1 ]; hasKey {
|
||||
return v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ByRouteId []BusStop
|
||||
func ( this *BusStop ) Reload() {
|
||||
i18n_Name := map[string] string{}
|
||||
i18n_Name["en"] = this.Name_en
|
||||
i18n_Name["zh-Hant"] = this.Name_zh
|
||||
|
||||
func (a ByRouteId) Len() int { return len(a) }
|
||||
func (a ByRouteId) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByRouteId) Less(i, j int) bool { return a[i].RouteId < a[j].RouteId }
|
||||
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
|
||||
}
|
||||
|
@ -4,68 +4,87 @@ import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
query "github.com/tgckpg/golifehk/query"
|
||||
utils "github.com/tgckpg/golifehk/utils"
|
||||
)
|
||||
|
||||
type QueryResult struct {
|
||||
Schedules *map[BusStop] *BusStopBuses
|
||||
Schedules *map[*BusStop] *BusStopBuses
|
||||
Lang string
|
||||
Error error
|
||||
Query *QueryObject
|
||||
}
|
||||
|
||||
var MARKDOWN_ESC []string = []string{"_", "*", "[", "]", "(", ")", "~", "`", ">", "#", "+", "-", "=", "|", "{", "}", ".", "!"}
|
||||
func _escape( t string ) string {
|
||||
for _, c := range MARKDOWN_ESC {
|
||||
t = strings.Replace( t, c, "\\" + c, -1 )
|
||||
}
|
||||
return t
|
||||
Query *query.QueryObject
|
||||
}
|
||||
|
||||
func writeShortRoute( lang *string, sb *strings.Builder, b *BusStop ) {
|
||||
if b.PrevStop() != nil {
|
||||
sb.WriteString( _escape( (*b.PrevStop().Name)[ *lang ] ) )
|
||||
utils.WriteMDv2Text( sb, (*b.PrevStop().Name)[ *lang ] )
|
||||
sb.WriteString( " \\> " )
|
||||
}
|
||||
|
||||
sb.WriteString( "*" )
|
||||
sb.WriteString( _escape( (*b.Name)[ *lang ] ) )
|
||||
utils.WriteMDv2Text( sb, (*b.Name)[ *lang ] )
|
||||
sb.WriteString( "*" )
|
||||
|
||||
if b.NextStop() != nil {
|
||||
sb.WriteString( " \\> " )
|
||||
sb.WriteString( _escape( (*b.NextStop().Name)[ *lang ] ) )
|
||||
utils.WriteMDv2Text( sb, (*b.NextStop().Name)[ *lang ] )
|
||||
}
|
||||
|
||||
sb.WriteString( "\n" )
|
||||
}
|
||||
|
||||
func ( result QueryResult ) Message() string {
|
||||
func ( this QueryResult ) Message() ( string, error ) {
|
||||
|
||||
if result.Error != nil {
|
||||
return fmt.Sprintf( "%s", result.Error )
|
||||
if this.Error != nil {
|
||||
return "", this.Error
|
||||
}
|
||||
|
||||
sb := strings.Builder{}
|
||||
|
||||
if result.Schedules == nil {
|
||||
if this.Schedules == nil {
|
||||
|
||||
query := *result.Query
|
||||
if query.Route == "" {
|
||||
sort.Sort( ByRouteId( *query.BusStops ) )
|
||||
for _, busStop := range *query.BusStops {
|
||||
sb.WriteString( _escape( busStop.RouteId ) )
|
||||
q := *this.Query
|
||||
if q.Key == "" {
|
||||
sort.Sort( query.ByKey( *q.Results ) )
|
||||
for _, entry := range *q.Results {
|
||||
busStop := any( entry ).( *BusStop )
|
||||
utils.WriteMDv2Text( &sb, busStop.RouteId )
|
||||
sb.WriteString( " " )
|
||||
writeShortRoute( &result.Lang, &sb, &busStop )
|
||||
writeShortRoute( &this.Lang, &sb, busStop )
|
||||
}
|
||||
} else if query.BusStops == nil && query.RouteStarts != nil {
|
||||
for d, b := range *query.RouteStarts {
|
||||
sb.WriteString( query.Route )
|
||||
} else if 1 == len( *q.SearchTerms ) && 0 < len( *q.Results ) {
|
||||
|
||||
// Route listing
|
||||
st := map[string] *BusStop{}
|
||||
keys := [] string{}
|
||||
|
||||
for _, entry := range *q.Results {
|
||||
|
||||
busStop := any( entry ).( *BusStop )
|
||||
if _, ok := st[ busStop.Direction ]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
st[ busStop.Direction ] = busStop
|
||||
keys = append( keys, busStop.Direction )
|
||||
|
||||
for st[ busStop.Direction ].PrevStop() != nil {
|
||||
st[ busStop.Direction ] = st[ busStop.Direction ].PrevStop()
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings( keys )
|
||||
|
||||
for _, d := range keys {
|
||||
b := st[ d ]
|
||||
sb.WriteString( q.Key )
|
||||
sb.WriteString( "\\-" )
|
||||
sb.WriteString( _escape( d ) )
|
||||
utils.WriteMDv2Text( &sb, d )
|
||||
sb.WriteString( "\n " )
|
||||
|
||||
for {
|
||||
sb.WriteString( _escape( (*b.Name)[ result.Lang ] ) )
|
||||
utils.WriteMDv2Text( &sb, (*b.Name)[ this.Lang ] )
|
||||
b = b.NextStop()
|
||||
if b == nil {
|
||||
break
|
||||
@ -75,28 +94,35 @@ func ( result QueryResult ) Message() string {
|
||||
}
|
||||
sb.WriteString( "\n" )
|
||||
}
|
||||
|
||||
} else if 1 < len( *q.SearchTerms ) && 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, "\", \"" ) )
|
||||
} else {
|
||||
sb.WriteString( _escape( "Unreachable condition occured!?" ) )
|
||||
return "", fmt.Errorf( "%s", "Unreachable condition occured!?" )
|
||||
}
|
||||
} else {
|
||||
if 0 < len(*result.Schedules) {
|
||||
for busStop, buses := range *result.Schedules {
|
||||
writeShortRoute( &result.Lang, &sb, &busStop )
|
||||
if 0 < len( *this.Schedules ) {
|
||||
for busStop, buses := range *this.Schedules {
|
||||
writeShortRoute( &this.Lang, &sb, busStop )
|
||||
for _, bus := range buses.Buses {
|
||||
sb.WriteString( " \\* " )
|
||||
if bus.ETAText == "" {
|
||||
sb.WriteString( _escape( bus.ETDText ) )
|
||||
utils.WriteMDv2Text( &sb, bus.ETDText )
|
||||
} else {
|
||||
sb.WriteString( _escape( bus.ETAText ) )
|
||||
utils.WriteMDv2Text( &sb, bus.ETAText )
|
||||
}
|
||||
sb.WriteString( "\n" )
|
||||
}
|
||||
sb.WriteString( "\n" )
|
||||
}
|
||||
} else {
|
||||
sb.WriteString( _escape( "Schedules are empty...perhaps Out of Service Time?" ) )
|
||||
utils.WriteMDv2Text( &sb, "Schedules are empty...perhaps Out of Service Time?" )
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
@ -45,9 +45,15 @@ func getSchedule( lang string, routeName string ) ( *BusSchedule, error ) {
|
||||
"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": lang , "routeName": routeName }
|
||||
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",
|
||||
|
@ -9,12 +9,13 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
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 ) {
|
||||
func readBusStopData( r io.Reader ) ( *map[string] *BusStop, error ) {
|
||||
|
||||
reader := csv.NewReader( r )
|
||||
entries, err := reader.ReadAll()
|
||||
@ -22,7 +23,7 @@ func readBusStopData( r io.Reader ) ( *map[string]BusStop, error ) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
busStops := map[string]BusStop{}
|
||||
busStops := map[string] *BusStop{}
|
||||
routeStops := map[string] map[string] map[int] *BusStop{}
|
||||
|
||||
var headers []string
|
||||
@ -34,7 +35,6 @@ func readBusStopData( r io.Reader ) ( *map[string]BusStop, error ) {
|
||||
}
|
||||
|
||||
var entry BusStop
|
||||
i18n_Name := map[string] string{}
|
||||
|
||||
for j, value := range line {
|
||||
switch headers[j] {
|
||||
@ -54,15 +54,15 @@ func readBusStopData( r io.Reader ) ( *map[string]BusStop, error ) {
|
||||
v, _ := strconv.ParseFloat( value, 64 )
|
||||
entry.Longtitude = v
|
||||
case "STATION_NAME_CHI":
|
||||
i18n_Name["zh"] = value
|
||||
entry.Name_zh = value
|
||||
case "STATION_NAME_ENG":
|
||||
i18n_Name["en"] = value
|
||||
entry.Name_en = value
|
||||
default:
|
||||
return nil, fmt.Errorf( "Unknown header \"%s\"", headers[j] )
|
||||
}
|
||||
}
|
||||
|
||||
if _, t := busStops[ entry.StationId ]; t {
|
||||
if busStops[ entry.StationId ] != nil {
|
||||
return nil, fmt.Errorf( "Duplicated entry %+v", entry )
|
||||
}
|
||||
|
||||
@ -83,15 +83,15 @@ func readBusStopData( r io.Reader ) ( *map[string]BusStop, error ) {
|
||||
route[ entry.StationSeq ] = &entry
|
||||
}
|
||||
|
||||
entry.Name = &i18n_Name
|
||||
entry.RouteStops = &route
|
||||
entry.Reload()
|
||||
|
||||
busStops[ entry.StationId ] = entry
|
||||
busStops[ entry.StationId ] = &entry
|
||||
}
|
||||
return &busStops, nil
|
||||
}
|
||||
|
||||
func getBusStops() (*map[string]BusStop, error) {
|
||||
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" )
|
||||
@ -106,5 +106,16 @@ func getBusStops() (*map[string]BusStop, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return readBusStopData( 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
|
||||
|
||||
}
|
||||
|
@ -1,76 +1,44 @@
|
||||
package bus
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"github.com/tgckpg/golifehk/utils"
|
||||
query "github.com/tgckpg/golifehk/query"
|
||||
)
|
||||
|
||||
type qTerm struct {
|
||||
Org string
|
||||
Value string
|
||||
ProblyRoute bool
|
||||
}
|
||||
func Query( lang string, message string ) query.IQueryResult {
|
||||
|
||||
type QueryObject struct {
|
||||
Route string
|
||||
BusStops *[]BusStop
|
||||
RouteStarts *map[string] *BusStop
|
||||
SearchTerms *[] *qTerm
|
||||
}
|
||||
|
||||
func NotFound ( terms *[] *qTerm ) string {
|
||||
sb := strings.Builder{}
|
||||
sb.WriteString( "Not Found" )
|
||||
if 0 < len( *terms ) {
|
||||
sb.WriteString( ":" )
|
||||
for _, term := range *terms {
|
||||
sb.WriteString( " \"" )
|
||||
sb.WriteString( term.Org )
|
||||
sb.WriteString( "\"" )
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func Query( lang string, message string ) *QueryResult {
|
||||
var qBusStops *query.QueryObject
|
||||
var err error
|
||||
|
||||
qr := QueryResult{ Lang: lang }
|
||||
|
||||
qo, err := parse( message )
|
||||
busStops, err := getBusStops()
|
||||
if err != nil {
|
||||
qr.Error = err
|
||||
return &qr
|
||||
goto qrReturn
|
||||
}
|
||||
|
||||
qr.Query = qo
|
||||
qBusStops, err = query.Parse( strings.ToUpper( message ), busStops )
|
||||
if err != nil {
|
||||
qr.Error = err
|
||||
goto qrReturn
|
||||
}
|
||||
|
||||
if qo.Route != "" {
|
||||
|
||||
if qo.BusStops == nil {
|
||||
return &qr
|
||||
}
|
||||
|
||||
if len( *qo.BusStops ) == 0 {
|
||||
qr.Error = errors.New( NotFound( qo.SearchTerms ) )
|
||||
return &qr
|
||||
}
|
||||
|
||||
schedules, err := getSchedule( lang, qo.Route )
|
||||
qr.Query = qBusStops
|
||||
if 0 < len( *qBusStops.Results ) && 1 < len( *qBusStops.SearchTerms ) {
|
||||
schedules, err := getSchedule( lang, qBusStops.Key )
|
||||
if err != nil {
|
||||
qr.Error = err
|
||||
return &qr
|
||||
goto qrReturn
|
||||
}
|
||||
|
||||
if len( schedules.BusStops ) == 0 {
|
||||
qr.Schedules = &map[BusStop] *BusStopBuses{}
|
||||
return &qr
|
||||
qr.Schedules = &map[*BusStop] *BusStopBuses{}
|
||||
goto qrReturn
|
||||
}
|
||||
|
||||
matches := map[BusStop] *BusStopBuses{}
|
||||
for _, busStop := range *qo.BusStops {
|
||||
matches := map[*BusStop] *BusStopBuses{}
|
||||
for _, entry := range *qBusStops.Results {
|
||||
busStop := any( entry ).( *BusStop )
|
||||
|
||||
for _, busStopSch := range schedules.BusStops {
|
||||
if busStopSch.BusStopId == busStop.StationId {
|
||||
@ -82,131 +50,10 @@ func Query( lang string, message string ) *QueryResult {
|
||||
}
|
||||
|
||||
qr.Schedules = &matches
|
||||
return &qr
|
||||
} else {
|
||||
if len( *qo.BusStops ) == 0 {
|
||||
return &QueryResult{ Error: errors.New( NotFound( qo.SearchTerms ) ) }
|
||||
}
|
||||
|
||||
return &qr
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func test( entry BusStop, val string ) bool {
|
||||
switch true {
|
||||
case strings.Contains( strings.ToUpper( (*entry.Name)["zh"] ), val ):
|
||||
fallthrough
|
||||
case strings.Contains( strings.ToUpper( (*entry.Name)["en"] ), val ):
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parse( line string ) ( *QueryObject, error ) {
|
||||
busStops, err := getBusStops()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var route string = ""
|
||||
matches := []BusStop{}
|
||||
|
||||
// Sanitize and assume properties for each of the keywords
|
||||
terms := [] *qTerm{}
|
||||
for _, val := range strings.Split( line, " " ) {
|
||||
|
||||
if val == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
term := qTerm{
|
||||
Org: val,
|
||||
Value: strings.ToUpper( strings.Trim( val, " " ) ),
|
||||
ProblyRoute: strings.ContainsAny( val, utils.ROUTE_CHARS ),
|
||||
}
|
||||
terms = append( terms, &term )
|
||||
}
|
||||
|
||||
// Search for route name first, otherwise search in other props
|
||||
for _, entry := range *busStops {
|
||||
|
||||
// Search for RouteId
|
||||
for _, term := range terms {
|
||||
|
||||
if term.ProblyRoute && term.Value == entry.RouteId {
|
||||
if route != "" && route != term.Value {
|
||||
return nil, fmt.Errorf( "Cannot %s & %s", route, term.Value )
|
||||
}
|
||||
matches = append( matches, entry )
|
||||
route = entry.RouteId
|
||||
break
|
||||
}
|
||||
|
||||
if test( entry, term.Value ) {
|
||||
matches = append( matches, entry )
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
searchTerms := [] *qTerm{}
|
||||
matches_in := []BusStop{}
|
||||
|
||||
for _, term := range terms {
|
||||
if term.ProblyRoute {
|
||||
continue
|
||||
}
|
||||
searchTerms = append( searchTerms, term )
|
||||
}
|
||||
|
||||
if 0 < len( searchTerms ) {
|
||||
// If route found, filter out all other route
|
||||
// then search the terms within that route
|
||||
for _, entry := range matches {
|
||||
if route != "" && entry.RouteId != route {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, term := range searchTerms {
|
||||
if test( entry, term.Value ) {
|
||||
matches_in = append( matches_in, entry )
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matches = matches_in
|
||||
|
||||
return &QueryObject{
|
||||
Route: route,
|
||||
BusStops: &matches,
|
||||
SearchTerms: &searchTerms,
|
||||
}, err
|
||||
|
||||
} else if route != "" {
|
||||
// Route listing
|
||||
st := map[string] *BusStop{}
|
||||
|
||||
for _, entry := range *busStops {
|
||||
if entry.RouteId != route {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := st[ entry.Direction ]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
st[ entry.Direction ] = &entry
|
||||
|
||||
for st[ entry.Direction ].PrevStop() != nil {
|
||||
st[ entry.Direction ] = st[ entry.Direction ].PrevStop()
|
||||
}
|
||||
}
|
||||
|
||||
return &QueryObject{ Route: route, RouteStarts: &st, BusStops: nil }, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf( "Cannot parse: %s", line )
|
||||
qrReturn:
|
||||
var iqr query.IQueryResult
|
||||
iqr = &qr
|
||||
return iqr
|
||||
}
|
||||
|
@ -2,11 +2,32 @@ package bus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestQuerySchedule( t *testing.T ) {
|
||||
qo := Query( "zh", "水" )
|
||||
func TestQuery( t *testing.T ) {
|
||||
qo := Query( "zh-Hant", "K73" )
|
||||
mesg, err := qo.Message()
|
||||
if err != nil {
|
||||
t.Errorf( "Unexpected Error: %s", err )
|
||||
}
|
||||
|
||||
fmt.Print( qo.Message() )
|
||||
if !strings.Contains( mesg, "K73\\-O" ) {
|
||||
t.Errorf( "Expected Route Listing, got \"%s\" instead", mesg )
|
||||
}
|
||||
|
||||
qo = Query( "zh-Hant", "K76 池" )
|
||||
mesg, err = qo.Message()
|
||||
if err == nil {
|
||||
t.Errorf( "Expecting error, got \"%s\" instead", mesg )
|
||||
}
|
||||
|
||||
qo = Query( "zh-Hant", "K73 池" )
|
||||
mesg, err = qo.Message()
|
||||
if err != nil {
|
||||
t.Errorf( "Unexpected Error: %s", err )
|
||||
}
|
||||
|
||||
fmt.Println( mesg )
|
||||
}
|
||||
|
10
i18n/Generics.go
Normal file
10
i18n/Generics.go
Normal file
@ -0,0 +1,10 @@
|
||||
package i18n
|
||||
|
||||
import (
|
||||
query "github.com/tgckpg/golifehk/query"
|
||||
)
|
||||
|
||||
type Generics struct {
|
||||
Name *map[string] string
|
||||
query.Searchable
|
||||
}
|
55
main.go
55
main.go
@ -1,9 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
mtrbus "github.com/tgckpg/golifehk/datasources/mtr/bus"
|
||||
kmb "github.com/tgckpg/golifehk/datasources/kmb"
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
@ -29,13 +31,58 @@ func main() {
|
||||
|
||||
log.Printf( "[%s] %s", update.Message.From.UserName, update.Message.Text )
|
||||
|
||||
result := mtrbus.Query( "zh", update.Message.Text )
|
||||
mesg, err := mtrbus.Query( "zh-Hant", update.Message.Text ).Message()
|
||||
|
||||
if update.Message.Chat.IsGroup() {
|
||||
if err == nil {
|
||||
msg := tgbotapi.NewMessage( update.Message.Chat.ID, mesg )
|
||||
msg.ParseMode = "MarkdownV2"
|
||||
msg.ReplyToMessageID = update.Message.MessageID
|
||||
|
||||
bot.Send( msg )
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
var msg tgbotapi.MessageConfig
|
||||
if err == nil {
|
||||
msg = tgbotapi.NewMessage( update.Message.Chat.ID, mesg )
|
||||
msg.ParseMode = "MarkdownV2"
|
||||
} else {
|
||||
msg = tgbotapi.NewMessage( update.Message.Chat.ID, fmt.Sprintf( "%s", err ) )
|
||||
}
|
||||
|
||||
if result.Error == nil || !update.Message.Chat.IsGroup() {
|
||||
msg := tgbotapi.NewMessage( update.Message.Chat.ID, result.Message() )
|
||||
msg.ParseMode = "MarkdownV2"
|
||||
msg.ReplyToMessageID = update.Message.MessageID
|
||||
bot.Send( msg )
|
||||
}
|
||||
|
||||
mesg, err2 := kmb.Query( "zh-Hant", update.Message.Text ).Message()
|
||||
|
||||
if err != nil && err2 != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
err = err2
|
||||
if update.Message.Chat.IsGroup() {
|
||||
if err == nil {
|
||||
msg := tgbotapi.NewMessage( update.Message.Chat.ID, mesg )
|
||||
msg.ParseMode = "MarkdownV2"
|
||||
msg.ReplyToMessageID = update.Message.MessageID
|
||||
|
||||
bot.Send( msg )
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
var msg tgbotapi.MessageConfig
|
||||
if err == nil {
|
||||
msg = tgbotapi.NewMessage( update.Message.Chat.ID, mesg )
|
||||
msg.ParseMode = "MarkdownV2"
|
||||
} else {
|
||||
msg = tgbotapi.NewMessage( update.Message.Chat.ID, fmt.Sprintf( "%s", err ) )
|
||||
}
|
||||
|
||||
msg.ReplyToMessageID = update.Message.MessageID
|
||||
bot.Send( msg )
|
||||
}
|
||||
}
|
||||
|
5
query/IQueryResult.go
Normal file
5
query/IQueryResult.go
Normal file
@ -0,0 +1,5 @@
|
||||
package query
|
||||
|
||||
type IQueryResult interface {
|
||||
Message() ( string, error )
|
||||
}
|
40
query/Searchable.go
Normal file
40
query/Searchable.go
Normal file
@ -0,0 +1,40 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ISearchable interface {
|
||||
Test( string ) bool
|
||||
GetKey() *string
|
||||
}
|
||||
|
||||
type Searchable struct {
|
||||
Key *string
|
||||
SearchData *[] *string
|
||||
}
|
||||
|
||||
func ( this *Searchable ) Test( val string ) bool {
|
||||
|
||||
data := this.SearchData
|
||||
if data == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, v := range *data {
|
||||
if strings.Contains( *v, val ) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ( this *Searchable ) GetKey() *string {
|
||||
return this.Key
|
||||
}
|
||||
|
||||
type ByKey [] ISearchable
|
||||
|
||||
func (a ByKey) Len() int { return len(a) }
|
||||
func (a ByKey) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByKey) Less(i, j int) bool { return *a[i].GetKey() < *a[j].GetKey() }
|
74
query/query.go
Normal file
74
query/query.go
Normal file
@ -0,0 +1,74 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type QTerm struct {
|
||||
Org string
|
||||
Value string
|
||||
IsKey bool
|
||||
}
|
||||
|
||||
type QueryObject struct {
|
||||
Key string
|
||||
SearchTerms *[] *QTerm
|
||||
Results *[] ISearchable
|
||||
}
|
||||
|
||||
func Parse( line string, entries *[] ISearchable ) ( *QueryObject, error ) {
|
||||
|
||||
var Key string = ""
|
||||
|
||||
// Sanitize and assume properties for each of the keywords
|
||||
terms := [] *QTerm{}
|
||||
for _, val := range strings.Split( line, " " ) {
|
||||
|
||||
if val == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
term := QTerm{
|
||||
Org: val,
|
||||
Value: strings.Trim( val, " " ),
|
||||
}
|
||||
terms = append( terms, &term )
|
||||
}
|
||||
|
||||
lookForKey:
|
||||
for _, term := range terms {
|
||||
for _, entry := range *entries {
|
||||
if term.Value == *entry.GetKey() {
|
||||
Key = term.Value
|
||||
term.IsKey = true
|
||||
break lookForKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matches := [] ISearchable{}
|
||||
|
||||
if Key != "" && len( terms ) == 1 {
|
||||
for _, entry := range *entries {
|
||||
if Key == *entry.GetKey() {
|
||||
matches = append( matches, entry )
|
||||
}
|
||||
}
|
||||
return &QueryObject{ Key: Key, Results: &matches, SearchTerms: &terms }, nil
|
||||
} else if 0 < len( terms ) {
|
||||
for _, entry := range *entries {
|
||||
for _, term := range terms {
|
||||
if term.IsKey {
|
||||
continue
|
||||
}
|
||||
if ( Key == "" || Key == *entry.GetKey() ) && entry.Test( term.Value ) {
|
||||
matches = append( matches, entry )
|
||||
}
|
||||
}
|
||||
}
|
||||
return &QueryObject{ Key: Key, Results: &matches, SearchTerms: &terms }, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf( "Cannot parse: %s", line )
|
||||
}
|
79
query/query_test.go
Normal file
79
query/query_test.go
Normal file
@ -0,0 +1,79 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
type TestItem struct {
|
||||
Searchable
|
||||
}
|
||||
|
||||
func TestQuery( t *testing.T ) {
|
||||
testItems := [] ISearchable{}
|
||||
|
||||
s1234 := "1234"
|
||||
sApple := "Apple"
|
||||
s0000 := "0000"
|
||||
|
||||
sDat0 := "Dat0"
|
||||
data0 := [] *string{}
|
||||
data0 = append( data0, &s1234 )
|
||||
data0 = append( data0, &sApple )
|
||||
t0 := TestItem{}
|
||||
t0.Key = &sDat0
|
||||
t0.SearchData = &data0
|
||||
testItems = append( testItems, &t0 )
|
||||
|
||||
sDat1 := "Dat1"
|
||||
data1 := [] *string{}
|
||||
data1 = append( data1, &s0000 )
|
||||
data1 = append( data1, &sApple )
|
||||
t1 := TestItem{}
|
||||
t1.Key = &sDat1
|
||||
t1.SearchData = &data1
|
||||
testItems = append( testItems, &t1 )
|
||||
|
||||
sDat2 := "Dat2"
|
||||
data2 := [] *string{}
|
||||
data2 = append( data2, &sDat0 )
|
||||
t2 := TestItem{}
|
||||
t2.Key = &sDat2
|
||||
t2.SearchData = &data2
|
||||
testItems = append( testItems, &t2 )
|
||||
|
||||
qo, err := Parse( "Apple", &testItems )
|
||||
if err != nil {
|
||||
t.Error( err )
|
||||
}
|
||||
|
||||
if len(*qo.Results) != 2 {
|
||||
t.Error( "Expected 2 results when searching for \"Apple\"" )
|
||||
}
|
||||
|
||||
qo, err = Parse( "0000", &testItems )
|
||||
if err != nil {
|
||||
t.Error( err )
|
||||
}
|
||||
|
||||
if len(*qo.Results) != 1 {
|
||||
t.Error( "Expected 1 results when searching for \"0000\"" )
|
||||
}
|
||||
|
||||
qo, err = Parse( "Dat0", &testItems )
|
||||
if err != nil {
|
||||
t.Error( err )
|
||||
}
|
||||
|
||||
if len(*qo.Results) != 1 {
|
||||
t.Error( "Expected 1 results when searching for \"Dat0\"" )
|
||||
}
|
||||
|
||||
qo, err = Parse( "Dat2 Dat0", &testItems )
|
||||
if err != nil {
|
||||
t.Error( err )
|
||||
}
|
||||
|
||||
if len(*qo.Results) != 1 {
|
||||
t.Error( "Expected 1 results when searching for \"Dat2 Dat0\"" )
|
||||
}
|
||||
}
|
@ -9,4 +9,4 @@ var WORKDIR string = TryGetEnv( "GOLIFEHK_WORKDIR", filepath.Join( os.TempDir(),
|
||||
|
||||
var BOM string = bytes.NewBuffer([]byte{ 0xEF, 0xBB, 0xBF }).String()
|
||||
|
||||
const ROUTE_CHARS string = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-"
|
||||
const KEY_CHARS string = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-"
|
||||
|
@ -3,8 +3,19 @@ package utils
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var MARKDOWN_ESC []string = []string{"_", "*", "[", "]", "(", ")", "~", "`", ">", "#", "+", "-", "=", "|", "{", "}", ".", "!"}
|
||||
|
||||
func WriteMDv2Text( sb *strings.Builder, t string ) {
|
||||
for _, c := range MARKDOWN_ESC {
|
||||
t = strings.Replace( t, c, "\\" + c, -1 )
|
||||
}
|
||||
|
||||
sb.WriteString( t )
|
||||
}
|
||||
|
||||
func TryGetEnv[T any]( name string, fallback T ) T {
|
||||
v := os.Getenv( name )
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user