Skip to content

Commit

Permalink
Merge pull request #2 from catmullet/speed_update
Browse files Browse the repository at this point in the history
speed update/refactor
  • Loading branch information
catmullet authored Aug 31, 2021
2 parents 37238d3 + ef4586c commit 96ff9a3
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 130 deletions.
Binary file added geodb/combined-with-oceans.json.snappy
Binary file not shown.
10 changes: 10 additions & 0 deletions geodb/embed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package geodb

import (
"embed"
)

var (
//go:embed *
GeoDbEmbedDirectory embed.FS
)
183 changes: 58 additions & 125 deletions geojson.go
Original file line number Diff line number Diff line change
@@ -1,36 +1,30 @@
package tz

import (
"archive/zip"
"bytes"
"encoding/gob"
"encoding/json"
"fmt"
"github.com/golang/snappy"
"io"
"log"
"os"
"path/filepath"
"runtime"
"github.com/catmullet/tz/geodb"
"math"
"sort"
"strings"
"time"
)

var currentDirectory = func() string {
var _, currentFilePath, _, _ = runtime.Caller(0)
return strings.ReplaceAll(currentFilePath, "/geojson.go", "")
}()
const (
timeZonesFilename = "combined-with-oceans.json.snappy"
)

type GeoJsonLookup interface {
TimeZone(lat, lon float64) string
Location(lat, lon float64) (*time.Location, error)
}

