diff --git a/Convert.go b/Convert.go index df881a9..a36f52f 100644 --- a/Convert.go +++ b/Convert.go @@ -33,9 +33,6 @@ const ( WGS84 = EPSG4326 ) -// ensure only one person is updating our cache of converters at a time -var cacheLock = sync.Mutex{} - // Convert performs a conversion from a 4326 coordinate system (lon/lat // degrees, 2D) to the given projected system (x/y meters, 2D). // @@ -80,7 +77,9 @@ func Inverse(src EPSGCode, input []float64) ([]float64, error) { // CustomProjection provides write-only access to the internal projection list // so that projections may be added without having to modify the library code. func CustomProjection(code EPSGCode, str string) { + projStringLock.Lock() projStrings[code] = str + projStringLock.Unlock() } //--------------------------------------------------------------------------- @@ -94,19 +93,48 @@ type conversion struct { converter core.IConvertLPToXY } -var conversions = map[EPSGCode]*conversion{} +var ( + // cacheLock ensure only one person is updating our cache of converters at a time + cacheLock = sync.Mutex{} + conversions = map[EPSGCode]*conversion{} + + projStringLock = sync.RWMutex{} + projStrings = map[EPSGCode]string{ + EPSG3395: "+proj=merc +lon_0=0 +k=1 +x_0=0 +y_0=0 +datum=WGS84", // TODO: support +units=m +no_defs + EPSG3857: "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0", // TODO: support +units=m +nadgrids=@null +wktext +no_defs + EPSG4087: "+proj=eqc +lat_ts=0 +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84", // TODO: support +units=m +no_defs + } +) + +// AvailableConversions returns a list of conversion that the system knows about +func AvailableConversions() (ret []EPSGCode) { + projStringLock.RLock() + defer projStringLock.RUnlock() + if len(projStrings) == 0 { + return nil + } + ret = make([]EPSGCode, 0, len(projStrings)) + for k := range projStrings { + ret = append(ret, k) + } + return ret +} -var projStrings = map[EPSGCode]string{ - EPSG3395: "+proj=merc +lon_0=0 +k=1 +x_0=0 +y_0=0 +datum=WGS84", // TODO: support +units=m +no_defs - EPSG3857: "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0", // TODO: support +units=m +nadgrids=@null +wktext +no_defs - EPSG4087: "+proj=eqc +lat_ts=0 +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84", // TODO: support +units=m +no_defs +// IsKnownConversionSRID returns if we know about the conversion +func IsKnownConversionSRID(srid EPSGCode) bool { + projStringLock.RLock() + defer projStringLock.RUnlock() + _, ok := projStrings[srid] + return ok } // newConversion creates a conversion object for the destination systems. If // such a conversion already exists in the cache, use that. func newConversion(dest EPSGCode) (*conversion, error) { + projStringLock.RLock() str, ok := projStrings[dest] + projStringLock.RUnlock() if !ok { return nil, fmt.Errorf("epsg code is not a supported projection") } diff --git a/cmd/proj/proj.go b/cmd/proj/proj.go index 17a9ef2..a99603b 100644 --- a/cmd/proj/proj.go +++ b/cmd/proj/proj.go @@ -35,9 +35,9 @@ func Main(inS io.Reader, outS io.Writer, args []string) error { // unverbosify all the things merror.ShowSource = false - mlog.DebugEnabled = false - mlog.InfoEnabled = false - mlog.ErrorEnabled = false + mlog.DisableDebug() + mlog.DisableInfo() + mlog.DisableError() cli := flag.NewFlagSet(args[0], flag.ContinueOnError) cli.SetOutput(outS) @@ -67,9 +67,9 @@ func Main(inS io.Reader, outS io.Writer, args []string) error { } merror.ShowSource = true - mlog.DebugEnabled = true - mlog.InfoEnabled = true - mlog.ErrorEnabled = true + mlog.EnableDebug() + mlog.EnableInfo() + mlog.EnableError() } // handle "-epsg" usage, using the Convert API diff --git a/core/ConvertLPToXY.go b/core/ConvertLPToXY.go index 5c6135e..6359085 100644 --- a/core/ConvertLPToXY.go +++ b/core/ConvertLPToXY.go @@ -243,27 +243,29 @@ func (op *ConvertLPToXY) inverseFinalize(coo *CoordLP) (*CoordLP, error) { sys := op.System - if sys.Left == IOUnitsAngular { - - if sys.Right != IOUnitsAngular { - /* Distance from central meridian, taking system zero meridian into account */ - coo.Lam = coo.Lam + sys.FromGreenwich + sys.Lam0 + // if left is not in radians return the value as is. + if sys.Left != IOUnitsAngular { + return coo, nil + } - /* adjust longitude to central meridian */ - if !sys.Over { - coo.Lam = support.Adjlon(coo.Lam) - } + if sys.Right != IOUnitsAngular { + /* Distance from central meridian, taking system zero meridian into account */ + coo.Lam = coo.Lam + sys.FromGreenwich + sys.Lam0 - if coo.Lam == math.MaxFloat64 { - return coo, nil - } + /* adjust longitude to central meridian */ + if !sys.Over { + coo.Lam = support.Adjlon(coo.Lam) } - /* If input latitude was geocentrical, convert back to geocentrical */ - if sys.Geoc { - coo = GeocentricLatitude(sys, DirectionForward, coo) + if coo.Lam == math.MaxFloat64 { + return coo, nil } } + /* If input latitude was geocentrical, convert back to geocentrical */ + if sys.Geoc { + coo = GeocentricLatitude(sys, DirectionForward, coo) + } + return coo, nil } diff --git a/core/OperationDescription.go b/core/OperationDescription.go index 15cda16..3ef98e8 100644 --- a/core/OperationDescription.go +++ b/core/OperationDescription.go @@ -8,6 +8,8 @@ package core import ( + "fmt" + "github.com/go-spatial/proj/merror" ) @@ -58,7 +60,7 @@ func RegisterConvertLPToXY( _, ok := OperationDescriptionTable[id] if ok { - panic(99) + panic(fmt.Sprintf("duplicate operation description id '%s' : %s", id, description)) } OperationDescriptionTable[id] = pi } @@ -73,7 +75,7 @@ func (desc *OperationDescription) CreateOperation(sys *System) (IOperation, erro return nil, merror.New(merror.NotYetSupported) } -// IsConvertLPToXY returns true iff the operation can be casted to an IConvertLPToXY +// IsConvertLPToXY returns true iff the operation can be cast to an IConvertLPToXY func (desc *OperationDescription) IsConvertLPToXY() bool { return desc.OperationType == OperationTypeConversion && desc.InputType == CoordTypeLP && diff --git a/core/OperationDescription_test.go b/core/OperationDescription_test.go index acbe6ba..7de1666 100644 --- a/core/OperationDescription_test.go +++ b/core/OperationDescription_test.go @@ -11,13 +11,12 @@ import ( "testing" "github.com/go-spatial/proj/core" - - "github.com/stretchr/testify/assert" ) func TestOperationDescription(t *testing.T) { - assert := assert.New(t) opDesc := core.OperationDescriptionTable["utm"] - assert.NotNil(opDesc) + if opDesc == nil { + t.Errorf("operaton description table for utm is nil") + } } diff --git a/core/System.go b/core/System.go index 608ff1c..b368c01 100644 --- a/core/System.go +++ b/core/System.go @@ -23,10 +23,10 @@ type DatumType int // All the DatumType constants (taken directly from the C) const ( DatumTypeUnknown DatumType = 0 - DatumType3Param = 1 - DatumType7Param = 2 - DatumTypeGridShift = 3 - DatumTypeWGS84 = 4 /* WGS84 (or anything considered equivalent) */ + DatumType3Param DatumType = 1 + DatumType7Param DatumType = 2 + DatumTypeGridShift DatumType = 3 + DatumTypeWGS84 DatumType = 4 /* WGS84 (or anything considered equivalent) */ ) // IOUnitsType is the enum for the types of input/output units we support @@ -35,10 +35,10 @@ type IOUnitsType int // All the IOUnitsType constants const ( IOUnitsWhatever IOUnitsType = 0 /* Doesn't matter (or depends on pipeline neighbours) */ - IOUnitsClassic = 1 /* Scaled meters (right), projected system */ - IOUnitsProjected = 2 /* Meters, projected system */ - IOUnitsCartesian = 3 /* Meters, 3D cartesian system */ - IOUnitsAngular = 4 /* Radians */ + IOUnitsClassic IOUnitsType = 1 /* Scaled meters (right), projected system */ + IOUnitsProjected IOUnitsType = 2 /* Meters, projected system */ + IOUnitsCartesian IOUnitsType = 3 /* Meters, 3D cartesian system */ + IOUnitsAngular IOUnitsType = 4 /* Radians */ ) // DirectionType is the enum for the operation's direction @@ -47,8 +47,8 @@ type DirectionType int // All the DirectionType constants const ( DirectionForward DirectionType = 1 /* Forward */ - DirectionIdentity = 0 /* Do nothing */ - DirectionInverse = -1 /* Inverse */ + DirectionIdentity DirectionType = 0 /* Do nothing */ + DirectionInverse DirectionType = -1 /* Inverse */ ) const epsLat = 1.0e-12 diff --git a/mlog/Log.go b/mlog/Log.go index b2d7a64..b6f60a8 100644 --- a/mlog/Log.go +++ b/mlog/Log.go @@ -10,6 +10,7 @@ package mlog import ( "encoding/json" "fmt" + "io" "log" "os" ) @@ -23,28 +24,44 @@ var InfoEnabled = true // ErrorEnabled controls whether Error log messages are generated var ErrorEnabled = true -var debugLogger, infoLogger, errorLogger *log.Logger +func EnableError() { + defaultLogger.EnableError() +} + +func EnableInfo() { + defaultLogger.EnableInfo() +} -func init() { - debugLogger = log.New(os.Stderr, "[DEBUG] ", log.Lshortfile) - infoLogger = log.New(os.Stderr, "[LOG] ", log.Lshortfile) - errorLogger = log.New(os.Stderr, "[ERROR] ", 0) +func EnableDebug() { + defaultLogger.EnableDebug() +} + +func DisableError() { + defaultLogger.DisableError() +} + +func DisableInfo() { + defaultLogger.DisableInfo() +} + +func DisableDebug() { + defaultLogger.DisableDebug() } // Debugf writes a debug message to stderr func Debugf(format string, v ...interface{}) { - if DebugEnabled { - s := fmt.Sprintf(format, v...) - _ = debugLogger.Output(2, s) + if !defaultLogger.debug.enabled { + return } + _ = defaultLogger.debug.Output(2, fmt.Sprintf(format, v...)) } // Printf writes a regular log message to stderr func Printf(format string, v ...interface{}) { - if InfoEnabled { - s := fmt.Sprintf(format, v...) - _ = infoLogger.Output(2, s) + if !defaultLogger.info.enabled { + return } + _ = defaultLogger.info.Output(2, fmt.Sprintf(format, v...)) } // Printv writes a variable as a regular log message to stderr @@ -52,22 +69,113 @@ func Printf(format string, v ...interface{}) { // TODO: would be nice if this could print the variable name // (and ideally the private fields too, if reflection allows // us access to them) -func Printv(v interface{}) { - if InfoEnabled { - //s := fmt.Sprintf("%#v", v) - b, err := json.MarshalIndent(v, "", " ") - if err != nil { - panic(err) - } - s := string(b) - _ = infoLogger.Output(2, s) +func Printv(v interface{}) error { + if !defaultLogger.info.enabled { + return nil } + b, err := json.MarshalIndent(v, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal indent: %w", err) + } + return defaultLogger.info.Output(2, string(b)) } // Error writes an error message to stderr func Error(err error) { - if ErrorEnabled { - s := err.Error() - _ = errorLogger.Output(2, s) + if !defaultLogger.error.enabled { + return + } + _ = defaultLogger.error.Output(2, err.Error()) +} + +type Outputer interface { + // Output writes the output for a logging event. + // The string s contains the text to print after the prefix specified by the flags of the Logger. + // A newline is appended if the last character of s is not already a newline. + // calldepth is used to recover the PC and is provided for generality, although at the moment on + // all pre-defined paths it will be 2 + Output(calldepth int, s string) error +} + +var defaultLogger = NewLoggerSingleOutput(os.Stderr) + +type levelLogger struct { + enabled bool + Outputer +} + +type Logger struct { + info levelLogger + error levelLogger + debug levelLogger +} + +func (l Logger) Debugf(format string, v ...interface{}) error { + if !l.debug.enabled { + return nil + } + return l.debug.Output(2, fmt.Sprintf(format, v...)) +} +func (l Logger) Printf(format string, v ...interface{}) error { + if !l.info.enabled { + return nil + } + return l.info.Output(2, fmt.Sprintf(format, v...)) +} + +func (l Logger) Printv(v interface{}) error { + if !l.info.enabled { + return nil + } + b, err := json.MarshalIndent(v, "", " ") + if err != nil { + return err + } + s := string(b) + return l.info.Output(2, s) +} + +func (l Logger) Error(err error) error { + if !l.error.enabled { + return nil + } + return l.error.Output(2, err.Error()) +} + +func (l *Logger) EnableError() { + l.error.enabled = true +} + +func (l *Logger) EnableInfo() { + l.info.enabled = true +} + +func (l *Logger) EnableDebug() { + l.debug.enabled = true +} + +func (l *Logger) DisableError() { + l.error.enabled = false +} + +func (l *Logger) DisableInfo() { + l.info.enabled = false +} + +func (l *Logger) DisableDebug() { + l.debug.enabled = false +} + +func NewLoggerSingleOutput(w io.Writer) Logger { + return Logger{ + info: levelLogger{ + Outputer: log.New(w, "[LOG] ", log.Lshortfile), + }, + debug: levelLogger{ + Outputer: log.New(w, "[DEBUG] ", log.Lshortfile), + }, + error: levelLogger{ + Outputer: log.New(w, "[ERROR] ", 0), + }, } } diff --git a/mlog/Log_test.go b/mlog/Log_test.go index 042e032..f26937e 100644 --- a/mlog/Log_test.go +++ b/mlog/Log_test.go @@ -8,102 +8,80 @@ package mlog_test import ( + "bufio" "fmt" "io" - "io/ioutil" - "log" "os" - "strings" - "syscall" + "regexp" "testing" "github.com/go-spatial/proj/mlog" - "github.com/stretchr/testify/assert" ) -func redirectStderr(f *os.File) int { - savedFd, err := syscall.Dup(int(os.Stderr.Fd())) - if err != nil { - panic(err) - } - err = syscall.Dup2(int(f.Fd()), int(os.Stderr.Fd())) - if err != nil { - panic(err) - } - return savedFd -} - -func unredirectStderr(savedFd int) { - err := syscall.Dup2(savedFd, int(os.Stderr.Fd())) - if err != nil { - log.Fatalf("Failed to redirect stderr to file: %v", err) - } - syscall.Close(savedFd) -} - func TestLogger(t *testing.T) { - assert := assert.New(t) + //assert := assert.New(t) - tmpfile, err := ioutil.TempFile("", "mlog-test") + tmpfile, err := os.CreateTemp("", "mlog-test") if err != nil { - panic(err) + t.Fatalf("Failed to create temporary file: %v", err) + return } - defer func() { - tmpfile.Close() - os.Remove(tmpfile.Name()) - }() + defer tmpfile.Close() + tempFilename := tmpfile.Name() + t.Cleanup(func() { os.Remove(tempFilename) }) - savedFd := redirectStderr(tmpfile) - - // save log state - oldDebug := mlog.DebugEnabled - oldInfo := mlog.InfoEnabled - oldError := mlog.ErrorEnabled + log := mlog.NewLoggerSingleOutput(tmpfile) + log.EnableDebug() + log.EnableError() + log.EnableInfo() // the following is put in an inlined lambda, so that // we have a place to put the defer: we need it to always get // called immediately after the log stmts run, even if // they crash -- otherwise, we'd have lost our stderr! - func() { - defer unredirectStderr(savedFd) - - mlog.DebugEnabled = true - mlog.InfoEnabled = true - mlog.ErrorEnabled = true - mlog.Debugf("debug %d", 1) - mlog.Printf("print %s", "2") - e := fmt.Errorf("E") - mlog.Error(e) - x := "yow" - mlog.Printv(x) + log.Debugf("debug %d", 1) + log.Printf("print %s", "2") + e := fmt.Errorf("E") + log.Error(e) + x := "yow" + log.Printv(x) - mlog.DebugEnabled = false - mlog.InfoEnabled = false - mlog.ErrorEnabled = false + log.DisableDebug() + log.DisableError() + log.DisableInfo() - mlog.Debugf("nope") - mlog.Printf("nope") - mlog.Error(e) - }() - - // restore log state - mlog.DebugEnabled = oldDebug - mlog.InfoEnabled = oldInfo - mlog.ErrorEnabled = oldError + log.Debugf("nope") + log.Printf("nope") + log.Error(e) tmpfile.Seek(0, io.SeekStart) - buf := make([]byte, 1024) - n, err := tmpfile.Read(buf) - assert.NoError(err) - buf = buf[0:n] - ex := []string{ - "[DEBUG] Log_test.go:74: debug 1", - "[LOG] Log_test.go:75: print 2", - "[ERROR] E", - "[LOG] Log_test.go:79: \"yow\"", + expectedLines := []string{ + `\[DEBUG\] Log_test.go:\d+: debug 1`, + `\[LOG\] Log_test.go:\d+: print 2`, + `\[ERROR\] E`, + `\[LOG\] Log_test.go:\d+: "yow"`, + } + + scanner := bufio.NewScanner(tmpfile) + count := 0 + for scanner.Scan() { + scanner.Text() + if count > len(expectedLines) { + t.Errorf("Found too many lines, expected %d, got %d", len(expectedLines), count) + return + } + txt := scanner.Text() + m, err := regexp.MatchString(expectedLines[count], txt) + if err != nil { + t.Errorf("error failed to match regexp[%s]: error: %v", expectedLines[count], err) + return + } + if !m { + t.Errorf("failed to match regexp[%s]: %s", expectedLines[count], txt) + return + } + count++ } - expected := strings.Join(ex, "\n") + "\n" - assert.Equal(expected, string(buf)) } diff --git a/support/ProjString.go b/support/ProjString.go index 36e2292..267dffd 100644 --- a/support/ProjString.go +++ b/support/ProjString.go @@ -9,9 +9,9 @@ package support import ( "encoding/json" + "regexp" "strconv" "strings" - "regexp" "github.com/go-spatial/proj/merror" ) @@ -215,7 +215,7 @@ func (pl *ProjString) GetAsFloat(key string) (float64, bool) { } // GetAsFloats returns the value of the first occurrence of the key, -// interpretted as comma-separated floats +// interpreted as comma-separated floats func (pl *ProjString) GetAsFloats(key string) ([]float64, bool) { value, ok := pl.get(key) @@ -225,7 +225,7 @@ func (pl *ProjString) GetAsFloats(key string) ([]float64, bool) { nums := strings.Split(value, ",") - floats := []float64{} + floats := make([]float64, 0, len(nums)) for _, num := range nums { f, err := strconv.ParseFloat(num, 64)