Source

go-wise / timeat.go

Full commit
// Mash between Google maps API and geonames API to get the current time at a
// given location
package main

import (
	"encoding/json"
	"encoding/xml"
	"flag"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"os"
)

const (
	googleKey    = "your google API key goes here"
	geonamesUser = "your geonames user goes here"
)

type Location struct {
	location string  // Location name
	lat, lng float64 // Place (from Google Maps)
	time     string  // Time (from geonames)
	err      error   // Error (if was one)
}

// parseTimeReply parse the XML returned from geonames, returns the time
func parseTimeReply(reader io.Reader) (string, error) {
	dec := xml.NewDecoder(reader)
	get := false

	// SAX style parsing, we're only interested in first <time> element
	for {
		tok, err := dec.Token()
		if err != nil {
			return "", err
		}

		switch typ := tok.(type) {
		case xml.StartElement:
			if typ.Name.Local == "time" {
				get = true
			}
		case xml.CharData:
			if get {
				return string(typ), nil
			}
		}
	}

	return "", fmt.Errorf("'time' element not found in reply")
}

// finTime finds current time in location from geonames
func findTime(loc *Location, out chan *Location) {
	// Emit loc to channel when done
	defer func() {
		out <- loc
	}()

	values := url.Values{
		"username": {geonamesUser},
		"lat":      {fmt.Sprintf("%f", loc.lat)},
		"lng":      {fmt.Sprintf("%f", loc.lng)},
	}

	url := fmt.Sprintf("http://api.geonames.org/timezone?%s", values.Encode())
	resp, err := http.Get(url)
	if err != nil {
		loc.err = err
		return
	}

	defer resp.Body.Close()
	loc.time, loc.err = parseTimeReply(resp.Body)
}

// findPlaces finds matches for location from Google maps API
func findPlaces(location string) (chan *Location, error) {
	values := url.Values{
		"key": {googleKey},
		"q":   {location},
	}

	url := fmt.Sprintf("http://maps.google.com/maps/geo?%s", values.Encode())
	resp, err := http.Get(url)
	if err != nil {
		return nil, err
	}

	defer resp.Body.Close()

	dec := json.NewDecoder(resp.Body)
	// Just enought to get the data from the JSON reply
	var reply struct {
		Status struct {
			Code int
		}

		Placemark []struct {
			Point struct {
				Coordinates []float64
			}
			Address string
		}
	}

	if err = dec.Decode(&reply); err != nil {
		return nil, err
	}

	out := make(chan *Location)
	// Fill the channel with locations
	go func() {
		for _, pm := range reply.Placemark {
			out <- &Location{
				lng:      pm.Point.Coordinates[0],
				lat:      pm.Point.Coordinates[1],
				location: pm.Address,
			}
		}
		close(out)
	}()

	return out, nil
}

func main() {
	flag.Usage = func() {
		fmt.Fprintf(os.Stderr, "usage: %s LOCATION\n", os.Args[0])
	}
	flag.Parse()

	if flag.NArg() != 1 {
		log.Fatalf("error: wrong number of arguments")
	}

	places, err := findPlaces(flag.Arg(0))
	if err != nil {
		log.Fatalf("error: %s\n", err)
	}

	out := make(chan *Location)

	// We count since we don't close the channel
	count := 0
	for loc := range places {
		go findTime(loc, out)
		count++
	}

	for ; count > 0; count-- {
		loc := <-out
		if loc.err != nil {
			fmt.Fprintf(os.Stderr, "error: %s [%s]", loc.err, loc.location)
			continue
		}
		fmt.Printf("%s: %s\n", loc.location, loc.time)
	}
}