diff --git a/cmd/analyze-gpx/main.go b/cmd/analyze-gpx/main.go index 6603874..ddb842d 100644 --- a/cmd/analyze-gpx/main.go +++ b/cmd/analyze-gpx/main.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "flag" "fmt" "io" "io/ioutil" @@ -17,27 +18,32 @@ import ( func main() { log.SetFlags(0) - if len(os.Args) != 2 { - log.Fatal("Usage: %s GPX_FILE_OR_DIRECTORY") + stopNames := flag.String("stops", "", "Source for refreshment stops") + flag.Parse() + if flag.NArg() != 1 { + log.Fatal("Usage: %s [--stops=ctccambridge|cyclingmaps] GPX_FILE_OR_DIRECTORY") } - inFile := os.Args[1] + inFile := flag.Arg(0) info, err := os.Stat(inFile) if err != nil { log.Fatal(err) } - // TODO add --stops flag to select stops database - stopsIndex, err := cafes.FetchCtcCamIndex() - if err != nil { - log.Fatal(err) + var stops *rtreego.Rtree + if *stopNames != "" { + var err error + stops, err = cafes.New().Get(*stopNames) + if err != nil { + log.Fatal(err) + } } gs, err := placenames.NewGPXSummarizer() if err != nil { log.Fatal(err) } if info.IsDir() { - err = summarizeDirectory(gs, stopsIndex, inFile) + err = summarizeDirectory(gs, stops, inFile) } else { - err = summarizeSingleFile(gs, stopsIndex, inFile) + err = summarizeSingleFile(gs, stops, inFile) } if err != nil { log.Fatal(err) diff --git a/cmd/serve-rwgps/main.go b/cmd/serve-rwgps/main.go index 2c3e7ee..99b061f 100644 --- a/cmd/serve-rwgps/main.go +++ b/cmd/serve-rwgps/main.go @@ -3,6 +3,7 @@ package main import ( "bytes" "encoding/json" + "errors" "fmt" "log" "net/http" @@ -26,33 +27,13 @@ func main() { log.Fatal(err) } gpxSummarizer = gs - if err = loadStops(); err != nil { - log.Fatal(err) - } http.HandleFunc("/rwgps", rwgpsHandler) log.Printf("Listening for requests on %s", listenAddr) log.Fatal(http.ListenAndServe(listenAddr, nil)) } -func loadStops() error { - var err error - log.Println("Fetching CTC Cambridge cafe stops") - stops["ctccam"], err = cafes.FetchCtcCamIndex() - if err != nil { - return err - } - log.Printf("Loaded %d ctccam stops", stops["ctccam"].Size()) - log.Println("Fetching cyclingmaps.net cafe stops") - stops["cyclingmapsnet"], err = cafes.FetchCyclingMapsIndex() - if err != nil { - return err - } - log.Printf("Loaded %d cyclingmapsnet stops", stops["cyclingmapsnet"].Size()) - return nil -} - var gpxSummarizer *placenames.GPXSummarizer -var stops = make(map[string]*rtreego.Rtree) +var stops = cafes.New() func rwgpsHandler(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() @@ -72,10 +53,15 @@ func rwgpsHandler(w http.ResponseWriter, r *http.Request) { } var stopsIndex *rtreego.Rtree if stopsName != "" { - stopsIndex = stops[stopsName] - if stopsIndex == nil { - log.Printf("Invalid stops: %s", stopsName) - http.Error(w, fmt.Sprintf("Invalid stops: %s", stopsName), http.StatusBadRequest) + var err error + stopsIndex, err = stops.Get(stopsName) + if err != nil { + log.Println(err) + if errors.Is(err, cafes.ErrInvalidStops) { + http.Error(w, err.Error(), http.StatusBadRequest) + } else { + http.Error(w, err.Error(), http.StatusInternalServerError) + } return } } diff --git a/pkg/cafes/common.go b/pkg/cafes/common.go new file mode 100644 index 0000000..e956e51 --- /dev/null +++ b/pkg/cafes/common.go @@ -0,0 +1,87 @@ +package cafes + +import ( + "errors" + "fmt" + "sync" + "time" + + "github.com/dhconnelly/rtreego" +) + +type RefreshmentStop struct { + Name string + Url string + Easting float64 + Northing float64 +} + +func (s *RefreshmentStop) Bounds() *rtreego.Rect { + p := rtreego.Point{s.Easting, s.Northing} + return p.ToRect(100) +} + +func (s *RefreshmentStop) Contains(p rtreego.Point) bool { + if len(p) != 2 { + panic("Expected a 2-dimensional point") + } + bounds := s.Bounds() + for i := 0; i < 2; i++ { + if p[i] < bounds.PointCoord(i) || p[i] > bounds.PointCoord(i)+bounds.LengthsCoord(i) { + return false + } + } + return true +} + +// TTL cache based on "9.7 Example: Concurrent Non-Blocking Cache" from +// "The Go Programming Language", Alan A. A. Dovovan and Brian W. Kernighan + +type result struct { + value *rtreego.Rtree + err error +} + +type entry struct { + res result + expires time.Time + ready chan struct{} // closed when res is ready +} + +type Cache struct { + mu sync.Mutex + entries map[string]*entry +} + +func New() *Cache { + return &Cache{entries: make(map[string]*entry)} +} + +func (c *Cache) Get(k string) (*rtreego.Rtree, error) { + c.mu.Lock() + e := c.entries[k] + if e == nil || e.expires.Before(time.Now()) { + e = &entry{ready: make(chan struct{}), expires: time.Now().Add(4 * time.Hour)} + c.entries[k] = e + c.mu.Unlock() + e.res.value, e.res.err = FetchStops(k) + close(e.ready) + } else { + c.mu.Unlock() + <-e.ready + } + return e.res.value, e.res.err +} + +var ErrInvalidStops = errors.New("invalid stops") + +func FetchStops(k string) (*rtreego.Rtree, error) { + switch k { + case "ctccambridge": + return FetchCtcCamIndex() + case "cyclingmaps": + return FetchCyclingMapsIndex() + default: + return nil, fmt.Errorf("%w: %s", ErrInvalidStops, k) + } +} diff --git a/pkg/cafes/ctccam.go b/pkg/cafes/ctccam.go index 10de25c..a95c147 100644 --- a/pkg/cafes/ctccam.go +++ b/pkg/cafes/ctccam.go @@ -4,6 +4,7 @@ import ( "encoding/xml" "fmt" "io" + "log" "net/http" "github.com/dhconnelly/rtreego" @@ -23,31 +24,6 @@ type Waypoints struct { Waypoints []Waypoint `xml:"wpt"` } -type RefreshmentStop struct { - Name string - Url string - Easting float64 - Northing float64 -} - -func (s *RefreshmentStop) Bounds() *rtreego.Rect { - p := rtreego.Point{s.Easting, s.Northing} - return p.ToRect(100) -} - -func (s *RefreshmentStop) Contains(p rtreego.Point) bool { - if len(p) != 2 { - panic("Expected a 2-dimensional point") - } - bounds := s.Bounds() - for i := 0; i < 2; i++ { - if p[i] < bounds.PointCoord(i) || p[i] > bounds.PointCoord(i)+bounds.LengthsCoord(i) { - return false - } - } - return true -} - func BuildCtcCamIndex(r io.Reader) (*rtreego.Rtree, error) { dec := xml.NewDecoder(r) var wpt Waypoints @@ -77,6 +53,7 @@ func BuildCtcCamIndex(r io.Reader) (*rtreego.Rtree, error) { } func FetchCtcCamIndex() (*rtreego.Rtree, error) { + log.Printf("Fetching %s", ctcCamWaypointsUrl) res, err := http.Get(ctcCamWaypointsUrl) if err != nil { return nil, fmt.Errorf("error getting %s: %v", ctcCamWaypointsUrl, err) @@ -89,5 +66,6 @@ func FetchCtcCamIndex() (*rtreego.Rtree, error) { if err != nil { return nil, fmt.Errorf("error building CTC Cambridge stops index: %v", err) } + log.Printf("Loaded %d CTC Cambridge stops", index.Size()) return index, nil } diff --git a/pkg/cafes/cyclingmaps.go b/pkg/cafes/cyclingmaps.go index c1dc0ed..71a4f48 100644 --- a/pkg/cafes/cyclingmaps.go +++ b/pkg/cafes/cyclingmaps.go @@ -54,6 +54,7 @@ func BuildCyclingMapsIndex(r io.Reader) (*rtreego.Rtree, error) { } func FetchCyclingMapsIndex() (*rtreego.Rtree, error) { + log.Printf("Fetching %s", cyclingMapsCafesUrl) res, err := http.Get(cyclingMapsCafesUrl) if err != nil { return nil, fmt.Errorf("error getting %s: %v", cyclingMapsCafesUrl, err) @@ -66,5 +67,6 @@ func FetchCyclingMapsIndex() (*rtreego.Rtree, error) { if err != nil { return nil, fmt.Errorf("error building cyclingmaps.net cafe stops index: %v", err) } + log.Printf("Loaded %d cyclingmaps.net stops", index.Size()) return index, nil }