photoprism-client-go/vendor/gopkg.in/ugjka/go-tz.v2/tz/tz.go

161 lines
3.5 KiB
Go
Raw Normal View History

// Package tz provides timezone lookup for a given location
//
// Features
//
// * The timezone shapefile is embedded in the build binary using go-bindata
//
// * Supports overlapping zones
//
// * You can load your own geojson shapefile if you want
//
// * Sub millisecond lookup even on old hardware
//
// Problems
//
// * The shapefile is simplified using a lossy method so it may be innacurate along the borders
//
// * This is purerly in-memory. Uses ~50MB of ram
package tz
import (
"bytes"
"compress/gzip"
"encoding/json"
"errors"
"fmt"
"io"
"math"
"runtime/debug"
)
func init() {
load()
}
func load() {
g, err := gzip.NewReader(bytes.NewBuffer(tzShapeFile))
if err != nil {
panic(err)
}
defer g.Close()
if err := json.NewDecoder(g).Decode(&tzdata); err != nil {
panic(err)
}
buildCenterCache()
debug.FreeOSMemory()
}
var tzdata FeatureCollection
type centers map[string][]Point
var centerCache centers
// Point describes a location by Latitude and Longitude
type Point struct {
Lon float64
Lat float64
}
// ErrNoZoneFound is returned when a zone for the given point is not found in the shapefile
var ErrNoZoneFound = errors.New("no corresponding zone found in shapefile")
// ErrOutOfRange is returned when latitude exceeds 90 degrees or longitude exceeds 180 degrees
var ErrOutOfRange = errors.New("point's coordinates out of range")
// GetZone returns a slice of strings containing time zone id's for a given Point
func GetZone(p Point) (tzid []string, err error) {
if p.Lon > 180 || p.Lon < -180 || p.Lat > 90 || p.Lat < -90 {
return nil, ErrOutOfRange
}
var id string
for _, v := range tzdata.Features {
if v.Properties.Tzid == "" {
continue
}
id = v.Properties.Tzid
polys := v.Geometry.Coordinates
bboxes := v.Geometry.BoundingBoxes
for i := 0; i < len(polys); i++ {
//Check bounding box first
if !inBoundingBox(bboxes[i], &p) {
continue
}
if polygon(polys[i]).contains(&p) {
tzid = append(tzid, id)
}
}
}
if len(tzid) > 0 {
return tzid, nil
}
return getClosestZone(&p)
}
func distanceFrom(p1, p2 *Point) float64 {
d0 := (p1.Lon - p2.Lon)
d1 := (p1.Lat - p2.Lat)
return math.Sqrt(d0*d0 + d1*d1)
}
func getClosestZone(point *Point) (tzid []string, err error) {
mindist := math.Inf(1)
var winner string
for id, v := range centerCache {
for _, p := range v {
tmp := distanceFrom(&p, point)
if tmp < mindist {
mindist = tmp
winner = id
}
}
}
// Limit search radius
if mindist > 2.0 {
return getNauticalZone(point)
}
return append(tzid, winner), nil
}
func getNauticalZone(point *Point) (tzid []string, err error) {
z := point.Lon / 7.5
z = (math.Abs(z) + 1) / 2
z = math.Floor(z)
if z == 0 {
return append(tzid, "Etc/GMT"), nil
}
if point.Lon < 0 {
return append(tzid, fmt.Sprintf("Etc/GMT+%.f", z)), nil
}
return append(tzid, fmt.Sprintf("Etc/GMT-%.f", z)), nil
}
//BuildCenterCache builds centers for polygons
func buildCenterCache() {
centerCache = make(centers)
var tzid string
for _, v := range tzdata.Features {
if v.Properties.Tzid == "" {
continue
}
tzid = v.Properties.Tzid
for _, poly := range v.Geometry.Coordinates {
centerCache[tzid] = append(centerCache[tzid], polygon(poly).centroid())
}
}
}
// LoadGeoJSON loads a custom GeoJSON shapefile from a Reader
func LoadGeoJSON(r io.Reader) error {
tzdata = FeatureCollection{}
err := json.NewDecoder(r).Decode(&tzdata)
if err != nil {
load()
return err
}
buildCenterCache()
debug.FreeOSMemory()
return nil
}