commit 7e327abbae5bd4b0188551469783b79565e27781
| author | 斟酌 鵬兄 <tgckpg@gmail.com> |
| date | 2022-09-13T13:42:51Z |
| subject | Initial commit |
commit 7e327abbae5bd4b0188551469783b79565e27781
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date: 2022-09-13T13:42:51Z
Initial commit
---
.gitignore | 2 +
businfo/mtr/busschedule.go | 78 ++++++++++++++++++++++
businfo/mtr/busschedule_test.go | 12 ++++
businfo/mtr/busstops.go | 141 ++++++++++++++++++++++++++++++++++++++++
businfo/mtr/busstops_test.go | 22 +++++++
businfo/mtr/k66.json | 0
businfo/mtr/query.go | 91 ++++++++++++++++++++++++++
businfo/mtr/query_test.go | 16 +++++
go.mod | 3 +
handlers/index.go | 11 ++++
main.go | 12 ++++
utils/env.go | 12 ++++
utils/shortcuts.go | 45 +++++++++++++
utils/shortcuts_test.go | 29 +++++++++
14 files changed, 474 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..343a25c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+*.csv
+*.json
diff --git a/businfo/mtr/busschedule.go b/businfo/mtr/busschedule.go
new file mode 100644
index 0000000..c796394
--- /dev/null
+++ b/businfo/mtr/busschedule.go
@@ -0,0 +1,78 @@
+package mtr
+
+import (
+ "fmt"
+ // "byte"
+ // "net/http"
+ "io"
+ "os"
+ "encoding/json"
+)
+
+type Location struct {
+ 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"`
+}
+
+type BusStopSchedules struct {
+ Buses [] Bus `json:"bus"`
+ Suspended string `json:"isSuspended"`
+}
+
+type BusSchedule struct {
+ RefreshTime int `json:"appRefreshTimeInSecond,string"`
+ BusStops [] BusStopSchedules `json:"busStop"`
+}
+
+func getSchedule( lang string, routeName string ) ( *BusSchedule, error ) {
+
+ /*
+ values := map[string]string { "language": lang , "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
+ }
+
+ defer resp.Body.Close()
+
+ data, err := io.ReadAll( resp.Body )
+ /*/
+ f, err := os.Open( "abc.json" )
+ defer f.Close()
+
+ data, err := io.ReadAll( f )
+ //*/
+ if err != nil {
+ return nil, err
+ }
+
+ schedules := BusSchedule{}
+
+ err = json.Unmarshal( data, &schedules )
+ if err != nil {
+ return nil, err
+ }
+
+ fmt.Printf( "%+v", schedules )
+ return &schedules, nil
+}
diff --git a/businfo/mtr/busschedule_test.go b/businfo/mtr/busschedule_test.go
new file mode 100644
index 0000000..9f8ccf6
--- /dev/null
+++ b/businfo/mtr/busschedule_test.go
@@ -0,0 +1,12 @@
+package mtr
+
+import (
+ "testing"
+)
+
+func TestGetSchedule(t *testing.T) {
+ _, err := getSchedule( "en", "K66" )
+ if err != nil {
+ t.Error( err )
+ }
+}
diff --git a/businfo/mtr/busstops.go b/businfo/mtr/busstops.go
new file mode 100644
index 0000000..a1a05ad
--- /dev/null
+++ b/businfo/mtr/busstops.go
@@ -0,0 +1,141 @@
+package mtr
+
+import (
+ "encoding/csv"
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "github.com/tgckpg/golifehk/utils"
+)
+
+type BusStop struct {
+ RouteId string
+ Direction string
+ StationSeq string
+ StationId string
+ Latitude float64
+ Longtitude float64
+ Name_zhant string
+ Name_en string
+}
+
+var mBusStops *map[string]BusStop
+
+var DEF_CSV 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{}
+ var headers []string
+ for i, line := range entries {
+ if i == 0 {
+ headers = line
+ line[0] = strings.TrimLeft( line[0], utils.BOM )
+ 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":
+ entry.StationSeq = value
+ 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_zhant = value
+ case "STATION_NAME_ENG":
+ entry.Name_en = value
+ default:
+ return nil, fmt.Errorf( "Unknown header \"%s\"", headers[j] )
+ }
+ }
+
+ if _, t := busStops[ entry.StationId ]; t {
+ return nil, fmt.Errorf( "Duplicated entry %+v", entry )
+ }
+
+ busStops[ entry.StationId ] = entry
+ }
+ return &busStops, nil
+}
+
+func getBusStops() (*map[string]BusStop, error) {
+
+ if mBusStops != nil {
+ return mBusStops, nil
+ }
+
+ mBusStops, err := pullLocal()
+ if mBusStops != nil {
+ return mBusStops, err
+ }
+
+ mBusStops, err = pullRemote()
+ if mBusStops != nil {
+ return mBusStops, err
+ }
+
+ return nil, err
+}
+
+func pullRemote() (*map[string]BusStop, error) {
+
+ err := os.MkdirAll( filepath.Dir( DEF_CSV ), 0750 )
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := http.Get( "https://opendata.mtr.com.hk/data/mtr_bus_stops.csv" )
+ if err != nil {
+ return nil, err
+ }
+
+ defer resp.Body.Close()
+
+ f, err := os.OpenFile( DEF_CSV, os.O_CREATE | os.O_RDWR, 0644 )
+ if err != nil {
+ return nil, err
+ }
+
+ defer f.Close()
+
+ _, err = io.Copy( f, resp.Body )
+ if err != nil {
+ return nil, err
+ }
+
+ f.Seek( 0, 0 )
+ return readBusStopData( f )
+}
+
+func pullLocal() (*map[string]BusStop, error) {
+ f, err := os.Open( DEF_CSV )
+ if err != nil {
+ return nil, err
+ }
+
+ defer f.Close()
+
+ return readBusStopData( f )
+}
diff --git a/businfo/mtr/busstops_test.go b/businfo/mtr/busstops_test.go
new file mode 100644
index 0000000..aa81062
--- /dev/null
+++ b/businfo/mtr/busstops_test.go
@@ -0,0 +1,22 @@
+package mtr
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestAll(t *testing.T) {
+ /*
+ entries, err := pullRemote()
+ if err != nil {
+ t.Error( err )
+ }
+ fmt.Println( entries )
+ */
+
+ entries, err := pullLocal()
+ if err != nil {
+ t.Error( err )
+ }
+ fmt.Println( entries )
+}
diff --git a/businfo/mtr/k66.json b/businfo/mtr/k66.json
new file mode 100644
index 0000000..e69de29
diff --git a/businfo/mtr/query.go b/businfo/mtr/query.go
new file mode 100644
index 0000000..e0e61d6
--- /dev/null
+++ b/businfo/mtr/query.go
@@ -0,0 +1,91 @@
+package mtr
+
+import (
+ "fmt"
+ "strings"
+ "github.com/tgckpg/golifehk/utils"
+)
+
+type QueryObject struct {
+ Route string
+ BusStops []BusStop
+}
+
+type Term struct {
+ Value string
+ ProblyRoute bool
+}
+
+func test( entry BusStop, val string ) bool {
+ switch true {
+ case strings.Contains( entry.Name_zhant, val ):
+ fallthrough
+ case strings.Contains( 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 = ""
+ var searches = []string{}
+ matches := []BusStop{}
+
+ terms := []Term{}
+ for _, val := range strings.Split( line, " " ) {
+ val = strings.ToUpper( strings.Trim( val, " " ) )
+ term := Term{
+ Value: val,
+ ProblyRoute: strings.ContainsAny( val, utils.ROUTE_CHARS ),
+ }
+ terms = append( terms, term )
+ }
+
+ 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
+ }
+
+ searches = append( searches, term.Value )
+ if test( entry, term.Value ) {
+ matches = append( matches, entry )
+ break
+ }
+ }
+ }
+
+ if route != "" && 0 < len( searches ) {
+ matches_in := []BusStop{}
+ for _, entry := range matches {
+ if entry.RouteId != route {
+ continue
+ }
+
+ for _, val := range searches {
+ if test( entry, val ) {
+ matches_in = append( matches_in, entry )
+ break
+ }
+ }
+ }
+ matches = matches_in
+ }
+
+
+ return &QueryObject{ Route: route, BusStops: matches }, err
+}
diff --git a/businfo/mtr/query_test.go b/businfo/mtr/query_test.go
new file mode 100644
index 0000000..db0feb0
--- /dev/null
+++ b/businfo/mtr/query_test.go
@@ -0,0 +1,16 @@
+package mtr
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestQuerySchedule(t *testing.T) {
+ qo, err := parse( "K74 天瑞" )
+ if err != nil {
+ t.Error( err )
+ }
+
+ fmt.Println( qo )
+}
+
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..15f974b
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module github.com/tgckpg/golifehk
+
+go 1.19
diff --git a/handlers/index.go b/handlers/index.go
new file mode 100644
index 0000000..634ed0f
--- /dev/null
+++ b/handlers/index.go
@@ -0,0 +1,11 @@
+package handlers
+
+import (
+ "fmt"
+ "html"
+ "net/http"
+)
+
+func Index(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..7552782
--- /dev/null
+++ b/main.go
@@ -0,0 +1,12 @@
+package main
+
+import (
+ "log"
+ "net/http"
+ "github.com/tgckpg/golifehk/handlers"
+)
+
+func main() {
+ http.HandleFunc( "/", handlers.Index )
+ log.Fatal(http.ListenAndServe(":8000", nil))
+}
diff --git a/utils/env.go b/utils/env.go
new file mode 100644
index 0000000..ec007e0
--- /dev/null
+++ b/utils/env.go
@@ -0,0 +1,12 @@
+package utils
+import (
+ "bytes"
+ "os"
+ "path/filepath"
+)
+
+var WORKDIR string = TryGetEnv( "GOLIFEHK_WORKDIR", filepath.Join( os.TempDir(), "golifehk" ) )
+
+var BOM string = bytes.NewBuffer([]byte{ 0xEF, 0xBB, 0xBF }).String()
+
+const ROUTE_CHARS string = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-"
diff --git a/utils/shortcuts.go b/utils/shortcuts.go
new file mode 100644
index 0000000..b2164ea
--- /dev/null
+++ b/utils/shortcuts.go
@@ -0,0 +1,45 @@
+package utils
+
+import (
+ "os"
+ "strconv"
+)
+
+func TryGetEnv[T any]( name string, fallback T ) T {
+ v := os.Getenv( name )
+
+ if v != "" {
+ switch any( fallback ).(type) {
+ case uint64:
+ p, err := strconv.ParseUint( v, 10, 64 )
+ if err == nil {
+ return any( uint64( p ) ).(T)
+ }
+ case uint32:
+ p, err := strconv.ParseUint( v, 10, 32 )
+ if err == nil {
+ return any( uint32( p ) ).(T)
+ }
+ case int:
+ p, err := strconv.ParseInt( v, 10, 32 )
+ if err == nil {
+ return any( int( p ) ).(T)
+ }
+ case float64:
+ p, err := strconv.ParseFloat( v, 64 )
+ if err == nil {
+ return any( float64( p ) ).(T)
+ }
+ case float32:
+ p, err := strconv.ParseFloat( v, 32 )
+ if err == nil {
+ return any( float32( p ) ).(T)
+ }
+ default:
+ return any( v ).(T)
+ }
+
+ }
+
+ return fallback
+}
diff --git a/utils/shortcuts_test.go b/utils/shortcuts_test.go
new file mode 100644
index 0000000..be16740
--- /dev/null
+++ b/utils/shortcuts_test.go
@@ -0,0 +1,29 @@
+package utils
+
+import (
+ "os"
+ "reflect"
+ "testing"
+)
+
+func testType[T any]( t *testing.T, name string, fallback T ) T {
+ got := TryGetEnv( "ABC", fallback )
+ a := reflect.TypeOf( got ).Kind()
+ b := reflect.TypeOf( fallback ).Kind()
+ if a != b {
+ t.Errorf( "%s is not of type %s", any( got ), b )
+ }
+ return got
+}
+
+func TestAll(t *testing.T) {
+ testType( t, "ABC", "Test" )
+ testType( t, "ABC", 11 )
+
+ os.Setenv( "ABC", "22" )
+ got := testType( t, "ABC", 11 )
+ if got != 22 {
+ t.Errorf( "Expected 22, got %d", got )
+ }
+}
+