github.com/wtfutil/wtf@v0.43.0/modules/nextbus/widget.go (about)

     1  package nextbus
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"strconv"
     9  
    10  	"github.com/rivo/tview"
    11  	"github.com/wtfutil/wtf/logger"
    12  	"github.com/wtfutil/wtf/view"
    13  )
    14  
    15  // Widget is the container for your module's data
    16  type Widget struct {
    17  	view.TextWidget
    18  
    19  	settings *Settings
    20  }
    21  
    22  // NewWidget creates and returns an instance of Widget
    23  func NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {
    24  	widget := Widget{
    25  		TextWidget: view.NewTextWidget(tviewApp, redrawChan, pages, settings.common),
    26  
    27  		settings: settings,
    28  	}
    29  	return &widget
    30  }
    31  
    32  /* -------------------- Exported Functions -------------------- */
    33  
    34  // Refresh updates the onscreen contents of the widget
    35  func (widget *Widget) Refresh() {
    36  	// The last call should always be to the display function
    37  	widget.display()
    38  }
    39  
    40  /* -------------------- Unexported Functions -------------------- */
    41  
    42  func (widget *Widget) content() string {
    43  	return getNextBus(widget.settings.agency, widget.settings.route, widget.settings.stopID)
    44  }
    45  
    46  type AutoGenerated struct {
    47  	Copyright   string      `json:"copyright"`
    48  	Predictions Predictions `json:"predictions"`
    49  }
    50  
    51  type Prediction struct {
    52  	AffectedByLayover string `json:"affectedByLayover"`
    53  	Seconds           string `json:"seconds"`
    54  	TripTag           string `json:"tripTag"`
    55  	Minutes           string `json:"minutes"`
    56  	IsDeparture       string `json:"isDeparture"`
    57  	Block             string `json:"block"`
    58  	DirTag            string `json:"dirTag"`
    59  	Branch            string `json:"branch"`
    60  	EpochTime         string `json:"epochTime"`
    61  	Vehicle           string `json:"vehicle"`
    62  }
    63  
    64  type Direction struct {
    65  	PredictionRaw json.RawMessage `json:"prediction"`
    66  	Title         string          `json:"title"`
    67  }
    68  
    69  type Predictions struct {
    70  	RouteTag    string    `json:"routeTag"`
    71  	StopTag     string    `json:"stopTag"`
    72  	RouteTitle  string    `json:"routeTitle"`
    73  	AgencyTitle string    `json:"agencyTitle"`
    74  	StopTitle   string    `json:"stopTitle"`
    75  	Direction   Direction `json:"direction"`
    76  }
    77  
    78  func getNextBus(agency string, route string, stopID string) string {
    79  	url := fmt.Sprintf("https://webservices.umoiq.com/service/publicJSONFeed?command=predictions&a=%s&r=%s&stopId=%s", agency, route, stopID)
    80  	resp, err := http.Get(url)
    81  	if err != nil {
    82  		logger.Log(fmt.Sprintf("[nextbus] Error: Failed to make requests to umoiq for next bus predictions. Reason: %s", err))
    83  		return "[nextbus] error calling umoiq"
    84  	}
    85  	body, readErr := io.ReadAll(resp.Body)
    86  
    87  	if (readErr) != nil {
    88  		logger.Log(fmt.Sprintf("[nextbus] Error: Failed to parse response body from umoiq. Reason: %s", err))
    89  		return "[nextbus] error parsing response body"
    90  	}
    91  
    92  	resp.Body.Close()
    93  
    94  	var parsedResponse AutoGenerated
    95  
    96  	// partial unmarshal, we don't have r.Predictions.Direction.PredictionRaw <- YET
    97  	unmarshalError := json.Unmarshal(body, &parsedResponse)
    98  	if unmarshalError != nil {
    99  		logger.Log(fmt.Sprintf("[nextbus] Error: Failed to unmarshal body from umoiq. Reason: %s", err))
   100  		return "[nextbus] error unmarshalling response body"
   101  	}
   102  
   103  	parseType := ""
   104  	// hacky, try object parse first
   105  	nextBusObject := Prediction{}
   106  	if err := json.Unmarshal(parsedResponse.Predictions.Direction.PredictionRaw, &nextBusObject); err == nil {
   107  		parseType = "object"
   108  	}
   109  
   110  	// if object parse failed, it probably means we have an array
   111  	nextBuses := []Prediction{}
   112  	if err := json.Unmarshal(parsedResponse.Predictions.Direction.PredictionRaw, &nextBuses); err == nil {
   113  		parseType = "array"
   114  	}
   115  
   116  	// build the final string
   117  	finalStr := ""
   118  	if parseType == "array" {
   119  		for _, nextBus := range nextBuses {
   120  			finalStr += fmt.Sprintf("%s | ETA [%s]\n", parsedResponse.Predictions.RouteTitle, strTimeToInt(nextBus.Minutes, nextBus.Seconds))
   121  		}
   122  	} else {
   123  		finalStr += fmt.Sprintf("%s | ETA [%s]\n", parsedResponse.Predictions.RouteTitle, strTimeToInt(nextBusObject.Minutes, nextBusObject.Seconds))
   124  	}
   125  
   126  	return finalStr
   127  }
   128  
   129  // takes minutes and seconds from the API, does math to find the remainder seconds
   130  // since the API only gives whole minutes
   131  func strTimeToInt(sourceMinutes string, sourceSeconds string) string {
   132  	min, _ := strconv.Atoi(sourceMinutes)
   133  	sec, _ := strconv.Atoi(sourceSeconds)
   134  	sec = sec % 60
   135  	return fmt.Sprintf("%02d:%02d", min, sec)
   136  }
   137  
   138  func (widget *Widget) display() {
   139  	widget.Redraw(func() (string, string, bool) {
   140  		return widget.CommonSettings().Title, widget.content(), false
   141  	})
   142  }