diff --git a/bench_test.go b/bench_test.go new file mode 100644 index 0000000..f6786f8 --- /dev/null +++ b/bench_test.go @@ -0,0 +1,132 @@ +// Copyright 2018 Evan Oberholster. +// +// SPDX-License-Identifier: MIT + +package timezoneLookup_test + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "testing" + + timezone "github.com/evanoberholster/timezoneLookup" +) + +func BenchmarkLookup(b *testing.B) { + _ = os.MkdirAll("testdata", 0755) + tzgo := filepath.Join("..", "cmd", "timezone.go") + for _, e := range []string{"msgpack", "protobuf", "json"} { + cfg := timezone.Config{ + DatabaseName: filepath.Join("testdata", "timezone"), + Snappy: true, + } + if e == "json" { + if _, err := os.Stat(cfg.DatabaseName + ".snap.json"); err != nil && os.IsNotExist(err) { + cmd := exec.Command("go", "run", tzgo, "-type=memory") + cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr + cmd.Dir = "testdata" + _ = cmd.Run() + } + cfg.DatabaseType = "memory" + } else { + if _, err := os.Stat(cfg.DatabaseName + "." + e + ".snap.db"); err != nil && os.IsNotExist(err) { + cmd := exec.Command("go", "run", tzgo, "-encoding="+e) + cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr + cmd.Dir = "testdata" + _ = cmd.Run() + } + var err error + if cfg.Encoding, err = timezone.EncodingFromString(e); err != nil { + b.Fatal(err) + } + cfg.DatabaseType = "boltdb" + } + b.Run(e, func(b *testing.B) { + tz, err := timezone.LoadTimezones(cfg) + if err != nil { + b.Fatalf("%q: %#v: %+v", e, cfg, err) + } + defer tz.Close() + + benchLookup(b, tz) + }) + } +} + +func benchLookup(b *testing.B, tz timezone.TimezoneInterface) { + querys := []timezone.Coord{ + {Lat: 5.261417, Lon: -3.925778}, // Abijan Airport + {Lat: -15.678889, Lon: 34.973889}, // Blantyre Airport + {Lat: -12.65945, Lon: 18.25674}, + {Lat: 41.8976, Lon: -87.6205}, + {Lat: 47.6897, Lon: -122.4023}, + {Lat: 42.7235, Lon: -73.6931}, + {Lat: 42.5807, Lon: -83.0223}, + {Lat: 36.8381, Lon: -84.8500}, + {Lat: 40.1674, Lon: -85.3583}, + {Lat: 37.9643, Lon: -86.7453}, + {Lat: 38.6043, Lon: -90.2417}, + {Lat: 41.1591, Lon: -104.8261}, + {Lat: 35.1991, Lon: -111.6348}, + {Lat: 43.1432, Lon: -115.6750}, + {Lat: 47.5886, Lon: -122.3382}, + {Lat: 58.3168, Lon: -134.4397}, + {Lat: 21.4381, Lon: -158.0493}, + {Lat: 42.7000, Lon: -80.0000}, + {Lat: 51.0036, Lon: -114.0161}, + {Lat: -16.4965, Lon: -68.1702}, + {Lat: -31.9369, Lon: 115.8453}, + {Lat: 42.0000, Lon: -87.5000}, + {Lat: 41.8976, Lon: -87.6205}, + {Lat: 47.6897, Lon: -122.4023}, + {Lat: 42.7235, Lon: -73.6931}, + {Lat: 42.5807, Lon: -83.0223}, + {Lat: 36.8381, Lon: -84.8500}, + {Lat: 40.1674, Lon: -85.3583}, + {Lat: 37.9643, Lon: -86.7453}, + {Lat: 38.6043, Lon: -90.2417}, + {Lat: 41.1591, Lon: -104.8261}, + {Lat: 35.1991, Lon: -111.6348}, + {Lat: 43.1432, Lon: -115.6750}, + {Lat: 47.5886, Lon: -122.3382}, + {Lat: 58.3168, Lon: -134.4397}, + {Lat: 21.4381, Lon: -158.0493}, + {Lat: 42.7000, Lon: -80.0000}, + {Lat: 51.0036, Lon: -114.0161}, + {Lat: -16.4965, Lon: -68.1702}, + {Lat: -31.9369, Lon: 115.8453}, + {Lat: 42.0000, Lon: -87.5000}, + {Lat: 41.8976, Lon: -87.6205}, + {Lat: 47.6897, Lon: -122.4023}, + {Lat: 42.7235, Lon: -73.6931}, + {Lat: 42.5807, Lon: -83.0223}, + {Lat: 36.8381, Lon: -84.8500}, + {Lat: 40.1674, Lon: -85.3583}, + {Lat: 37.9643, Lon: -86.7453}, + {Lat: 38.6043, Lon: -90.2417}, + {Lat: 41.1591, Lon: -104.8261}, + {Lat: 35.1991, Lon: -111.6348}, + {Lat: 43.1432, Lon: -115.6750}, + {Lat: 47.5886, Lon: -122.3382}, + {Lat: 58.3168, Lon: -134.4397}, + {Lat: 21.4381, Lon: -158.0493}, + {Lat: 42.7000, Lon: -80.0000}, + {Lat: 51.0036, Lon: -114.0161}, + {Lat: -16.4965, Lon: -68.1702}, + {Lat: -31.9369, Lon: 115.8453}, + {Lat: 42.0000, Lon: -87.5000}, + } + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + query := querys[i%len(querys)] + _, err := tz.Query(query) + if err != nil { + fmt.Println(err) + } + } + b.StopTimer() +} diff --git a/cmd/benchmark.go b/cmd/benchmark.go index 76356fa..d2ac449 100644 --- a/cmd/benchmark.go +++ b/cmd/benchmark.go @@ -1,88 +1,97 @@ +//go:build benchmark +// +build benchmark + +// Copyright 2018 Evan Oberholster. +// +// SPDX-License-Identifier: MIT + package main + import ( - "time" "fmt" + "time" + timezone "github.com/evanoberholster/timezoneLookup" ) func main() { - + tz, err := timezone.LoadTimezones(timezone.Config{ - DatabaseType:"boltdb", // memory or boltdb - DatabaseName:"timezone", // Name without suffix - Snappy: true, - Encoding: "msgpack", // json or msgpack - }) + DatabaseType: "boltdb", // memory or boltdb + DatabaseName: "timezone", // Name without suffix + Snappy: true, + Encoding: "msgpack", // json or msgpack + }) if err != nil { fmt.Println(err) } querys := []timezone.Coord{ - {Lat: 5.261417, Lon: -3.925778,}, // Abijan Airport - {Lat: -15.678889,Lon: 34.973889,}, // Blantyre Airport - {Lat: -12.65945, Lon: 18.25674,}, - {Lat: 41.8976, Lon:-87.6205}, - {Lat: 47.6897, Lon: -122.4023}, - {Lat: 42.7235, Lon:-73.6931}, - {Lat: 42.5807, Lon:-83.0223}, - {Lat: 36.8381, Lon:-84.8500}, - {Lat: 40.1674, Lon:-85.3583}, - {Lat: 37.9643, Lon:-86.7453}, - {Lat: 38.6043, Lon:-90.2417}, - {Lat: 41.1591, Lon:-104.8261}, - {Lat: 35.1991, Lon:-111.6348}, - {Lat: 43.1432, Lon:-115.6750}, - {Lat: 47.5886, Lon:-122.3382}, - {Lat: 58.3168, Lon:-134.4397}, - {Lat: 21.4381, Lon:-158.0493}, - {Lat: 42.7000, Lon:-80.0000}, - {Lat: 51.0036, Lon:-114.0161}, - {Lat:-16.4965, Lon:-68.1702}, - {Lat:-31.9369, Lon:115.8453}, - {Lat: 42.0000, Lon:-87.5000}, - {Lat: 41.8976, Lon:-87.6205}, - {Lat: 47.6897, Lon: -122.4023}, - {Lat: 42.7235, Lon:-73.6931}, - {Lat: 42.5807, Lon:-83.0223}, - {Lat: 36.8381, Lon:-84.8500}, - {Lat: 40.1674, Lon:-85.3583}, - {Lat: 37.9643, Lon:-86.7453}, - {Lat: 38.6043, Lon:-90.2417}, - {Lat: 41.1591, Lon:-104.8261}, - {Lat: 35.1991, Lon:-111.6348}, - {Lat: 43.1432, Lon:-115.6750}, - {Lat: 47.5886, Lon:-122.3382}, - {Lat: 58.3168, Lon:-134.4397}, - {Lat: 21.4381, Lon:-158.0493}, - {Lat: 42.7000, Lon:-80.0000}, - {Lat: 51.0036, Lon:-114.0161}, - {Lat:-16.4965, Lon:-68.1702}, - {Lat:-31.9369, Lon:115.8453}, - {Lat: 42.0000, Lon:-87.5000}, - {Lat: 41.8976, Lon:-87.6205}, - {Lat: 47.6897, Lon: -122.4023}, - {Lat: 42.7235, Lon:-73.6931}, - {Lat: 42.5807, Lon:-83.0223}, - {Lat: 36.8381, Lon:-84.8500}, - {Lat: 40.1674, Lon:-85.3583}, - {Lat: 37.9643, Lon:-86.7453}, - {Lat: 38.6043, Lon:-90.2417}, - {Lat: 41.1591, Lon:-104.8261}, - {Lat: 35.1991, Lon:-111.6348}, - {Lat: 43.1432, Lon:-115.6750}, - {Lat: 47.5886, Lon:-122.3382}, - {Lat: 58.3168, Lon:-134.4397}, - {Lat: 21.4381, Lon:-158.0493}, - {Lat: 42.7000, Lon:-80.0000}, - {Lat: 51.0036, Lon:-114.0161}, - {Lat:-16.4965, Lon:-68.1702}, - {Lat:-31.9369, Lon:115.8453}, - {Lat: 42.0000, Lon:-87.5000}, - } + {Lat: 5.261417, Lon: -3.925778}, // Abijan Airport + {Lat: -15.678889, Lon: 34.973889}, // Blantyre Airport + {Lat: -12.65945, Lon: 18.25674}, + {Lat: 41.8976, Lon: -87.6205}, + {Lat: 47.6897, Lon: -122.4023}, + {Lat: 42.7235, Lon: -73.6931}, + {Lat: 42.5807, Lon: -83.0223}, + {Lat: 36.8381, Lon: -84.8500}, + {Lat: 40.1674, Lon: -85.3583}, + {Lat: 37.9643, Lon: -86.7453}, + {Lat: 38.6043, Lon: -90.2417}, + {Lat: 41.1591, Lon: -104.8261}, + {Lat: 35.1991, Lon: -111.6348}, + {Lat: 43.1432, Lon: -115.6750}, + {Lat: 47.5886, Lon: -122.3382}, + {Lat: 58.3168, Lon: -134.4397}, + {Lat: 21.4381, Lon: -158.0493}, + {Lat: 42.7000, Lon: -80.0000}, + {Lat: 51.0036, Lon: -114.0161}, + {Lat: -16.4965, Lon: -68.1702}, + {Lat: -31.9369, Lon: 115.8453}, + {Lat: 42.0000, Lon: -87.5000}, + {Lat: 41.8976, Lon: -87.6205}, + {Lat: 47.6897, Lon: -122.4023}, + {Lat: 42.7235, Lon: -73.6931}, + {Lat: 42.5807, Lon: -83.0223}, + {Lat: 36.8381, Lon: -84.8500}, + {Lat: 40.1674, Lon: -85.3583}, + {Lat: 37.9643, Lon: -86.7453}, + {Lat: 38.6043, Lon: -90.2417}, + {Lat: 41.1591, Lon: -104.8261}, + {Lat: 35.1991, Lon: -111.6348}, + {Lat: 43.1432, Lon: -115.6750}, + {Lat: 47.5886, Lon: -122.3382}, + {Lat: 58.3168, Lon: -134.4397}, + {Lat: 21.4381, Lon: -158.0493}, + {Lat: 42.7000, Lon: -80.0000}, + {Lat: 51.0036, Lon: -114.0161}, + {Lat: -16.4965, Lon: -68.1702}, + {Lat: -31.9369, Lon: 115.8453}, + {Lat: 42.0000, Lon: -87.5000}, + {Lat: 41.8976, Lon: -87.6205}, + {Lat: 47.6897, Lon: -122.4023}, + {Lat: 42.7235, Lon: -73.6931}, + {Lat: 42.5807, Lon: -83.0223}, + {Lat: 36.8381, Lon: -84.8500}, + {Lat: 40.1674, Lon: -85.3583}, + {Lat: 37.9643, Lon: -86.7453}, + {Lat: 38.6043, Lon: -90.2417}, + {Lat: 41.1591, Lon: -104.8261}, + {Lat: 35.1991, Lon: -111.6348}, + {Lat: 43.1432, Lon: -115.6750}, + {Lat: 47.5886, Lon: -122.3382}, + {Lat: 58.3168, Lon: -134.4397}, + {Lat: 21.4381, Lon: -158.0493}, + {Lat: 42.7000, Lon: -80.0000}, + {Lat: 51.0036, Lon: -114.0161}, + {Lat: -16.4965, Lon: -68.1702}, + {Lat: -31.9369, Lon: 115.8453}, + {Lat: 42.0000, Lon: -87.5000}, + } var times []int64 var total int64 - + for _, query := range querys { start := time.Now() res, err := tz.Query(query) @@ -92,10 +101,10 @@ func main() { elapsed := time.Since(start) fmt.Println("Query Result: ", res, " took: ", elapsed) times = append(times, elapsed.Nanoseconds()) - total += elapsed.Nanoseconds() + total += elapsed.Nanoseconds() } fmt.Println("Average time per query: ", time.Duration(total/int64(len(times)))) tz.Close() - timezone.PrintMemUsage() + timezone.PrintMemUsage() } diff --git a/cmd/example.go b/cmd/example.go index e356c72..bec56ad 100644 --- a/cmd/example.go +++ b/cmd/example.go @@ -1,27 +1,34 @@ +//go:build example +// +build example + +// Copyright 2018 Evan Oberholster. +// +// SPDX-License-Identifier: MIT + package main + import ( "fmt" + timezone "github.com/evanoberholster/timezoneLookup" ) -var tz timezone.TimezoneInterface func main() { tz, err := timezone.LoadTimezones(timezone.Config{ - DatabaseType:"boltdb", // memory or boltdb - DatabaseName:"timezone", // Name without suffix - Snappy: true, - Encoding: "msgpack", // json or msgpack - }) + DatabaseType: "boltdb", // memory or boltdb + DatabaseName: "timezone", // Name without suffix + Snappy: true, + Encoding: "msgpack", // json or msgpack + }) if err != nil { fmt.Println(err) } + defer tz.Close() res, err := tz.Query(timezone.Coord{ - Lat: 5.261417, Lon: -3.925778,}) + Lat: 5.261417, Lon: -3.925778}) if err != nil { fmt.Println(err) } fmt.Println("Query Result: ", res) - - tz.Close() } diff --git a/cmd/timezone.go b/cmd/timezone.go index 982c230..356da21 100644 --- a/cmd/timezone.go +++ b/cmd/timezone.go @@ -1,47 +1,104 @@ +// Copyright 2018 Evan Oberholster. +// +// SPDX-License-Identifier: MIT + package main + import ( + "archive/zip" + "bytes" + "context" + "errors" "flag" + "io" "log" + "net/http" + "os" + "os/signal" + timezone "github.com/evanoberholster/timezoneLookup" ) var ( - snappy = flag.Bool("snappy", true, "Use Snappy compression (true/false)") + snappy = flag.Bool("snappy", true, "Use Snappy compression (true/false)") jsonFilename = flag.String("json", "combined-with-oceans.json", "GEOJSON Filename") - dbFilename = flag.String("db", "timezone", "Destination database filename") - storageType = flag.String("type", "boltdb", "Storage: boltdb or memory") - encoding = flag.String("encoding", "msgpack", "BoltDB encoding type: json or msgpack") + dbFilename = flag.String("db", "timezone", "Destination database filename") + storageType = flag.String("type", "boltdb", "Storage: boltdb or memory") + jsonURL = flag.String("json-url", "https://github.com/evansiroky/timezone-boundary-builder/releases/download/2020d/timezones-with-oceans.geojson.zip", "Download GeoJSON file from here if not exist") + encoding = flag.String("encoding", "msgpack", "BoltDB encoding type: json or msgpack") ) func main() { + if err := Main(); err != nil { + log.Fatalln(err) + } +} +func Main() error { flag.Parse() if *dbFilename == "" || *jsonFilename == "" { log.Printf("Options:\n\t -snappy=true\t Use Snappy compression\n\t -json=filename\t GEOJSON filename \n\t -db=filename\t Database destination\n\t -type=boltdb\t Type of Storage (boltdb or memory) ") - } else { - var tz timezone.TimezoneInterface - if *storageType == "memory" { - tz = timezone.MemoryStorage(*snappy, *dbFilename) - } else if *storageType == "boltdb" { - tz = timezone.BoltdbStorage(*snappy, *dbFilename, *encoding) - } else { - log.Println("\"-db\" No database type specified") - return - } - - if *jsonFilename != "" { - err := tz.CreateTimezones(*jsonFilename) - if err != nil { - log.Println(err) - return - } - } else { - log.Println("\"-json\" No GeoJSON source file specified") - return + return nil + } + var tz timezone.TimezoneInterface + if *storageType == "memory" { + tz = timezone.MemoryStorage(*snappy, *dbFilename) + } else if *storageType == "boltdb" { + e, err := timezone.EncodingFromString(*encoding) + if err != nil { + return err } - - tz.Close() + tz = timezone.BoltdbStorage(*snappy, *dbFilename, e) + } else { + return errors.New("\"-db\" No database type specified") } - -} \ No newline at end of file + if *jsonFilename == "" { + return errors.New("\"-json\" No GeoJSON source file specified") + } + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + if _, err := os.Stat(*jsonFilename); err != nil && os.IsNotExist(err) { + log.Println("Downloading " + *jsonURL) + req, err := http.NewRequest("GET", *jsonURL, nil) + if err != nil { + return err + } + resp, err := http.DefaultClient.Do(req.WithContext(ctx)) + if err != nil { + return err + } + var buf bytes.Buffer + _, err = io.Copy(&buf, resp.Body) + resp.Body.Close() + if err != nil { + return err + } + zr, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len())) + if err != nil { + return err + } + sr, err := zr.Open("combined-with-oceans.json") + if err != nil { + return err + } + fh, err := os.Create(*jsonFilename) + if err != nil { + return err + } + defer func() { _ = os.Remove(fh.Name()) }() + defer fh.Close() + if _, err = io.Copy(fh, sr); err != nil { + return err + } + if err := fh.Close(); err != nil { + return err + } + } + err := tz.CreateTimezones(*jsonFilename) + if err != nil { + return err + } + tz.Close() + return nil +} diff --git a/db.go b/db.go index 7558231..cca1fa4 100644 --- a/db.go +++ b/db.go @@ -1,48 +1,106 @@ +// Copyright 2018 Evan Oberholster. +// +// SPDX-License-Identifier: MIT + package timezoneLookup + import ( - "os" - "errors" + "bytes" "encoding/binary" - "encoding/json" + "errors" + "fmt" + "os" + + "github.com/evanoberholster/timezoneLookup/pb" + json "github.com/goccy/go-json" + "github.com/klauspost/compress/snappy" + "github.com/vmihailenco/msgpack/v5" bolt "go.etcd.io/bbolt" - "github.com/golang/snappy" - "github.com/vmihailenco/msgpack" + "google.golang.org/protobuf/proto" ) -type Store struct { // Database struct - db *bolt.DB - pIndex []PolygonIndex - filename string - snappy bool - encoding string +type Store struct { // Database struct + db *bolt.DB + pIndex []PolygonIndex + filename string + snappy bool + encoding encoding } type PolygonIndex struct { - Id uint64 `json:"-"` - Tzid string `json:"tzid"` - Max Coord `json:"max"` - Min Coord `json:"min"` + Id uint64 `json:"-"` + Tzid string `json:"tzid"` + Max Coord `json:"max"` + Min Coord `json:"min"` +} + +func (dst *PolygonIndex) FromPB(src *pb.PolygonIndex) { + dst.Id, dst.Tzid = src.Id, src.Tzid + dst.Max.FromPB(src.Max) + dst.Min.FromPB(src.Min) +} +func (src *PolygonIndex) ToPB(dst *pb.PolygonIndex) { + dst.Reset() + dst.Id, dst.Tzid = src.Id, src.Tzid + dst.Max = src.Max.ToPB(dst.Max) + dst.Min = src.Min.ToPB(dst.Min) } -func BoltdbStorage(snappy bool, filename string, encoding string) TimezoneInterface { +func BoltdbStorage(snappy bool, filename string, encoding encoding) TimezoneInterface { + filename += "." + encoding.String() if snappy { - filename = filename + ".snap.db" - } else { - filename = filename + ".db" + filename += ".snap" } + filename += ".db" return &Store{ filename: filename, - pIndex: []PolygonIndex{}, - snappy: snappy, + pIndex: []PolygonIndex{}, + snappy: snappy, encoding: encoding, } } -func (s *Store)Close() { +func (s *Store) Close() { defer s.db.Close() } -func (s *Store)LoadTimezones() (error) { +type encoding struct { + Type uint8 +} + +func (e encoding) String() string { + switch e { + case EncMsgPack: + return "msgpack" + case EncJSON: + return "json" + case EncProtobuf: + return "protobuf" + default: + return "unknown" + } +} +func EncodingFromString(s string) (encoding, error) { + switch s { + case "msgpack": + return EncMsgPack, nil + case "json": + return EncJSON, nil + case "protobuf": + return EncProtobuf, nil + default: + return EncUnknown, fmt.Errorf("unknown encoding %q (neither msgpack, nor json)", s) + } +} + +var ( + EncUnknown = encoding{} + EncMsgPack = encoding{1} + EncJSON = encoding{2} + EncProtobuf = encoding{3} +) + +func (s *Store) LoadTimezones() error { if _, err := os.Stat(s.filename); os.IsNotExist(err) { return errors.New(errNotExistDatabase) } @@ -50,37 +108,50 @@ func (s *Store)LoadTimezones() (error) { if err != nil { return err } - // Load polygon indexes + var pbIndex pb.PolygonIndex + var U func(index *PolygonIndex, v []byte) error + switch s.encoding { + case EncMsgPack: + U = func(index *PolygonIndex, v []byte) error { + return msgpack.Unmarshal(v, index) + } + case EncJSON: + U = func(index *PolygonIndex, v []byte) error { + return json.Unmarshal(v, index) + } + case EncProtobuf: + U = func(index *PolygonIndex, v []byte) error { + if err := proto.Unmarshal(v, &pbIndex); err != nil { + return err + } + index.FromPB(&pbIndex) + return nil + } + } + // Load polygon indexes return s.db.View(func(tx *bolt.Tx) error { // Assume bucket exists and has keys b := tx.Bucket([]byte("Index")) - - var err error - b.ForEach(func(k, v []byte) error { + + return b.ForEach(func(k, v []byte) error { var index PolygonIndex - if s.encoding == "msgpack" { - err = msgpack.Unmarshal(v, &index) - } else { - err = json.Unmarshal(v, &index) - } - if err != nil { + if err := U(&index, v); err != nil { return err } index.Id = binary.BigEndian.Uint64(k) s.pIndex = append(s.pIndex, index) return nil }) - return nil }) } -func (s *Store)Query(q Coord) (string, error) { +func (s *Store) Query(q Coord) (string, error) { for _, i := range s.pIndex { if i.Min.Lat < q.Lat && i.Min.Lon < q.Lon && i.Max.Lat > q.Lat && i.Max.Lon > q.Lon { p, err := s.loadPolygon(i.Id) if err != nil { return i.Tzid, errors.New(errPolygonNotFound) - } + } if p.contains(q) { return i.Tzid, nil } @@ -89,7 +160,7 @@ func (s *Store)Query(q Coord) (string, error) { return "Error", errors.New(errTimezoneNotFound) } -func (s *Store)CreateTimezones(jsonFilename string) (error) { +func (s *Store) CreateTimezones(jsonFilename string) error { err := checkFilesExist(jsonFilename, s.filename) if err != nil { return err @@ -107,12 +178,14 @@ func (s *Store)CreateTimezones(jsonFilename string) (error) { return err } for _, tz := range tzs { - s.InsertPolygons(tz) + if err := s.InsertPolygons(tz); err != nil { + return err + } } return nil } -func checkFilesExist(src string, dest string) (error) { +func checkFilesExist(src string, dest string) error { if _, err := os.Stat(src); os.IsNotExist(err) { return errors.New(errNotExistGeoJSON) } @@ -122,74 +195,122 @@ func checkFilesExist(src string, dest string) (error) { return nil } -func (s *Store)createBuckets() (error) { +func (s *Store) createBuckets() error { return s.db.Update(func(tx *bolt.Tx) error { - _, err := tx.CreateBucket([]byte("Index")) - if err != nil { - return err - } - _, err = tx.CreateBucket([]byte("Polygon")) - if err != nil { - return err - } - return nil + _, err := tx.CreateBucket([]byte("Index")) + if err != nil { + return err + } + _, err = tx.CreateBucket([]byte("Polygon")) + if err != nil { + return err + } + return nil }) } -func (s *Store)InsertPolygons(tz Timezone) { +func (s *Store) InsertPolygons(tz Timezone) error { + var bufPolygon, bufIndex []byte + var E func(polygon Polygon, index PolygonIndex) ([]byte, []byte, error) + switch s.encoding { + case EncMsgPack: + pBuf, iBuf := bytes.NewBuffer(bufPolygon), bytes.NewBuffer(bufIndex) + eP := msgpack.NewEncoder(pBuf) + eI := msgpack.NewEncoder(iBuf) + E = func(polygon Polygon, index PolygonIndex) ([]byte, []byte, error) { + pBuf.Reset() + if err := eP.Encode(polygon); err != nil { + return nil, nil, err + } + // Marshal Polygon Index + iBuf.Reset() + err := eI.Encode(index) + return pBuf.Bytes(), iBuf.Bytes(), err + } + case EncJSON: + pBuf, iBuf := bytes.NewBuffer(bufPolygon), bytes.NewBuffer(bufIndex) + eP := json.NewEncoder(pBuf) + eI := json.NewEncoder(iBuf) + E = func(polygon Polygon, index PolygonIndex) ([]byte, []byte, error) { + pBuf.Reset() + if err := eP.Encode(polygon); err != nil { + return nil, nil, err + } + iBuf.Reset() + err := eI.Encode(index) + return pBuf.Bytes(), iBuf.Bytes(), err + } + case EncProtobuf: + var pbPoly pb.Polygon + var pbIndex pb.PolygonIndex + var mo proto.MarshalOptions + E = func(polygon Polygon, index PolygonIndex) ([]byte, []byte, error) { + polygon.ToPB(&pbPoly) + bufPolygon, err := mo.MarshalAppend(bufPolygon[:0], &pbPoly) + if err != nil { + return nil, nil, err + } + index.ToPB(&pbIndex) + bufIndex, err := mo.MarshalAppend(bufIndex[:0], &pbIndex) + return bufPolygon, bufIndex, err + } + } for _, polygon := range tz.Polygons { - s.db.Update(func(tx *bolt.Tx) error { + if err := s.db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("Polygon")) i := tx.Bucket([]byte("Index")) // Get ID number autoIncrement id, _ := b.NextSequence() - intId := int(id) - - // Create Polygon Index - index := PolygonIndex{ - Tzid: tz.Tzid, - Max: polygon.Max, - Min: polygon.Min, - } - var bufPolygon, bufIndex []byte - var err error - - if s.encoding == "msgpack" { - // Marshal Polygons - bufPolygon, err = msgpack.Marshal(polygon) - if err != nil { - return err - } - // Marshal Polygon Index - bufIndex, err = msgpack.Marshal(index) - if err != nil { - return err - } - } else { - bufPolygon, err = json.Marshal(polygon) - if err != nil { - return err - } - bufIndex, err = json.Marshal(index) - if err != nil { - return err - } + intId := int(id) + + // Create Polygon Index + index := PolygonIndex{ + Tzid: tz.Tzid, + Max: polygon.Max, + Min: polygon.Min, } - if s.snappy { - bufPolygon = snappy.Encode(nil, bufPolygon) - } - // Write Polygon Index - err = i.Put(itob(intId), bufIndex) - if err != nil { - return err - } - return b.Put(itob(intId), bufPolygon) - }) + bufPolygon, bufIndex, err := E(polygon, index) + if err != nil { + return err + } + if s.snappy { + bufPolygon = snappy.Encode(nil, bufPolygon) + } + // Write Polygon Index + err = i.Put(itob(intId), bufIndex) + if err != nil { + return err + } + return b.Put(itob(intId), bufPolygon) + }); err != nil { + return err + } } + return nil } -func (s *Store)loadPolygon(id uint64) (Polygon, error) { +func (s *Store) loadPolygon(id uint64) (Polygon, error) { + var pbPoly pb.Polygon + var U func(polygon *Polygon, v []byte) error + switch s.encoding { + case EncMsgPack: + U = func(polygon *Polygon, v []byte) error { + return msgpack.Unmarshal(v, polygon) + } + case EncJSON: + U = func(polygon *Polygon, v []byte) error { + return json.Unmarshal(v, polygon) + } + case EncProtobuf: + U = func(polygon *Polygon, v []byte) error { + if err := proto.Unmarshal(v, &pbPoly); err != nil { + return err + } + polygon.FromPB(&pbPoly) + return nil + } + } var polygon Polygon err := s.db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("Polygon")) @@ -201,28 +322,23 @@ func (s *Store)loadPolygon(id uint64) (Polygon, error) { return err } } - if s.encoding == "msgpack" { - return msgpack.Unmarshal(v, &polygon) - } else { - return json.Unmarshal(v, &polygon) - } + return U(&polygon, v) }) return polygon, err } // itob returns an 8-byte big endian representation of v. func itob(v int) []byte { - b := make([]byte, 8) - binary.BigEndian.PutUint64(b, uint64(v)) - return b + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, uint64(v)) + return b } - -func (s *Store)OpenDB(path string) (error) { +func (s *Store) OpenDB(path string) error { var err error s.db, err = bolt.Open(path, 0666, nil) if err != nil { - return err + return err } return nil } diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..159ea10 --- /dev/null +++ b/example_test.go @@ -0,0 +1,31 @@ +// Copyright 2018 Evan Oberholster. +// +// SPDX-License-Identifier: MIT + +package timezoneLookup_test + +import ( + "fmt" + + timezone "github.com/evanoberholster/timezoneLookup" +) + +func ExampleQuery() { + tz, err := timezone.LoadTimezones(timezone.Config{ + DatabaseType: "boltdb", // memory or boltdb + DatabaseName: "timezone", // Name without suffix + Snappy: true, + Encoding: timezone.EncMsgPack, // json or msgpack + }) + if err != nil { + fmt.Println(err) + } + defer tz.Close() + + res, err := tz.Query(timezone.Coord{ + Lat: 5.261417, Lon: -3.925778}) + if err != nil { + fmt.Println(err) + } + fmt.Println("Query Result: ", res) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9807e72 --- /dev/null +++ b/go.mod @@ -0,0 +1,22 @@ +module github.com/evanoberholster/timezoneLookup + +go 1.17 + +require ( + github.com/golang/snappy v0.0.4 + github.com/vmihailenco/msgpack v4.0.4+incompatible + go.etcd.io/bbolt v1.3.6 +) + +require ( + github.com/goccy/go-json v0.7.10 // indirect + github.com/golang/protobuf v1.5.0 // indirect + github.com/klauspost/compress v1.13.6 // indirect + github.com/vmihailenco/msgpack/v5 v5.3.4 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + golang.org/x/net v0.0.0-20190603091049-60506f45cf65 // indirect + golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.27.1 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..003cc46 --- /dev/null +++ b/go.sum @@ -0,0 +1,46 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/goccy/go-json v0.7.10 h1:ulhbuNe1JqE68nMRXXTJRrUu0uhouf0VevLINxQq4Ec= +github.com/goccy/go-json v0.7.10/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= +github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +github.com/vmihailenco/msgpack/v5 v5.3.4 h1:qMKAwOV+meBw2Y8k9cVwAy7qErtYCwBzZ2ellBfvnqc= +github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d h1:L/IKR6COd7ubZrs2oTnTi73IhgqJ71c9s80WsQnh0Es= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/memory.go b/memory.go index a0cccff..a928181 100644 --- a/memory.go +++ b/memory.go @@ -1,46 +1,51 @@ -package timezoneLookup +// Copyright 2018 Evan Oberholster. +// +// SPDX-License-Identifier: MIT + +package timezoneLookup + import ( - "os" - "encoding/json" "errors" - "github.com/golang/snappy" + "io" + "os" + + json "github.com/goccy/go-json" + "github.com/klauspost/compress/snappy" ) type Memory struct { // Memory struct - filename string - timezones []Timezone - snappy bool + filename string + timezones []Timezone + snappy bool } func MemoryStorage(snappy bool, filename string) *Memory { if snappy { - filename = filename + ".snap.json" - } else { - filename = filename + ".json" + filename += ".snap" } + filename += ".json" return &Memory{ - filename: filename, + filename: filename, timezones: []Timezone{}, - snappy: snappy, + snappy: snappy, } } -func (m *Memory)Close() { +func (m *Memory) Close() { m.timezones = []Timezone{} } -func (m *Memory)LoadTimezones() (error) { +func (m *Memory) LoadTimezones() error { file, err := os.Open(m.filename) if err != nil { return err } - + var tzs []Timezone if m.snappy { data := snappy.NewReader(file) dec := json.NewDecoder(data) for dec.More() { - err := dec.Decode(&tzs) if err != nil { return err @@ -61,7 +66,7 @@ func (m *Memory)LoadTimezones() (error) { return nil } -func (m *Memory)Query(q Coord) (string, error) { +func (m *Memory) Query(q Coord) (string, error) { for _, tz := range m.timezones { for _, p := range tz.Polygons { if p.Min.Lat < q.Lat && p.Min.Lon < q.Lon && p.Max.Lat > q.Lat && p.Max.Lon > q.Lon { @@ -74,30 +79,29 @@ func (m *Memory)Query(q Coord) (string, error) { return "Error", errors.New(errTimezoneNotFound) } -func (m *Memory)writeTimezoneJSON(dbFilename string) (error) { - data, err := json.Marshal(m.timezones) - if err != nil { - return err - } +func (m *Memory) writeTimezoneJSON(dbFilename string) error { w, err := os.Create(dbFilename) - if err != nil { - return err - } - defer w.Close() - if m.snappy { - snap := snappy.NewBufferedWriter(w) - _, err := snap.Write(data) - if err != nil { - return err + if err != nil { + return err + } + defer w.Close() + sw := io.WriteCloser(w) + if m.snappy { + sw = snappy.NewBufferedWriter(w) + } + err = json.NewEncoder(sw).Encode(m.timezones) + if closeErr := sw.Close(); closeErr != nil && err == nil { + err = closeErr + } + if m.snappy { + if closeErr := w.Close(); closeErr != nil && err == nil { + err = closeErr } - defer snap.Close() - } else { - _ , err = w.Write(data) - } - return err + } + return err } -func (m *Memory)CreateTimezones(jsonFilename string) (error) { +func (m *Memory) CreateTimezones(jsonFilename string) error { tzs, err := TimezonesFromGeoJSON(jsonFilename) if err != nil { return err @@ -108,4 +112,4 @@ func (m *Memory)CreateTimezones(jsonFilename string) (error) { return err } return nil -} \ No newline at end of file +} diff --git a/pb/timezone.pb.go b/pb/timezone.pb.go new file mode 100644 index 0000000..16fca81 --- /dev/null +++ b/pb/timezone.pb.go @@ -0,0 +1,333 @@ +// Copyright 2021 Tamás Gulácsi. +// +// SPDX-License-Identifier: MIT + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.12.4 +// source: timezone.proto + +package pb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Coord struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Lat float32 `protobuf:"fixed32,1,opt,name=Lat,proto3" json:"Lat,omitempty"` + Lon float32 `protobuf:"fixed32,2,opt,name=Lon,proto3" json:"Lon,omitempty"` +} + +func (x *Coord) Reset() { + *x = Coord{} + if protoimpl.UnsafeEnabled { + mi := &file_timezone_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Coord) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Coord) ProtoMessage() {} + +func (x *Coord) ProtoReflect() protoreflect.Message { + mi := &file_timezone_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Coord.ProtoReflect.Descriptor instead. +func (*Coord) Descriptor() ([]byte, []int) { + return file_timezone_proto_rawDescGZIP(), []int{0} +} + +func (x *Coord) GetLat() float32 { + if x != nil { + return x.Lat + } + return 0 +} + +func (x *Coord) GetLon() float32 { + if x != nil { + return x.Lon + } + return 0 +} + +type Polygon struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Max *Coord `protobuf:"bytes,1,opt,name=Max,proto3" json:"Max,omitempty"` + Min *Coord `protobuf:"bytes,2,opt,name=Min,proto3" json:"Min,omitempty"` + Coords []*Coord `protobuf:"bytes,4,rep,name=Coords,proto3" json:"Coords,omitempty"` +} + +func (x *Polygon) Reset() { + *x = Polygon{} + if protoimpl.UnsafeEnabled { + mi := &file_timezone_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Polygon) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Polygon) ProtoMessage() {} + +func (x *Polygon) ProtoReflect() protoreflect.Message { + mi := &file_timezone_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Polygon.ProtoReflect.Descriptor instead. +func (*Polygon) Descriptor() ([]byte, []int) { + return file_timezone_proto_rawDescGZIP(), []int{1} +} + +func (x *Polygon) GetMax() *Coord { + if x != nil { + return x.Max + } + return nil +} + +func (x *Polygon) GetMin() *Coord { + if x != nil { + return x.Min + } + return nil +} + +func (x *Polygon) GetCoords() []*Coord { + if x != nil { + return x.Coords + } + return nil +} + +type PolygonIndex struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id uint64 `protobuf:"varint,1,opt,name=Id,proto3" json:"Id,omitempty"` + Tzid string `protobuf:"bytes,2,opt,name=Tzid,proto3" json:"Tzid,omitempty"` + Max *Coord `protobuf:"bytes,3,opt,name=Max,proto3" json:"Max,omitempty"` + Min *Coord `protobuf:"bytes,4,opt,name=Min,proto3" json:"Min,omitempty"` +} + +func (x *PolygonIndex) Reset() { + *x = PolygonIndex{} + if protoimpl.UnsafeEnabled { + mi := &file_timezone_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PolygonIndex) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PolygonIndex) ProtoMessage() {} + +func (x *PolygonIndex) ProtoReflect() protoreflect.Message { + mi := &file_timezone_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PolygonIndex.ProtoReflect.Descriptor instead. +func (*PolygonIndex) Descriptor() ([]byte, []int) { + return file_timezone_proto_rawDescGZIP(), []int{2} +} + +func (x *PolygonIndex) GetId() uint64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *PolygonIndex) GetTzid() string { + if x != nil { + return x.Tzid + } + return "" +} + +func (x *PolygonIndex) GetMax() *Coord { + if x != nil { + return x.Max + } + return nil +} + +func (x *PolygonIndex) GetMin() *Coord { + if x != nil { + return x.Min + } + return nil +} + +var File_timezone_proto protoreflect.FileDescriptor + +var file_timezone_proto_rawDesc = []byte{ + 0x0a, 0x0e, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x22, 0x2b, 0x0a, 0x05, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x4c, 0x61, 0x74, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x02, 0x52, 0x03, 0x4c, 0x61, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x4c, + 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x02, 0x52, 0x03, 0x4c, 0x6f, 0x6e, 0x22, 0x5d, 0x0a, + 0x07, 0x50, 0x6f, 0x6c, 0x79, 0x67, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x03, 0x4d, 0x61, 0x78, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x06, 0x2e, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x52, 0x03, 0x4d, + 0x61, 0x78, 0x12, 0x18, 0x0a, 0x03, 0x4d, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x06, 0x2e, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x52, 0x03, 0x4d, 0x69, 0x6e, 0x12, 0x1e, 0x0a, 0x06, + 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x06, 0x2e, 0x43, + 0x6f, 0x6f, 0x72, 0x64, 0x52, 0x06, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x66, 0x0a, 0x0c, + 0x50, 0x6f, 0x6c, 0x79, 0x67, 0x6f, 0x6e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x0e, 0x0a, 0x02, + 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, + 0x54, 0x7a, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x54, 0x7a, 0x69, 0x64, + 0x12, 0x18, 0x0a, 0x03, 0x4d, 0x61, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x06, 0x2e, + 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x52, 0x03, 0x4d, 0x61, 0x78, 0x12, 0x18, 0x0a, 0x03, 0x4d, 0x69, + 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x06, 0x2e, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x52, + 0x03, 0x4d, 0x69, 0x6e, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x65, 0x76, 0x61, 0x6e, 0x6f, 0x62, 0x65, 0x72, 0x68, 0x6f, 0x6c, 0x73, 0x74, + 0x65, 0x72, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, + 0x70, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_timezone_proto_rawDescOnce sync.Once + file_timezone_proto_rawDescData = file_timezone_proto_rawDesc +) + +func file_timezone_proto_rawDescGZIP() []byte { + file_timezone_proto_rawDescOnce.Do(func() { + file_timezone_proto_rawDescData = protoimpl.X.CompressGZIP(file_timezone_proto_rawDescData) + }) + return file_timezone_proto_rawDescData +} + +var file_timezone_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_timezone_proto_goTypes = []interface{}{ + (*Coord)(nil), // 0: Coord + (*Polygon)(nil), // 1: Polygon + (*PolygonIndex)(nil), // 2: PolygonIndex +} +var file_timezone_proto_depIdxs = []int32{ + 0, // 0: Polygon.Max:type_name -> Coord + 0, // 1: Polygon.Min:type_name -> Coord + 0, // 2: Polygon.Coords:type_name -> Coord + 0, // 3: PolygonIndex.Max:type_name -> Coord + 0, // 4: PolygonIndex.Min:type_name -> Coord + 5, // [5:5] is the sub-list for method output_type + 5, // [5:5] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_timezone_proto_init() } +func file_timezone_proto_init() { + if File_timezone_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_timezone_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Coord); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_timezone_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Polygon); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_timezone_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PolygonIndex); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_timezone_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_timezone_proto_goTypes, + DependencyIndexes: file_timezone_proto_depIdxs, + MessageInfos: file_timezone_proto_msgTypes, + }.Build() + File_timezone_proto = out.File + file_timezone_proto_rawDesc = nil + file_timezone_proto_goTypes = nil + file_timezone_proto_depIdxs = nil +} diff --git a/pb/timezone.proto b/pb/timezone.proto new file mode 100644 index 0000000..14ea824 --- /dev/null +++ b/pb/timezone.proto @@ -0,0 +1,26 @@ +// Copyright 2021 Tamás Gulácsi. +// +// SPDX-License-Identifier: MIT + +syntax = "proto3"; +option go_package = "github.com/evanoberholster/timezoneLookup/pb"; +//import "google/protobuf/timestamp.proto"; + +message Coord { + float Lat = 1; + float Lon = 2; +} + +message Polygon { + Coord Max = 1; + Coord Min = 2; + repeated Coord Coords = 4; +} + +message PolygonIndex { + uint64 Id = 1; + string Tzid = 2; + Coord Max = 3; + Coord Min = 4; +} + diff --git a/timezone.go b/timezone.go index b0da676..cbd0afe 100644 --- a/timezone.go +++ b/timezone.go @@ -1,33 +1,43 @@ +// Copyright 2018 Evan Oberholster. +// +// SPDX-License-Identifier: MIT + package timezoneLookup + import ( - "os" - "time" "errors" "fmt" + "os" "runtime" - "encoding/json" + "time" + + "github.com/evanoberholster/timezoneLookup/pb" + json "github.com/goccy/go-json" ) +//go:generate go install google.golang.org/protobuf/cmd/protoc-gen-go@latest +//go:generate protoc --proto_path=pb --go_out=pb --go_opt=paths=source_relative pb/timezone.proto + const ( WithSnappy = true - NoSnappy = false + NoSnappy = false // Errors - errNotExistGeoJSON = "Error: GeoJSON file does not exist" - errExistDatabase = "Error: Destination Database file already exists" - errNotExistDatabase = "Error: Database file does not exist" - errPolygonNotFound = "Error: Polygon for Timezone not found" - errTimezoneNotFound = "Error: Timezone not found" + errNotExistGeoJSON = "Error: GeoJSON file does not exist" + errExistDatabase = "Error: Destination Database file already exists" + errNotExistDatabase = "Error: Database file does not exist" + errPolygonNotFound = "Error: Polygon for Timezone not found" + errTimezoneNotFound = "Error: Timezone not found" errDatabaseTypeUknown = "Error: Database type unknown" ) type TimezoneInterface interface { - CreateTimezones(jsonFilename string) (error) - LoadTimezones() (error) - Query(q Coord) (string, error) + CreateTimezones(jsonFilename string) error + LoadTimezones() error + Query(q Coord) (string, error) Close() } - + type TimezoneGeoJSON struct { Type string `json:"type"` Features []struct { @@ -43,26 +53,64 @@ type TimezoneGeoJSON struct { } type Timezone struct { - Tzid string `json:"tzid"` - Polygons []Polygon `json:"polygons"` + Tzid string `json:"tzid"` + Polygons []Polygon `json:"polygons"` } type Polygon struct { - Max Coord `json:"max"` - Min Coord `json:"min"` - Coords []Coord `json:"coords"` + Max Coord `json:"max"` + Min Coord `json:"min"` + Coords []Coord `json:"coords"` +} + +func (dst *Polygon) FromPB(src *pb.Polygon) { + dst.Max.FromPB(src.Max) + dst.Min.FromPB(src.Min) + if cap(dst.Coords) < len(src.Coords) { + dst.Coords = make([]Coord, len(src.Coords)) + } else { + dst.Coords = dst.Coords[:len(src.Coords)] + } + for i, c := range src.Coords { + dst.Coords[i].FromPB(c) + } +} +func (src Polygon) ToPB(dst *pb.Polygon) { + dst.Reset() + dst.Max = src.Max.ToPB(dst.Max) + dst.Min = src.Min.ToPB(dst.Min) + if cap(dst.Coords) < len(src.Coords) { + dst.Coords = make([]*pb.Coord, len(src.Coords)) + } else { + dst.Coords = dst.Coords[:len(src.Coords)] + } + for i, c := range src.Coords { + dst.Coords[i] = c.ToPB(dst.Coords[i]) + } } type Coord struct { - Lat float32 `json:"lat"` - Lon float32 `json:"lon"` -} + Lat float32 `json:"lat"` + Lon float32 `json:"lon"` +} + +func (src Coord) ToPB(dst *pb.Coord) *pb.Coord { + if dst == nil { + return &pb.Coord{Lat: src.Lat, Lon: src.Lon} + } + dst.Reset() + dst.Lat, dst.Lon = src.Lat, src.Lon + return dst +} +func (dst *Coord) FromPB(src *pb.Coord) { + dst.Lat, dst.Lon = src.Lat, src.Lon +} type Config struct { - DatabaseName string - DatabaseType string - Snappy bool - Encoding string + DatabaseName string + DatabaseType string + Snappy bool + Encoding encoding } var Tz TimezoneInterface @@ -93,7 +141,7 @@ func TimezonesFromGeoJSON(filename string) ([]Timezone, error) { for dec.More() { var js TimezoneGeoJSON - + err := dec.Decode(&js) if err != nil { return timeZones, err @@ -101,10 +149,10 @@ func TimezonesFromGeoJSON(filename string) ([]Timezone, error) { for _, tz := range js.Features { t := Timezone{Tzid: tz.Properties.Tzid} switch tz.Geometry.Item { - case "Polygon": - t.decodePolygons(tz.Geometry.Coordinates) - case "MultiPolygon": - t.decodeMultiPolygons(tz.Geometry.Coordinates) + case "Polygon": + t.decodePolygons(tz.Geometry.Coordinates) + case "MultiPolygon": + t.decodeMultiPolygons(tz.Geometry.Coordinates) } timeZones = append(timeZones, t) } @@ -114,78 +162,85 @@ func TimezonesFromGeoJSON(filename string) ([]Timezone, error) { return timeZones, nil } -func (t *Timezone)decodePolygons(polys []interface{}) { //1 +func (t *Timezone) decodePolygons(polys []interface{}) { //1 for _, points := range polys { p := t.newPolygon() for _, point := range points.([]interface{}) { //3 - p.updatePolygon(point.([]interface{})) + p.updatePolygon(point.([]interface{})) } t.Polygons = append(t.Polygons, p) } } -func (t *Timezone)decodeMultiPolygons(polys []interface{}) { //1 +func (t *Timezone) decodeMultiPolygons(polys []interface{}) { //1 for _, v := range polys { p := t.newPolygon() for _, points := range v.([]interface{}) { // 2 for _, point := range points.([]interface{}) { //3 - p.updatePolygon(point.([]interface{})) + p.updatePolygon(point.([]interface{})) } } t.Polygons = append(t.Polygons, p) } } -func (t *Timezone)newPolygon() (Polygon) { +func (t *Timezone) newPolygon() Polygon { return Polygon{ - Max: Coord{ Lat: -90, Lon: -180, }, - Min: Coord{ Lat: 90, Lon: 180, }, - } + Max: Coord{Lat: -90, Lon: -180}, + Min: Coord{Lat: 90, Lon: 180}, + } } -func (p *Polygon)updatePolygon(xy []interface{}) { +func (p *Polygon) updatePolygon(xy []interface{}) { lon := float32(xy[0].(float64)) lat := float32(xy[1].(float64)) // Update max and min limits - if p.Max.Lat < lat { p.Max.Lat = lat } - if p.Max.Lon < lon { p.Max.Lon = lon } - if p.Min.Lat > lat { p.Min.Lat = lat } - if p.Min.Lon > lon { p.Min.Lon = lon } + if p.Max.Lat < lat { + p.Max.Lat = lat + } + if p.Max.Lon < lon { + p.Max.Lon = lon + } + if p.Min.Lat > lat { + p.Min.Lat = lat + } + if p.Min.Lon > lon { + p.Min.Lon = lon + } // add Coords to Polygon - p.Coords = append(p.Coords, Coord{Lat:lat, Lon:lon}) + p.Coords = append(p.Coords, Coord{Lat: lat, Lon: lon}) } -func (p *Polygon)contains(queryPt Coord) bool { - if len(p.Coords) < 3 { - return false - } - in := rayIntersectsSegment(queryPt, p.Coords[len(p.Coords)-1], p.Coords[0]) - for i := 1; i < len(p.Coords); i++ { - if rayIntersectsSegment(queryPt, p.Coords[i-1], p.Coords[i]) { - in = !in - } - } - return in +func (p *Polygon) contains(queryPt Coord) bool { + if len(p.Coords) < 3 { + return false + } + in := rayIntersectsSegment(queryPt, p.Coords[len(p.Coords)-1], p.Coords[0]) + for i := 1; i < len(p.Coords); i++ { + if rayIntersectsSegment(queryPt, p.Coords[i-1], p.Coords[i]) { + in = !in + } + } + return in } - func rayIntersectsSegment(p, a, b Coord) bool { - return (a.Lon > p.Lon) != (b.Lon > p.Lon) && - p.Lat < (b.Lat-a.Lat)*(p.Lon-a.Lon)/(b.Lon-a.Lon)+a.Lat + return (a.Lon > p.Lon) != (b.Lon > p.Lon) && + p.Lat < (b.Lat-a.Lat)*(p.Lon-a.Lon)/(b.Lon-a.Lon)+a.Lat } func PrintMemUsage() { - var m runtime.MemStats - runtime.ReadMemStats(&m) - // For info on each, see: https://golang.org/pkg/runtime/#MemStats - fmt.Printf("Allocated Memory = %v MiB", bToMb(m.Alloc)) - fmt.Printf("\tTotal Allocated Memory = %v MiB", bToMb(m.TotalAlloc)) - fmt.Printf("\tSystem Memory = %v MiB", bToMb(m.Sys)) - fmt.Printf("\tNumber of GC = %v\n", m.NumGC) + var m runtime.MemStats + runtime.ReadMemStats(&m) + // For info on each, see: https://golang.org/pkg/runtime/#MemStats + fmt.Printf("Allocated Memory = %v MiB", bToMb(m.Alloc)) + fmt.Printf("\tTotal Allocated Memory = %v MiB", bToMb(m.TotalAlloc)) + fmt.Printf("\tSystem Memory = %v MiB", bToMb(m.Sys)) + fmt.Printf("\tNumber of GC = %v\n", m.NumGC) } func bToMb(b uint64) uint64 { - return b / 1024 / 1024 + return b / 1024 / 1024 }