Initial commit

This commit is contained in:
Penguin Tsang 2022-09-13 21:42:51 +08:00
commit cea039a921
15 changed files with 474 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.csv
*.json

View File

@ -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
}

View File

@ -0,0 +1,12 @@
package mtr
import (
"testing"
)
func TestGetSchedule(t *testing.T) {
_, err := getSchedule( "en", "K66" )
if err != nil {
t.Error( err )
}
}

141
businfo/mtr/busstops.go Normal file
View File

@ -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 )
}

View File

@ -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 )
}

0
businfo/mtr/k66.json Normal file
View File

91
businfo/mtr/query.go Normal file
View File

@ -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
}

16
businfo/mtr/query_test.go Normal file
View File

@ -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 )
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module github.com/tgckpg/golifehk
go 1.19

BIN
golifehk Executable file

Binary file not shown.

11
handlers/index.go Normal file
View File

@ -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))
}

12
main.go Normal file
View File

@ -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))
}

12
utils/env.go Normal file
View File

@ -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-"

45
utils/shortcuts.go Normal file
View File

@ -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
}

29
utils/shortcuts_test.go Normal file
View File

@ -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 )
}
}