type TimeZoneCollection struct {
type Collection struct {
Features []*Feature
}

type Feature struct {
Geometry Geometry
Properties struct {
Tzid string
}
Properties map[string]string
}

type Geometry struct {
Expand Down Expand Up @@ -63,64 +57,17 @@ var multiPolygon struct {
Coordinates [][][][]float64
}

func NewGeoJsonTimeZoneLookup(geoJsonFile string, logOutput ...io.Writer) (TimeZoneLookup, error) {

logger := log.New(io.MultiWriter(logOutput...), "tz", log.Lshortfile)
logger.Println("initializing...")

var timeZoneFile = filepath.Join(currentDirectory, "timezones-with-oceans.geojson.zip")

if len(geoJsonFile) == 0 {
geoJsonFile = os.Getenv("GEO_JSON_FILE")
}
if len(geoJsonFile) == 0 {
logger.Println("no geo time zone file specified, using default")
geoJsonFile = timeZoneFile
}

var fc = &TimeZoneCollection{
Features: make([]*Feature, 500),
}

if err := findCachedModel(fc); err == nil {
logger.Println("cached model found")
return fc, nil
} else {
err = nil
}

g, err := zip.OpenReader(geoJsonFile)
if err != nil {
return nil, fmt.Errorf("failed to read geojson file: %w", err)
}

if len(g.File) == 0 {
return nil, fmt.Errorf("zip file is empty")
}
var file = g.File[0]
var buf = bytes.NewBuffer([]byte{})
src, err := file.Open()
if err != nil {
return nil, fmt.Errorf("failed to unzip file: %w", err)
}

if _, err := io.Copy(buf, src); err != nil {
return nil, fmt.Errorf("failed to unzip file: %w", err)
}

if err := src.Close(); err != nil {
return nil, fmt.Errorf("failed to read geojson file: %w", err)
}
if err := g.Close(); err != nil {
return nil, fmt.Errorf("failed to read geojson file: %w", err)
}
func NewTZ() (GeoJsonLookup, error) {
var (
fc = &Collection{Features: make([]*Feature, 500)}
)

if err := json.NewDecoder(buf).Decode(fc); err != nil {
return nil, fmt.Errorf("failed to read geojson file: %w", err)
if b, err := newLocalGeoStorage(geodb.GeoDbEmbedDirectory).LoadFile(timeZonesFilename, &fc); err != nil || len(b) == 0 {
return nil, fmt.Errorf("failed to load file, %w", err)
}

for _, feat := range fc.Features {
f := feat
for i := range fc.Features {
f := fc.Features[i]
sort.SliceStable(f.Geometry.Coordinates, func(i, j int) bool {
return f.Geometry.Coordinates[i].MinPoint.Lon <= f.Geometry.Coordinates[j].MinPoint.Lon
})
Expand All @@ -130,36 +77,7 @@ func NewGeoJsonTimeZoneLookup(geoJsonFile string, logOutput ...io.Writer) (TimeZ
return fc.Features[i].Geometry.MinPoint.Lon <= fc.Features[j].Geometry.MinPoint.Lon
})

logger.Println("finished")
return fc, createCachedModel(fc)
}

func findCachedModel(fc *TimeZoneCollection) error {
cache, err := os.Open(filepath.Join(currentDirectory, "tzdata.snappy"))
if err != nil {
return err
}
defer cache.Close()
snp := snappy.NewReader(cache)
dec := gob.NewDecoder(snp)
if err := dec.Decode(fc); err != nil && err != io.EOF {
return err
}
return nil
}

func createCachedModel(fc *TimeZoneCollection) error {
cache, err := os.Create(filepath.Join(currentDirectory, "tzdata.snappy"))
if err != nil {
return err
}
defer cache.Close()
snp := snappy.NewBufferedWriter(cache)
enc := gob.NewEncoder(snp)
if err := enc.Encode(fc); err != nil {
return err
}
return nil
return fc, nil
}

func (g *Geometry) UnmarshalJSON(data []byte) (err error) {
Expand Down Expand Up @@ -230,51 +148,66 @@ func updateMaxMin(maxPoint, minPoint *Point, lat, lon float64) {
}
}

func (fc TimeZoneCollection) TimeZone(lat, lon float64) (tz string) {
func (fc Collection) Location(lat, lon float64) (*time.Location, error) {
if tz := fc.TimeZone(lat, lon); tz != "" {
return time.LoadLocation(tz)
}
return nil, fmt.Errorf("failed to find time zone")
}

// TimeZone Recurse over lower find function for lat lon.
// First shrinking the polygon for search and if we find it return it. If we didn't find it search on full polygon.
func (fc Collection) TimeZone(lat, lon float64) string {
var start, end = 0.001, 0.0001
if result := fc.find(lat, lon, start); result != "" {
return result
}
return fc.find(lat, lon, end)
}

func (fc Collection) find(lat, lon, percentage float64) string {
for _, feat := range fc.Features {
f := feat
tzString := f.Properties.Tzid
if f.Geometry.MinPoint.Lat < lat &&
f.Geometry.MinPoint.Lon < lon &&
f.Geometry.MaxPoint.Lat > lat &&
f.Geometry.MaxPoint.Lon > lon {
properties := f.Properties
if f.Geometry.MinPoint.Lat <= lat &&
f.Geometry.MinPoint.Lon <= lon &&
f.Geometry.MaxPoint.Lat >= lat &&
f.Geometry.MaxPoint.Lon >= lon {
for _, c := range f.Geometry.Coordinates {
coord := c
if coord.MinPoint.Lat < lat &&
coord.MinPoint.Lon < lon &&
coord.MaxPoint.Lat > lat &&
coord.MaxPoint.Lon > lon {
if coord.contains(Point{lon, lat}) {
return tzString
if coord.MinPoint.Lat <= lat &&
coord.MinPoint.Lon <= lon &&
coord.MaxPoint.Lat >= lat &&
coord.MaxPoint.Lon >= lon {
if coord.contains(Point{lon, lat},
// get a percentage of the polygon, either shrinking it or leaving it alone.
int(math.Max(float64(len(coord.Polygon))*percentage, 1))) {
return properties["tzid"]
}
}
}
}
}
return
}

func (fc TimeZoneCollection) Location(lat, lon float64) (loc *time.Location, err error) {
return time.LoadLocation(fc.TimeZone(lat, lon))
return ""
}

func (c Coordinates) contains(point Point) bool {
func (c Coordinates) contains(point Point, indexjump int) bool {
var polygon = c.Polygon
if windingNumber(point.Lat, point.Lon, polygon) == 0 {
if windingNumber(point.Lat, point.Lon, polygon, indexjump) == 0 {
return false
}
return true
}

func windingNumber(lat, lon float64, polygon []Point) int {
func windingNumber(lat, lon float64, polygon []Point, indexjump int) int {
if len(polygon) < 3 {
return 0
}

var wn = 0
var edgeCount = len(polygon) - 5
var edgeCount = len(polygon) - indexjump

for i, j := 0, 5; i < edgeCount; i, j = i+5, j+5 {
for i, j := 0, indexjump; i < edgeCount; i, j = i+indexjump, j+indexjump {
var apLat, apLon, bLat, bLon = polygon[i].Lat, polygon[i].Lon, polygon[j].Lat, polygon[j].Lon
if apLat <= lat {
if bLat > lat {
Expand Down
6 changes: 3 additions & 3 deletions geojson_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"time"
)

var tzl TimeZoneLookup
var tzl GeoJsonLookup

type coords struct {
Lat float64
Expand Down Expand Up @@ -82,7 +82,7 @@ var querys = []coords{

func TestMain(m *testing.M) {
var err error
tzl, err = NewGeoJsonTimeZoneLookup("timezones-with-oceans.geojson.zip", os.Stdout)
tzl, err = NewTZ()
if err != nil {
log.Println(err)
os.Exit(1)
Expand Down Expand Up @@ -163,7 +163,7 @@ func BenchmarkLocation(b *testing.B) {
for _, bm := range querys {
b.Run(fmt.Sprintf("coordinates_%v_%v", bm.Lat, bm.Lon), func(b *testing.B) {
for i := 0; i < b.N; i++ {
tzl.Location(bm.Lat, bm.Lon)
_, _ = tzl.Location(bm.Lat, bm.Lon)
}
})
}
Expand Down
2 changes: 1 addition & 1 deletion geojson_test_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -994,7 +994,7 @@ var testdataset = []timeZoneTestData{
{Lat: 33.93812, Lon: -86.01397, Offset: -5},
{Lat: 33.44879, Lon: -86.38571, Offset: -5},
{Lat: 30.4724, Lon: -87.66619, Offset: -5},
{Lat: 32.509467, Lon: -85.130631, Offset: -4},
{Lat: 32.509467, Lon: -85.130631, Offset: -5},
{Lat: 34.70135, Lon: -86.6874, Offset: -5},
{Lat: 34.88397, Lon: -87.67059, Offset: -5},
{Lat: 32.39265, Lon: -86.27708, Offset: -5},
Expand Down
6 changes: 6 additions & 0 deletions geostorage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package tz

type geoStorage interface {
StoreFile(filename string, obj interface{}) error
LoadFile(filename string, obj interface{}) ([]byte, error)
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module github.com/catmullet/tz

go 1.15
go 1.16

require github.com/golang/snappy v0.0.3
38 changes: 38 additions & 0 deletions local_geostorage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package tz

import (
"embed"
"encoding/json"
"github.com/golang/snappy"
"log"
"strings"
)

type LocalGeoStorage struct {
efs embed.FS
}

func newLocalGeoStorage(fs embed.FS) geoStorage {
return &LocalGeoStorage{
efs: fs,
}
}

func (lgs LocalGeoStorage) StoreFile(_ string, _ interface{}) error {
log.Fatalln("local geo storage: store file not used")
return nil
}

func (lgs LocalGeoStorage) LoadFile(filename string, obj interface{}) ([]byte, error) {
var decodedJson, err = lgs.efs.ReadFile(filename)
if strings.Contains(filename, "snappy") {
decodedJson, err = snappy.Decode(nil, decodedJson)
if err != nil {
return decodedJson, err
}
}
if err != nil {
return decodedJson, err
}
return decodedJson, json.Unmarshal(decodedJson, &obj)
}

0 comments on commit 96ff9a3

Please sign in to comment.