├── .github └── workflows │ ├── go.yml │ └── golangci-lint.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── epsg.go ├── go.mod ├── go.sum ├── ntv2 ├── BeTA2007.gsb ├── NTV2_0.gsb └── OSTN15_NTv2_OSGBtoETRS.gsb ├── wgs84.go └── wgs84_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: [ "v2" ] 9 | pull_request: 10 | branches: [ "v2" ] 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: stable 23 | 24 | - name: Build 25 | run: go build -v ./... 26 | 27 | - name: Test 28 | run: go test -v -race ./... -cover 29 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - master 8 | - main 9 | - v2 10 | pull_request: 11 | permissions: 12 | contents: read 13 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 14 | # pull-requests: read 15 | jobs: 16 | golangci: 17 | name: lint 18 | runs-on: ubuntu-latest 19 | environment: CI 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: actions/setup-go@v5 23 | with: 24 | go-version: stable 25 | - name: golangci-lint 26 | uses: golangci/golangci-lint-action@v6 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | coverage.txt 4 | cmd -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 1.21.x 4 | 5 | before_install: 6 | - go mod tidy 7 | 8 | script: 9 | - go test -race -coverprofile=coverage.txt -covermode=atomic 10 | 11 | after_success: 12 | - bash <(curl -s https://codecov.io/bash) 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Malte Wrogemann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/wroge/wgs84@v2.0.0-alpha.13) 2 | 3 | ## WGS84 - Coordinate Transformations 4 | 5 | ``` 6 | go get github.com/wroge/wgs84/v2@v2.0.0-alpha.13 7 | ``` 8 | 9 | 10 | I am currently in the process of rewriting the package. Some things will change and some new features will be added. One of these features is the support of NTv2 grid transformations and other projections, such as Krovak. If you would like to help or have any comments, please report them in the issues. 11 | 12 | ### Web Mercator 13 | 14 | ```go 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/wroge/wgs84/v2" 21 | ) 22 | 23 | func main() { 24 | transform := wgs84.Transform(wgs84.EPSG(4326), wgs84.EPSG(3857)).Round(3) 25 | 26 | east, north, _ := transform(10, 50, 0) 27 | 28 | fmt.Println(east, north) 29 | // 1.113194908e+06 6.446275841e+06 30 | 31 | // echo 10 50 | cs2cs +init=epsg:4326 +to +init=epsg:3857 -d 3 32 | // 1113194.908 6446275.841 33 | } 34 | ``` 35 | 36 | ### OSGB 37 | 38 | ```go 39 | package main 40 | 41 | import ( 42 | "fmt" 43 | 44 | "github.com/wroge/wgs84/v2" 45 | ) 46 | 47 | func main() { 48 | transform := wgs84.Transform(wgs84.EPSG(4326), wgs84.EPSG(27700)).Round(3) 49 | 50 | east, north, h := transform(-2.25, 52.25, 0) 51 | 52 | fmt.Println(east, north, h) 53 | // 383029.296 261341.615 0 54 | 55 | // echo -2.25 52.25 | cs2cs +init=epsg:4326 +to +init=epsg:27700 -d 3 56 | // 383029.296 261341.615 0.000 57 | } 58 | ``` 59 | -------------------------------------------------------------------------------- /epsg.go: -------------------------------------------------------------------------------- 1 | //nolint:gomnd,goerr113,forcetypeassert,gochecknoglobals,lll,funlen,gocognit,gocyclo,cyclop,ireturn,maintidx 2 | package wgs84 3 | 4 | import ( 5 | "fmt" 6 | "sync" 7 | ) 8 | 9 | var crsStore sync.Map 10 | 11 | func EPSG(code int) CRS { 12 | if crs, ok := crsStore.Load(code); ok { 13 | return crs.(CRS) 14 | } 15 | 16 | var crs CRS 17 | 18 | switch code { 19 | case 2056: 20 | crs = Swiss(true) 21 | case 21781: 22 | crs = Swiss(false) 23 | case 2154: 24 | crs = LambertConformalConic2SP(EPSG(4171), 3, 46.5, 49, 44, 700000, 6600000) 25 | case 2157: 26 | crs = TransverseMercator(EPSG(4173), -8, 53.5, 0.99982, 600000, 750000) 27 | case 2158: 28 | crs = TransverseMercator(EPSG(4173), -9, 0, 0.9996, 500000, 0) 29 | case 3035: 30 | crs = LambertAzimuthalEqualArea(EPSG(4258), 10, 52, 4321000, 3210000) 31 | case 3126: 32 | crs = TransverseMercator(EPSG(4258), 19, 0, 1, 500000, 0) 33 | case 3127: 34 | crs = TransverseMercator(EPSG(4258), 20, 0, 1, 500000, 0) 35 | case 3128: 36 | crs = TransverseMercator(EPSG(4258), 21, 0, 1, 500000, 0) 37 | case 3129: 38 | crs = TransverseMercator(EPSG(4258), 22, 0, 1, 500000, 0) 39 | case 3130: 40 | crs = TransverseMercator(EPSG(4258), 23, 0, 1, 500000, 0) 41 | case 3131: 42 | crs = TransverseMercator(EPSG(4258), 24, 0, 1, 500000, 0) 43 | case 3132: 44 | crs = TransverseMercator(EPSG(4258), 25, 0, 1, 500000, 0) 45 | case 3133: 46 | crs = TransverseMercator(EPSG(4258), 26, 0, 1, 500000, 0) 47 | case 3134: 48 | crs = TransverseMercator(EPSG(4258), 27, 0, 1, 500000, 0) 49 | case 3135: 50 | crs = TransverseMercator(EPSG(4258), 28, 0, 1, 500000, 0) 51 | case 3136: 52 | crs = TransverseMercator(EPSG(4258), 29, 0, 1, 500000, 0) 53 | case 3137: 54 | crs = TransverseMercator(EPSG(4258), 30, 0, 1, 500000, 0) 55 | case 3138: 56 | crs = TransverseMercator(EPSG(4258), 31, 0, 1, 500000, 0) 57 | case 3161: 58 | crs = LambertConformalConic2SP(EPSG(4269), -85, 0, 44.5, 53.5, 930000, 6430000) 59 | case 3416: 60 | crs = LambertConformalConic2SP(EPSG(4258), 13.33333333333333, 47.5, 49, 46, 400000, 400000) 61 | case 3857: 62 | crs = WebMercator(EPSG(4326)) 63 | case 4156: 64 | crs = Geographic(Helmert(589, 76, 480, 0, 0, 0, 0), NewSpheroid(6377397.155, 299.1528128)) 65 | case 4171: 66 | crs = Geographic(EPSG(4978), NewSpheroid(6378137, 298.257222101)) 67 | case 4173: 68 | crs = Geographic(EPSG(4978), NewSpheroid(6378137, 298.257222101)) 69 | case 4188: 70 | crs = Geographic(Helmert(482.5, -130.6, 564.6, -1.042, -0.214, -0.631, 8.15), NewSpheroid(6377563.396, 299.3249646)) 71 | case 4230: 72 | crs = Geographic(Helmert(-87, -98, -121, 0, 0, 0, 0), NewSpheroid(6378388, 297)) 73 | case 4258: 74 | crs = Geographic(EPSG(4978), NewSpheroid(6378137, 298.257222101)) 75 | // case 4267: 76 | // crs = loadNTv2("NTv2_0.gsb", NewSpheroid(6378206.4, 294.978698213898), EPSG(4326)) 77 | case 4269: 78 | crs = Geographic(EPSG(4978), NewSpheroid(6378137, 298.257222101)) 79 | case 4277: 80 | crs = loadNTv2("OSTN15_NTv2_OSGBtoETRS.gsb", NewSpheroid(6377563.396, 299.3249646), EPSG(4326)) 81 | case 4299: 82 | crs = Geographic(Helmert(482.5, -130.6, 564.6, -1.042, -0.214, -0.631, 8.15), NewSpheroid(6377340.189, 299.3249646)) 83 | case 4300: 84 | crs = EPSG(4299) 85 | case 4312: 86 | crs = Geographic(Helmert(577.326, 90.129, 463.919, 5.137, 1.474, 5.297, 2.4232), NewSpheroid(6377397.155, 299.1528128)) 87 | case 4313: 88 | crs = Geographic(Helmert(-106.8686, 52.2978, -103.7239, 0.3366, -0.457, 1.8422, -1.2747), NewSpheroid(6378388, 297)) 89 | case 4314: 90 | crs = loadNTv2("BeTA2007.gsb", NewSpheroid(6377397.155, 299.1528128), EPSG(4326)) 91 | case 4326: 92 | crs = Geographic(EPSG(4978), NewSpheroid(6378137, 298.257223563)) 93 | case 4490: 94 | crs = Geographic(EPSG(4978), NewSpheroid(6378137, 298.257222101)) 95 | case 4549: 96 | crs = TransverseMercator(EPSG(4490), 120, 0, 1, 500000, 0) 97 | case 4978: 98 | crs = base{} 99 | case 5514: 100 | crs = Krovak(EPSG(4156), 24.8333333333333, 49.5, 30.2881397527778, 78.5, 0.9999, 0, 0) 101 | case 6318: 102 | crs = Geographic(EPSG(4978), NewSpheroid(6378137, 298.257222101)) 103 | case 6355: 104 | crs = TransverseMercator(EPSG(6318), -85.8333333333333, 30.5, 0.99996, 200000, 0) 105 | case 6356: 106 | crs = TransverseMercator(EPSG(6318), -87.5, 30, 0.999933333, 600000, 0) 107 | case 6414: 108 | crs = AlbersConicEqualArea(EPSG(6318), -120, 0, 34, 40.5, 0, -4000000) 109 | case 23090: 110 | crs = TransverseMercator(EPSG(4230), 0, 0, 0.9996, 500000, 0) 111 | case 26917: 112 | crs = TransverseMercator(EPSG(4269), -81, 0, 0.9996, 500000, 0) 113 | case 27700: 114 | crs = TransverseMercator(EPSG(4277), -2, 49, 0.9996012717, 400000, -100000) 115 | case 29901: 116 | crs = TransverseMercator(EPSG(4188), -8, 53.5, 1, 200000, 250000) 117 | case 29902: 118 | crs = TransverseMercator(EPSG(4299), -8, 53.5, 1.000035, 200000, 250000) 119 | case 29903: 120 | crs = TransverseMercator(EPSG(4300), -8, 53.5, 1.000035, 200000, 250000) 121 | case 31257: 122 | crs = TransverseMercator(EPSG(4312), 10.33333333333333, 0, 1, 150000, -5000000) 123 | case 31258: 124 | crs = TransverseMercator(EPSG(4312), 13.33333333333333, 0, 1, 450000, -5000000) 125 | case 31259: 126 | crs = TransverseMercator(EPSG(4312), 16.33333333333333, 0, 1, 750000, -5000000) 127 | case 31284: 128 | crs = TransverseMercator(EPSG(4312), 10.33333333333333, 0, 1, 150000, 0) 129 | case 31285: 130 | crs = TransverseMercator(EPSG(4312), 13.33333333333333, 0, 1, 450000, 0) 131 | case 31286: 132 | crs = TransverseMercator(EPSG(4312), 16.33333333333333, 0, 1, 750000, 0) 133 | case 31287: 134 | crs = LambertConformalConic2SP(EPSG(4312), 13.33333333333333, 47.5, 49, 46, 400000, 400000) 135 | case 31370: 136 | crs = LambertConformalConic2SP(EPSG(4313), 4.36748666666667, 90, 51.1666672333333, 49.8333339, 150000.013, 5400088.438) 137 | // case 32024: 138 | // crs = LambertConformalConic2SP(EPSG(4267), -98, 35, 35.5666666666667, 36.7666666666667, 2000000, 0) 139 | case 102109: 140 | crs = TransverseMercator(EPSG(4258), 15, 0, 0.9999, 500000, -5000000) 141 | case 102157: 142 | crs = TransverseMercator(EPSG(4258), 21, 0, 0.9999, 7500000, 0) 143 | case 102173: 144 | crs = TransverseMercator(EPSG(4258), 19, 0, 0.9993, 500000, -5300000) 145 | case 900913: 146 | crs = EPSG(3857) 147 | default: 148 | switch { 149 | case code > 3941 && code < 3951: 150 | lat := float64(code - 3900) 151 | 152 | crs = LambertConformalConic2SP(EPSG(4171), 3, lat, lat-0.75, lat+0.75, 1700000, 2200000+(lat-43)*1000000) 153 | case code > 25827 && code < 25839: 154 | zone := float64(code - 25800) 155 | 156 | crs = TransverseMercator(EPSG(4258), zone*6-183, 0, 0.9996, 500000, 0) 157 | case code > 31465 && code < 31470: 158 | zone := float64(code - 31464) 159 | 160 | crs = TransverseMercator(EPSG(4314), zone*3, 0, 1, zone*1000000+500000, 0) 161 | case code > 32600 && code < 32661: 162 | zone := float64(code - 32600) 163 | 164 | crs = TransverseMercator(EPSG(4326), zone*6-183, 0, 0.9996, 500000, 0) 165 | case code > 32700 && code < 32761: 166 | zone := code - 32700 167 | 168 | crs = TransverseMercator(EPSG(4326), float64(zone)*6-183, 0, 0.9996, 500000, 10000000) 169 | } 170 | } 171 | 172 | if crs == nil { 173 | return errorCRS{err: fmt.Errorf("epsg code '%d' not found", code)} 174 | } 175 | 176 | crsStore.Store(code, crs) 177 | 178 | return crs 179 | } 180 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wroge/wgs84/v2 2 | 3 | go 1.21 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wroge/wgs84/4b38145681a4481efa92390b26db113d0bd1983e/go.sum -------------------------------------------------------------------------------- /ntv2/BeTA2007.gsb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wroge/wgs84/4b38145681a4481efa92390b26db113d0bd1983e/ntv2/BeTA2007.gsb -------------------------------------------------------------------------------- /ntv2/NTV2_0.gsb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wroge/wgs84/4b38145681a4481efa92390b26db113d0bd1983e/ntv2/NTV2_0.gsb -------------------------------------------------------------------------------- /ntv2/OSTN15_NTv2_OSGBtoETRS.gsb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wroge/wgs84/4b38145681a4481efa92390b26db113d0bd1983e/ntv2/OSTN15_NTv2_OSGBtoETRS.gsb -------------------------------------------------------------------------------- /wgs84.go: -------------------------------------------------------------------------------- 1 | //nolint:varnamelen,nonamedreturns,ireturn,gomnd,exhaustivestruct,exhaustruct,cyclop,errname,lll,funlen 2 | package wgs84 3 | 4 | import ( 5 | "embed" 6 | "encoding/binary" 7 | "fmt" 8 | "io" 9 | "math" 10 | "strings" 11 | ) 12 | 13 | type CRS interface { 14 | Base() CRS 15 | Spheroid() Spheroid 16 | ToBase(float64, float64, float64) (float64, float64, float64) 17 | FromBase(float64, float64, float64) (float64, float64, float64) 18 | } 19 | 20 | func Transform(from, to CRS) Func { 21 | var ( 22 | toBase []Func 23 | fromBase []Func 24 | ) 25 | 26 | for { 27 | if from == nil { 28 | break 29 | } 30 | 31 | toBase = append(toBase, from.ToBase) 32 | 33 | from = from.Base() 34 | } 35 | 36 | for { 37 | if to == nil { 38 | break 39 | } 40 | 41 | fromBase = append(fromBase, to.FromBase) 42 | 43 | to = to.Base() 44 | } 45 | 46 | return chainFunc(chainFunc(toBase...), reverseChainFunc(fromBase...)) 47 | } 48 | 49 | func chainFunc(f ...Func) Func { 50 | return func(a, b, c float64) (float64, float64, float64) { 51 | for _, each := range f { 52 | if each != nil { 53 | a, b, c = each(a, b, c) 54 | } 55 | } 56 | 57 | return a, b, c 58 | } 59 | } 60 | 61 | func reverseChainFunc(f ...Func) Func { 62 | return func(a, b, c float64) (float64, float64, float64) { 63 | for i := len(f) - 1; i >= 0; i-- { 64 | if f[i] != nil { 65 | a, b, c = f[i](a, b, c) 66 | } 67 | } 68 | 69 | return a, b, c 70 | } 71 | } 72 | 73 | type Func func(float64, float64, float64) (float64, float64, float64) 74 | 75 | func (f Func) Round(dec int) Func { 76 | return func(a, b, c float64) (float64, float64, float64) { 77 | a, b, c = f(a, b, c) 78 | 79 | return round(a, dec), round(b, dec), round(c, dec) 80 | } 81 | } 82 | 83 | func round(val float64, dec int) float64 { 84 | factor := math.Pow(10, float64(dec)) 85 | 86 | val = math.Round(val*factor) / factor 87 | 88 | if val == -0 { 89 | return 0 90 | } 91 | 92 | return val 93 | } 94 | 95 | type errorCRS struct { 96 | err error 97 | } 98 | 99 | func (e errorCRS) Unwrap() error { 100 | return e.err 101 | } 102 | 103 | func (e errorCRS) Error() string { 104 | return e.err.Error() 105 | } 106 | 107 | func (errorCRS) Base() CRS { 108 | return nil 109 | } 110 | 111 | func (errorCRS) Spheroid() Spheroid { 112 | return Spheroid{} 113 | } 114 | 115 | func (errorCRS) ToBase(_, _, _ float64) (float64, float64, float64) { 116 | return math.NaN(), math.NaN(), math.NaN() 117 | } 118 | 119 | func (errorCRS) FromBase(_, _, _ float64) (float64, float64, float64) { 120 | return math.NaN(), math.NaN(), math.NaN() 121 | } 122 | 123 | type Spheroid struct { 124 | A, Fi float64 125 | A2, F, F2, B, E2, E, E4, E6, Ei, Ei2, Ei3, Ei4 float64 126 | } 127 | 128 | func NewSpheroid(a, fi float64) Spheroid { 129 | s := Spheroid{ 130 | A: a, 131 | Fi: fi, 132 | } 133 | 134 | s.A2 = s.A * s.A 135 | s.F = 1 / s.Fi 136 | s.F2 = s.F * s.F 137 | s.B = s.A * (1 - s.F) 138 | s.E2 = 2/s.Fi - s.F2 139 | s.E = math.Sqrt(s.E2) 140 | s.E4 = s.E2 * s.E2 141 | s.E6 = s.E4 * s.E2 142 | s.Ei = (1 - math.Sqrt(1-s.E2)) / (1 + math.Sqrt(1-s.E2)) 143 | s.Ei2 = s.Ei * s.Ei 144 | s.Ei3 = s.Ei2 * s.Ei 145 | s.Ei4 = s.Ei3 * s.Ei 146 | 147 | return s 148 | } 149 | 150 | func (s Spheroid) ToXYZ(lon, lat, h float64) (x, y, z float64) { 151 | n := s.A / math.Sqrt(1-s.E2*math.Pow(math.Sin(radian(lat)), 2)) 152 | 153 | x = (n + h) * math.Cos(radian(lon)) * math.Cos(radian(lat)) 154 | y = (n + h) * math.Cos(radian(lat)) * math.Sin(radian(lon)) 155 | z = (n*math.Pow(s.A*(1-s.F), 2)/(s.A2) + h) * math.Sin(radian(lat)) 156 | 157 | return x, y, z 158 | } 159 | 160 | func (s Spheroid) FromXYZ(x, y, z float64) (lon, lat, h float64) { 161 | sd := math.Sqrt(x*x + y*y) 162 | T := math.Atan(z * s.A / (sd * s.B)) 163 | B := math.Atan((z + s.E2*(s.A2)/s.B* 164 | math.Pow(math.Sin(T), 3)) / (sd - s.E2*s.A*math.Pow(math.Cos(T), 3))) 165 | n := s.A / math.Sqrt(1-s.E2*math.Pow(math.Sin(B), 2)) 166 | h = sd/math.Cos(B) - n 167 | lon = degree(math.Atan2(y, x)) 168 | lat = degree(B) 169 | 170 | return lon, lat, h 171 | } 172 | 173 | type base struct{} 174 | 175 | func (base) Base() CRS { 176 | return nil 177 | } 178 | 179 | func (base) Spheroid() Spheroid { 180 | return Spheroid{} 181 | } 182 | 183 | func (base) ToBase(x0, y0, z0 float64) (float64, float64, float64) { 184 | return x0, y0, z0 185 | } 186 | 187 | func (base) FromBase(x0, y0, z0 float64) (float64, float64, float64) { 188 | return x0, y0, z0 189 | } 190 | 191 | func Geographic(geocentric CRS, spheroid Spheroid) CRS { 192 | if geocentric == nil { 193 | geocentric = base{} 194 | } 195 | 196 | return geographic{ 197 | b: geocentric, 198 | s: spheroid, 199 | } 200 | } 201 | 202 | type geographic struct { 203 | b CRS 204 | s Spheroid 205 | } 206 | 207 | func (b geographic) Base() CRS { 208 | return b.b 209 | } 210 | 211 | func (b geographic) Spheroid() Spheroid { 212 | return b.s 213 | } 214 | 215 | func (b geographic) ToBase(lon, lat, h float64) (x, y, z float64) { 216 | return b.s.ToXYZ(lon, lat, h) 217 | } 218 | 219 | func (b geographic) FromBase(x, y, z float64) (lon, lat, h float64) { 220 | return b.s.FromXYZ(x, y, z) 221 | } 222 | 223 | func Helmert(tx, ty, tz, rx, ry, rz, ds float64) CRS { 224 | return helmert{ 225 | tx: tx, 226 | ty: ty, 227 | tz: tz, 228 | rx: rx, 229 | ry: ry, 230 | rz: rz, 231 | ds: ds, 232 | } 233 | } 234 | 235 | type helmert struct { 236 | tx, ty, tz, rx, ry, rz, ds float64 237 | } 238 | 239 | func (t helmert) Base() CRS { 240 | return base{} 241 | } 242 | 243 | func (t helmert) Spheroid() Spheroid { 244 | return Spheroid{} 245 | } 246 | 247 | func (t helmert) ToBase(x, y, z float64) (x0, y0, z0 float64) { 248 | return calcHelmert(x, y, z, t.tx, t.ty, t.tz, t.rx, t.ry, t.rz, t.ds) 249 | } 250 | 251 | func (t helmert) FromBase(x0, y0, z0 float64) (x, y, z float64) { 252 | return calcHelmert(x0, y0, z0, -t.tx, -t.ty, -t.tz, -t.rx, -t.ry, -t.rz, -t.ds) 253 | } 254 | 255 | const ( 256 | asec = math.Pi / 648000 257 | ppm = 0.000001 258 | ) 259 | 260 | func calcHelmert(x, y, z, tx, ty, tz, rx, ry, rz, ds float64) (x0, y0, z0 float64) { 261 | x0 = (1+ds*ppm)*(x+z*ry*asec-y*rz*asec) + tx 262 | y0 = (1+ds*ppm)*(y+x*rz*asec-z*rx*asec) + ty 263 | z0 = (1+ds*ppm)*(z+y*rx*asec-x*ry*asec) + tz 264 | 265 | return 266 | } 267 | 268 | //go:embed ntv2 269 | var res embed.FS 270 | 271 | func loadNTv2(name string, spheroid Spheroid, base CRS) CRS { 272 | file, err := res.Open("ntv2/" + name) 273 | if err != nil { 274 | return errorCRS{err: err} 275 | } 276 | 277 | crs := loadReaderNTv2(file, spheroid, base) 278 | 279 | return crs 280 | } 281 | 282 | func loadReaderNTv2(reader io.Reader, spheroid Spheroid, base CRS) CRS { 283 | if base == nil { 284 | base = Geographic(nil, NewSpheroid(6378137, 298.257223563)) 285 | } 286 | 287 | data := ntv2{ 288 | base: base, 289 | spheroid: spheroid, 290 | } 291 | 292 | for i := 1; ; i++ { 293 | set := make([]byte, 16) 294 | 295 | _, err := reader.Read(set) 296 | if err != nil { 297 | return errorCRS{err: err} 298 | } 299 | 300 | switch toString(set[:8]) { 301 | case "NUM_OREC": 302 | data.numOrec = int32(binary.LittleEndian.Uint32(set[8:])) 303 | case "NUM_SREC": 304 | data.numSrec = int32(binary.LittleEndian.Uint32(set[8:])) 305 | case "NUM_FILE": 306 | data.numFile = int32(binary.LittleEndian.Uint32(set[8:])) 307 | case "S_LAT": 308 | data.sLat = toFloat(set[8:]) 309 | case "N_LAT": 310 | data.nLat = toFloat(set[8:]) 311 | case "E_LONG": 312 | data.eLong = toFloat(set[8:]) 313 | case "W_LONG": 314 | data.wLong = toFloat(set[8:]) 315 | case "LAT_INC": 316 | data.latInc = toFloat(set[8:]) 317 | case "LONG_INC": 318 | data.longInc = toFloat(set[8:]) 319 | case "GS_COUNT": 320 | data.gsCount = int32(binary.LittleEndian.Uint32(set[8:])) 321 | default: 322 | offset := int(data.numOrec) + int(data.numSrec) + int(data.numFile) 323 | if i >= int(data.gsCount)+offset { 324 | return data 325 | } 326 | 327 | if i >= offset && i < int(data.gsCount)+offset { 328 | data.values = append(data.values, [4]float32{ 329 | toFloat32(set[0:4]), toFloat32(set[4:8]), toFloat32(set[8:12]), toFloat32(set[12:16]), 330 | }) 331 | 332 | continue 333 | } 334 | } 335 | } 336 | } 337 | 338 | func toFloat32(b []byte) float32 { 339 | i := binary.LittleEndian.Uint32(b) 340 | 341 | return math.Float32frombits(i) 342 | } 343 | 344 | func toFloat(b []byte) float64 { 345 | i := binary.LittleEndian.Uint64(b) 346 | 347 | return math.Float64frombits(i) 348 | } 349 | 350 | func toString(b []byte) string { 351 | return strings.TrimSpace(string(b)) 352 | } 353 | 354 | type ntv2 struct { 355 | spheroid Spheroid 356 | base CRS 357 | numOrec int32 358 | numSrec int32 359 | numFile int32 360 | gsCount int32 361 | sLat float64 362 | nLat float64 363 | eLong float64 364 | wLong float64 365 | latInc float64 366 | longInc float64 367 | values [][4]float32 368 | } 369 | 370 | func (n ntv2) String() string { 371 | return fmt.Sprintf("BASE: %v, NUM_OREC: %d, NUM_SREC: %d, NUM_FILE: %d, S_LAT: %f, N_LAT: %f, E_LONG: %f, W_LONG: %f, LAT_INC: %f, LONG_INC: %f, GS_COUNT: %d", n.base, n.numOrec, n.numSrec, n.numFile, n.sLat, n.nLat, n.eLong, n.wLong, n.latInc, n.longInc, n.gsCount) 372 | } 373 | 374 | func (n ntv2) Base() CRS { 375 | return n.base 376 | } 377 | 378 | func (n ntv2) Spheroid() Spheroid { 379 | return n.spheroid 380 | } 381 | 382 | func (n ntv2) ToBase(lon, lat, h float64) (lon2, lat2, h2 float64) { 383 | slon, slat := n.Shift(-lon, lat) 384 | 385 | return lon + slon, lat + slat, h 386 | } 387 | 388 | func (n ntv2) FromBase(lon, lat, h float64) (lon2, lat2, h2 float64) { 389 | qlat := lat 390 | qlon := lon 391 | 392 | for i := 0; i < 4; i++ { 393 | slon, slat := n.Shift(qlon, qlat) 394 | 395 | qlon = lon - slon 396 | qlat = lat - slat 397 | } 398 | 399 | return qlon, qlat, h 400 | } 401 | 402 | func (n ntv2) Shift(lon, lat float64) (float64, float64) { 403 | fcol := (-lon*3600 - n.eLong) / n.longInc 404 | frow := (lat*3600 - n.sLat) / n.latInc 405 | 406 | col := math.Floor(fcol) 407 | row := math.Floor(frow) 408 | 409 | ppr := math.Floor((n.wLong-n.eLong)/n.longInc+0.5) + 1 410 | ppc := math.Floor((n.nLat-n.sLat)/n.latInc+0.5) + 1 411 | 412 | se := row*ppr + col 413 | sw := se + 1 414 | ne := se + ppr 415 | nw := ne + 1 416 | 417 | if col >= ppr-1 { 418 | sw = se 419 | nw = ne 420 | } 421 | 422 | if row >= ppc-1 { 423 | ne = se 424 | nw = sw 425 | } 426 | 427 | if col <= 0 { 428 | se = sw 429 | ne = nw 430 | } 431 | 432 | if row <= 0 { 433 | se = ne 434 | sw = nw 435 | } 436 | 437 | seIndex := min(max(int(se), 0), len(n.values)-1) 438 | swIndex := min(max(int(sw), 0), len(n.values)-1) 439 | neIndex := min(max(int(ne), 0), len(n.values)-1) 440 | nwIndex := min(max(int(nw), 0), len(n.values)-1) 441 | 442 | sse := n.values[seIndex] 443 | ssw := n.values[swIndex] 444 | sne := n.values[neIndex] 445 | snw := n.values[nwIndex] 446 | 447 | dx := fcol - col 448 | dy := frow - row 449 | 450 | latsv := (1-dx)*(1-dy)*float64(sse[0]) + dx*(1-dy)*float64(ssw[0]) + (1-dx)*dy*float64(sne[0]) + dx*dy*float64(snw[0]) 451 | lonsv := (1-dx)*(1-dy)*float64(sse[1]) + dx*(1-dy)*float64(ssw[1]) + (1-dx)*dy*float64(sne[1]) + dx*dy*float64(snw[1]) 452 | 453 | return -lonsv / 3600, latsv / 3600 454 | } 455 | 456 | func WebMercator(base CRS) CRS { 457 | if base == nil { 458 | base = Geographic(nil, NewSpheroid(6378137, 298.257223563)) 459 | } 460 | 461 | return webMercator{ 462 | base: base, 463 | } 464 | } 465 | 466 | type webMercator struct { 467 | base CRS 468 | } 469 | 470 | func (p webMercator) Base() CRS { 471 | return p.base 472 | } 473 | 474 | func (p webMercator) Spheroid() Spheroid { 475 | return p.base.Spheroid() 476 | } 477 | 478 | func (p webMercator) ToBase(east, north, h float64) (lon, lat, h2 float64) { 479 | s := p.base.Spheroid() 480 | 481 | D := (-north) / s.A 482 | phi := math.Pi/2 - 2*math.Atan(math.Pow(math.E, D)) 483 | lambda := east / s.A 484 | 485 | return radian(lambda), radian(phi), h 486 | } 487 | 488 | func (p webMercator) FromBase(lon, lat, h float64) (east, north, h2 float64) { 489 | s := p.base.Spheroid() 490 | 491 | lambda := radian(lon) 492 | phi := radian(lat) 493 | 494 | east = s.A * lambda 495 | north = s.A * math.Log(math.Tan(math.Pi/4+phi/2)) 496 | 497 | return east, north, h 498 | } 499 | 500 | func TransverseMercator(base CRS, lonf, latf, scale, eastf, northf float64) CRS { 501 | if base == nil { 502 | base = Geographic(nil, NewSpheroid(6378137, 298.257223563)) 503 | } 504 | 505 | s := base.Spheroid() 506 | 507 | phi0 := radian(latf) 508 | lambda0 := radian(lonf) 509 | n := s.F / (2 - s.F) 510 | n2 := math.Pow(n, 2) 511 | n3 := math.Pow(n, 3) 512 | n4 := math.Pow(n, 4) 513 | B := (s.A / (1 + n)) * (1 + n2/4 + n4/64) 514 | h1 := n/2.0 - (2/3.0)*n2 + (5/16.0)*n3 + (41/180.0)*n4 515 | h2 := (13/48.0)*n2 - (3/5.0)*n3 + (557/1440.0)*n4 516 | h3 := (61/240.0)*n3 - (103/140.0)*n4 517 | h4 := (49561 / 161280.0) * n4 518 | e := math.Sqrt(s.E2) 519 | 520 | var M0 float64 521 | 522 | switch phi0 { 523 | case 0: 524 | M0 = 0 525 | case math.Pi / 2: 526 | M0 = B * (math.Pi / 2) 527 | case -math.Pi / 2: 528 | M0 = B * (-math.Pi / 2) 529 | default: 530 | Q0 := math.Asinh(math.Tan(phi0)) - (e * math.Atanh(e*math.Sin(phi0))) 531 | xi00 := math.Atan(math.Sinh(Q0)) 532 | xi01 := h1 * math.Sin(2*xi00) 533 | xi02 := h2 * math.Sin(4*xi00) 534 | xi03 := h3 * math.Sin(6*xi00) 535 | xi04 := h4 * math.Sin(8*xi00) 536 | xi0 := xi00 + xi01 + xi02 + xi03 + xi04 537 | M0 = B * xi0 538 | } 539 | 540 | h1i := n/2.0 - (2/3.0)*n2 + (37/96.0)*n3 + (1/360.0)*n4 541 | h2i := (1/48.0)*n2 - (1/15.0)*n3 + (437/1440.0)*n4 542 | h3i := (17/480.0)*n3 - (37/840.0)*n4 543 | h4i := (4397 / 161280.0) * n4 544 | 545 | return transverseMercator{ 546 | base: base, 547 | lambdaO: lambda0, 548 | scale: scale, 549 | eastf: eastf, 550 | northf: northf, 551 | b: B, 552 | h1: h1, 553 | h2: h2, 554 | h3: h3, 555 | h4: h4, 556 | mO: M0, 557 | h1i: h1i, 558 | h2i: h2i, 559 | h3i: h3i, 560 | h4i: h4i, 561 | } 562 | } 563 | 564 | type transverseMercator struct { 565 | base CRS 566 | lambdaO float64 567 | b, h1, h2, h3, h4, mO float64 568 | h1i, h2i, h3i, h4i float64 569 | scale float64 570 | eastf float64 571 | northf float64 572 | } 573 | 574 | func (p transverseMercator) Base() CRS { 575 | return p.base 576 | } 577 | 578 | func (p transverseMercator) Spheroid() Spheroid { 579 | return p.base.Spheroid() 580 | } 581 | 582 | func (p transverseMercator) ToBase(east, north, h float64) (lon, lat, h2 float64) { 583 | s := p.base.Spheroid() 584 | 585 | etai := (east - p.eastf) / (p.b * p.scale) 586 | xii := ((north - p.northf) + p.scale*p.mO) / (p.b * p.scale) 587 | 588 | xi1i := p.h1i * math.Sin(2*xii) * math.Cosh(2*etai) 589 | xi2i := p.h2i * math.Sin(4*xii) * math.Cosh(4*etai) 590 | xi3i := p.h3i * math.Sin(6*xii) * math.Cosh(6*etai) 591 | xi4i := p.h4i * math.Sin(8*xii) * math.Cosh(8*etai) 592 | 593 | xi0i := xii - (xi1i + xi2i + xi3i + xi4i) 594 | 595 | eta1i := p.h1i * math.Cos(2*xii) * math.Sinh(2*etai) 596 | eta2i := p.h2i * math.Cos(4*xii) * math.Sinh(4*etai) 597 | eta3i := p.h3i * math.Cos(6*xii) * math.Sinh(6*etai) 598 | eta4i := p.h4i * math.Cos(8*xii) * math.Sinh(8*etai) 599 | 600 | eta0i := etai - (eta1i + eta2i + eta3i + eta4i) 601 | 602 | betai := math.Asin(math.Sin(xi0i) / math.Cosh(eta0i)) 603 | Qi := math.Asinh(math.Tan(betai)) 604 | Qii := Qi + (s.E * math.Atanh(s.E*math.Tanh(Qi))) 605 | 606 | for i := 0; i < 15; i++ { 607 | q := Qi + (s.E * math.Atanh(s.E*math.Tanh(Qii))) 608 | 609 | Qii = q 610 | } 611 | 612 | phi := math.Atan(math.Sinh(Qii)) 613 | lambda := p.lambdaO + math.Asin(math.Tanh(eta0i)/math.Cos(betai)) 614 | 615 | return degree(lambda), degree(phi), h 616 | } 617 | 618 | func (p transverseMercator) FromBase(lon, lat, h float64) (east, north, h2 float64) { 619 | s := p.base.Spheroid() 620 | 621 | phi := radian(lat) 622 | lambda := radian(lon) 623 | 624 | Q := math.Asinh(math.Tan(phi)) - s.E*math.Atanh(s.E*math.Sin(phi)) 625 | beta := math.Atan(math.Sinh(Q)) 626 | eta0 := math.Atanh(math.Cos(beta) * math.Sin(lambda-p.lambdaO)) 627 | xi0 := math.Asin(math.Sin(beta) * math.Cosh(eta0)) 628 | 629 | xi1 := p.h1 * math.Sin(2*xi0) * math.Cosh(2*eta0) 630 | xi2 := p.h2 * math.Sin(4*xi0) * math.Cosh(4*eta0) 631 | xi3 := p.h3 * math.Sin(6*xi0) * math.Cosh(6*eta0) 632 | xi4 := p.h4 * math.Sin(8*xi0) * math.Cosh(8*eta0) 633 | 634 | xi := xi0 + xi1 + xi2 + xi3 + xi4 635 | 636 | eta1 := p.h1 * math.Cos(2*xi0) * math.Sinh(2*eta0) 637 | eta2 := p.h2 * math.Cos(4*xi0) * math.Sinh(4*eta0) 638 | eta3 := p.h3 * math.Cos(6*xi0) * math.Sinh(6*eta0) 639 | eta4 := p.h4 * math.Cos(8*xi0) * math.Sinh(8*eta0) 640 | 641 | eta := eta0 + eta1 + eta2 + eta3 + eta4 642 | 643 | east = p.eastf + p.scale*p.b*eta 644 | north = p.northf + p.scale*(p.b*xi-p.mO) 645 | 646 | return east, north, h 647 | } 648 | 649 | func LambertConformalConic2SP(base CRS, lonf, latf, sp1, sp2, eastf, northf float64) CRS { 650 | if base == nil { 651 | base = Geographic(nil, NewSpheroid(6378137, 298.257223563)) 652 | } 653 | 654 | s := base.Spheroid() 655 | 656 | phif := radian(latf) 657 | phi1 := radian(sp1) 658 | phi2 := radian(sp2) 659 | lambdaf := radian(lonf) 660 | 661 | tf := math.Tan(math.Pi/4-phif/2) / math.Pow((1-s.E*math.Sin(phif))/(1+s.E*math.Sin(phif)), s.E/2) 662 | t1 := math.Tan(math.Pi/4-phi1/2) / math.Pow((1-s.E*math.Sin(phi1))/(1+s.E*math.Sin(phi1)), s.E/2) 663 | t2 := math.Tan(math.Pi/4-phi2/2) / math.Pow((1-s.E*math.Sin(phi2))/(1+s.E*math.Sin(phi2)), s.E/2) 664 | 665 | m1 := math.Cos(phi1) / math.Sqrt(1-s.E2*sin2(phi1)) 666 | m2 := math.Cos(phi2) / math.Sqrt(1-s.E2*sin2(phi2)) 667 | 668 | n := (math.Log(m1) - math.Log(m2)) / (math.Log(t1) - math.Log(t2)) 669 | f := m1 / (n * math.Pow(t1, n)) 670 | rf := s.A * f * math.Pow(tf, n) 671 | 672 | return lambertConformalConic2SP{ 673 | base: base, 674 | phif: phif, 675 | phi1: phi1, 676 | phi2: phi2, 677 | lambdaf: lambdaf, 678 | n: n, 679 | f: f, 680 | rf: rf, 681 | eastf: eastf, 682 | northf: northf, 683 | } 684 | } 685 | 686 | type lambertConformalConic2SP struct { 687 | base CRS 688 | phif, phi1, phi2, lambdaf, n, f, rf float64 689 | eastf float64 690 | northf float64 691 | } 692 | 693 | func (p lambertConformalConic2SP) Base() CRS { 694 | return p.base 695 | } 696 | 697 | func (p lambertConformalConic2SP) Spheroid() Spheroid { 698 | return p.base.Spheroid() 699 | } 700 | 701 | func (p lambertConformalConic2SP) ToBase(east, north, h float64) (lon, lat, h2 float64) { 702 | s := p.base.Spheroid() 703 | 704 | ri := math.Sqrt(math.Pow(east-p.eastf, 2) + math.Pow(p.rf-(north-p.northf), 2)) 705 | if p.n < 0 && ri > 0 { 706 | ri = -ri 707 | } 708 | 709 | ti := math.Pow(ri/(s.A*p.f), 1/p.n) 710 | 711 | var theta float64 712 | if p.n > 0 { 713 | theta = math.Atan2((east - p.eastf), (p.rf - (north - p.northf))) 714 | } else { 715 | theta = math.Atan2(-(east - p.eastf), -(p.rf - (north - p.northf))) 716 | } 717 | 718 | phi := math.Pi/2 - 2*math.Atan(ti) 719 | 720 | for i := 0; i < 5; i++ { 721 | phi = math.Pi/2 - 2*math.Atan(ti*math.Pow((1-s.E*math.Sin(phi))/(1+s.E*math.Sin(phi)), s.E/2)) 722 | } 723 | 724 | lambda := theta/p.n + p.lambdaf 725 | 726 | return degree(lambda), degree(phi), h 727 | } 728 | 729 | func (p lambertConformalConic2SP) FromBase(lon, lat, h float64) (east, north, h2 float64) { 730 | s := p.base.Spheroid() 731 | 732 | phi := radian(lat) 733 | lambda := radian(lon) 734 | 735 | t := math.Tan(math.Pi/4-phi/2) / math.Pow((1-s.E*math.Sin(phi))/(1+s.E*math.Sin(phi)), s.E/2) 736 | tf := math.Tan(math.Pi/4-p.phif/2) / math.Pow((1-s.E*math.Sin(p.phif))/(1+s.E*math.Sin(p.phif)), s.E/2) 737 | t1 := math.Tan(math.Pi/4-p.phi1/2) / math.Pow((1-s.E*math.Sin(p.phi1))/(1+s.E*math.Sin(p.phi1)), s.E/2) 738 | t2 := math.Tan(math.Pi/4-p.phi2/2) / math.Pow((1-s.E*math.Sin(p.phi2))/(1+s.E*math.Sin(p.phi2)), s.E/2) 739 | 740 | m1 := math.Cos(p.phi1) / math.Sqrt(1-s.E2*sin2(p.phi1)) 741 | m2 := math.Cos(p.phi2) / math.Sqrt(1-s.E2*sin2(p.phi2)) 742 | 743 | n := (math.Log(m1) - math.Log(m2)) / (math.Log(t1) - math.Log(t2)) 744 | F := m1 / (n * math.Pow(t1, n)) 745 | r := s.A * F * math.Pow(t, n) 746 | rf := s.A * F * math.Pow(tf, n) 747 | theta := n * (lambda - p.lambdaf) 748 | 749 | east = p.eastf + r*math.Sin(theta) 750 | north = p.northf + rf - r*math.Cos(theta) 751 | 752 | return east, north, h 753 | } 754 | 755 | func AlbersConicEqualArea(base CRS, lonf, latf, sp1, sp2, eastf, northf float64) CRS { 756 | if base == nil { 757 | base = Geographic(nil, NewSpheroid(6378137, 298.257223563)) 758 | } 759 | 760 | s := base.Spheroid() 761 | 762 | phif := radian(latf) 763 | phi1 := radian(sp1) 764 | phi2 := radian(sp2) 765 | lambdaf := radian(lonf) 766 | alphaf := (1 - s.E2) * ((math.Sin(phif) / (1 - s.E2*sin2(phif))) - (1/(2*s.E))*math.Log((1-s.E*math.Sin(phif))/(1+s.E*math.Sin(phif)))) 767 | alpha1 := (1 - s.E2) * ((math.Sin(phi1) / (1 - s.E2*sin2(phi1))) - (1/(2*s.E))*math.Log((1-s.E*math.Sin(phi1))/(1+s.E*math.Sin(phi1)))) 768 | alpha2 := (1 - s.E2) * ((math.Sin(phi2) / (1 - s.E2*sin2(phi2))) - (1/(2*s.E))*math.Log((1-s.E*math.Sin(phi2))/(1+s.E*math.Sin(phi2)))) 769 | 770 | m1 := math.Cos(phi1) / math.Sqrt(1-s.E2*sin2(phi1)) 771 | m2 := math.Cos(phi2) / math.Sqrt(1-s.E2*sin2(phi2)) 772 | 773 | n := (math.Pow(m1, 2) - math.Pow(m2, 2)) / (alpha2 - alpha1) 774 | c := math.Pow(m1, 2) + (n * alpha1) 775 | rf := (s.A * math.Sqrt(c-n*alphaf)) / n 776 | 777 | return albersConicEqualArea{ 778 | base: base, 779 | lambdaf: lambdaf, 780 | alphaf: alphaf, 781 | n: n, 782 | c: c, 783 | rf: rf, 784 | eastf: eastf, 785 | northf: northf, 786 | } 787 | } 788 | 789 | type albersConicEqualArea struct { 790 | base CRS 791 | lambdaf, alphaf, n, c, rf float64 792 | eastf float64 793 | northf float64 794 | } 795 | 796 | func (p albersConicEqualArea) Base() CRS { 797 | return p.base 798 | } 799 | 800 | func (p albersConicEqualArea) Spheroid() Spheroid { 801 | return p.base.Spheroid() 802 | } 803 | 804 | func (p albersConicEqualArea) ToBase(east, north, h float64) (lon, lat, h2 float64) { 805 | s := p.base.Spheroid() 806 | 807 | ri := math.Sqrt(math.Pow(east-p.eastf, 2) + math.Pow(p.rf-(north-p.northf), 2)) 808 | alphai := (p.c - (math.Pow(ri, 2) * math.Pow(p.n, 2) / s.A2)) / p.n 809 | betai := math.Asin(alphai / (1 - ((1 - s.E2) / (2 * s.E) * math.Log((1-s.E)/(1+s.E))))) 810 | 811 | var theta float64 812 | 813 | if p.n > 0 { 814 | theta = math.Atan2((east - p.eastf), (p.rf - (north - p.northf))) 815 | } else { 816 | theta = math.Atan2(-(east - p.eastf), -(p.rf - (north - p.northf))) 817 | } 818 | 819 | phi := betai + ((s.E2/3 + 31*s.E4/180 + 517*s.E6/5040) * math.Sin(2*betai)) + ((23*s.E4/360 + 251*s.E6/3780) * math.Sin(4*betai)) + ((761 * s.E6 / 45360) * math.Sin(6*betai)) 820 | lambda := p.lambdaf + (theta / p.n) 821 | 822 | return degree(lambda), degree(phi), h 823 | } 824 | 825 | func (p albersConicEqualArea) FromBase(lon, lat, h float64) (east, north, h2 float64) { 826 | s := p.base.Spheroid() 827 | 828 | lambda := radian(lon) 829 | phi := radian(lat) 830 | 831 | alpha := (1 - s.E2) * ((math.Sin(phi) / (1 - s.E2*sin2(phi))) - (1/(2*s.E))*math.Log((1-s.E*math.Sin(phi))/(1+s.E*math.Sin(phi)))) 832 | 833 | theta := p.n * (lambda - p.lambdaf) 834 | r := (s.A * math.Sqrt(p.c-p.n*alpha)) / p.n 835 | rf := (s.A * math.Sqrt(p.c-p.n*p.alphaf)) / p.n 836 | 837 | east = p.eastf + r*math.Sin(theta) 838 | north = p.northf + rf - r*math.Cos(theta) 839 | 840 | return east, north, h 841 | } 842 | 843 | func LambertAzimuthalEqualArea(base CRS, lonf, latf, eastf, northf float64) CRS { 844 | if base == nil { 845 | base = Geographic(nil, NewSpheroid(6378137, 298.257223563)) 846 | } 847 | 848 | s := base.Spheroid() 849 | 850 | phi0 := radian(latf) 851 | lambda0 := radian(lonf) 852 | 853 | q0 := (1 - s.E2) * ((math.Sin(phi0) / (1 - s.E2*sin2(phi0))) - (1 / (2 * s.E) * math.Log((1-s.E*math.Sin(phi0))/(1+s.E*math.Sin(phi0))))) 854 | qp := (1 - s.E2) * ((1 / (1 - s.E2)) - ((1 / (2 * s.E)) * math.Log((1-s.E)/(1+s.E)))) 855 | 856 | beta0 := math.Asin(q0 / qp) 857 | rq := s.A * math.Sqrt(qp/2) 858 | G := s.A * (math.Cos(phi0) / math.Sqrt(1-s.E2*sin2(phi0))) / (rq * math.Cos(beta0)) 859 | 860 | return lambertAzimuthalEqualArea{ 861 | base: base, 862 | phi0: phi0, 863 | lambda0: lambda0, 864 | q0: q0, 865 | qp: qp, 866 | beta0: beta0, 867 | rq: rq, 868 | g: G, 869 | eastf: eastf, 870 | northf: northf, 871 | } 872 | } 873 | 874 | type lambertAzimuthalEqualArea struct { 875 | base CRS 876 | phi0, lambda0, q0, qp, beta0, rq, g float64 877 | eastf float64 878 | northf float64 879 | } 880 | 881 | func (p lambertAzimuthalEqualArea) Base() CRS { 882 | return p.base 883 | } 884 | 885 | func (p lambertAzimuthalEqualArea) Spheroid() Spheroid { 886 | return p.base.Spheroid() 887 | } 888 | 889 | func (p lambertAzimuthalEqualArea) ToBase(east, north, h float64) (lon, lat, h2 float64) { 890 | s := p.base.Spheroid() 891 | 892 | rho := math.Sqrt(math.Pow((east-p.eastf)/p.g, 2) + math.Pow(p.g*(north-p.northf), 2)) 893 | c := 2 * math.Asin(rho/(2*p.rq)) 894 | betai := math.Asin((math.Cos(c) * math.Sin(p.beta0)) + ((p.g * (north - p.northf) * math.Sin(c) * math.Cos(p.beta0)) / rho)) 895 | 896 | phi := betai + ((s.E2/3 + (31*s.E4)/180 + (517*s.E6)/5040) * math.Sin(2*betai)) + 897 | ((23*s.E4)/360+(251*s.E6)/3780)*math.Sin(4*betai) + 898 | ((761*s.E6)/45360)*math.Sin(6*betai) 899 | lambda := p.lambda0 + math.Atan2((east-p.eastf)*math.Sin(c), (p.g*rho*math.Cos(p.beta0)*math.Cos(c)-math.Pow(p.g, 2)*(north-p.northf)*math.Sin(p.beta0)*math.Sin(c))) 900 | 901 | return degree(lambda), degree(phi), h 902 | } 903 | 904 | func (p lambertAzimuthalEqualArea) FromBase(lon, lat, h float64) (east, north, h2 float64) { 905 | s := p.base.Spheroid() 906 | 907 | phi := radian(lat) 908 | lambda := radian(lon) 909 | 910 | q := (1 - s.E2) * ((math.Sin(phi) / (1 - s.E2*sin2(phi))) - (1 / (2 * s.E) * math.Log((1-s.E*math.Sin(phi))/(1+s.E*math.Sin(phi))))) 911 | 912 | beta0 := math.Asin(p.q0 / p.qp) 913 | rq := s.A * math.Sqrt(p.qp/2) 914 | g := s.A * (math.Cos(p.phi0) / math.Sqrt(1-s.E2*sin2(p.phi0))) / (rq * math.Cos(beta0)) 915 | 916 | beta := math.Asin(q / p.qp) 917 | b := rq * math.Sqrt(2/(1+math.Sin(beta0)*math.Sin(beta)+(math.Cos(beta0)*math.Cos(beta)*math.Cos(lambda-p.lambda0)))) 918 | 919 | east = p.eastf + ((b * g) * (math.Cos(beta) * math.Sin(lambda-p.lambda0))) 920 | north = p.northf + (b/g)*((math.Cos(beta0)*math.Sin(beta))-(math.Sin(beta0)*math.Cos(beta)*math.Cos(lambda-p.lambda0))) 921 | 922 | return east, north, h 923 | } 924 | 925 | func Krovak(base CRS, lonf, latf, azimuth, sp, scale, eastf, northf float64) CRS { 926 | if base == nil { 927 | base = Geographic(nil, NewSpheroid(6378137, 298.257223563)) 928 | } 929 | 930 | s := base.Spheroid() 931 | 932 | phic := radian(latf) 933 | lambda0 := radian(lonf) 934 | phip := radian(sp) 935 | alphac := radian(azimuth) 936 | 937 | A := s.A * math.Sqrt(1-s.E2) / (1 - s.E2*sin2(phic)) 938 | B := math.Sqrt(1 + (s.E2 * math.Pow(math.Cos(phic), 4) / (1 - s.E2))) 939 | gamma0 := math.Asin(math.Sin(phic) / B) 940 | t0 := math.Tan(math.Pi/4+gamma0/2) * math.Pow((1+s.E*math.Sin(phic))/(1-s.E*math.Sin(phic)), s.E*B/2) / math.Pow(math.Tan(math.Pi/4+phic/2), B) 941 | n := math.Sin(phip) 942 | r0 := scale * A / math.Tan(phip) 943 | 944 | return krovak{ 945 | base: base, 946 | lambda0: lambda0, 947 | phip: phip, 948 | alphac: alphac, 949 | b: B, 950 | t0: t0, 951 | n: n, 952 | r0: r0, 953 | eastf: eastf, 954 | northf: northf, 955 | } 956 | } 957 | 958 | type krovak struct { 959 | base CRS 960 | lambda0, phip, alphac, b, t0, n, r0 float64 961 | eastf float64 962 | northf float64 963 | } 964 | 965 | func (p krovak) Base() CRS { 966 | return p.base 967 | } 968 | 969 | func (p krovak) Spheroid() Spheroid { 970 | return p.base.Spheroid() 971 | } 972 | 973 | func (p krovak) FromBase(lon, lat, h float64) (east, north, h2 float64) { 974 | s := p.base.Spheroid() 975 | 976 | phi := radian(lat) 977 | lambda := radian(lon) 978 | 979 | U := 2 * (math.Atan(p.t0*math.Pow(math.Tan(phi/2+math.Pi/4), p.b)/math.Pow((1+s.E*math.Sin(phi))/(1-s.E*math.Sin(phi)), s.E*p.b/2)) - math.Pi/4) 980 | V := p.b * (p.lambda0 - lambda) 981 | T := math.Asin(math.Cos(p.alphac)*math.Sin(U) + math.Sin(p.alphac)*math.Cos(U)*math.Cos(V)) 982 | D := math.Asin(math.Cos(U) * math.Sin(V) / math.Cos(T)) 983 | theta := p.n * D 984 | r := p.r0 * math.Pow(math.Tan(math.Pi/4+p.phip/2), p.n) / math.Pow(math.Tan(T/2+math.Pi/4), p.n) 985 | Xp := r * math.Cos(theta) 986 | Yp := r * math.Sin(theta) 987 | 988 | return -(Yp + p.eastf), -(Xp + p.northf), h 989 | } 990 | 991 | func (p krovak) ToBase(east, north, h float64) (lon, lat, h2 float64) { 992 | s := p.base.Spheroid() 993 | 994 | Xpi := (-north) - p.northf 995 | Ypi := (-east) - p.eastf 996 | ri := math.Sqrt(math.Pow(Xpi, 2) + math.Pow(Ypi, 2)) 997 | thetai := math.Atan2(Ypi, Xpi) 998 | di := thetai / math.Sin(p.phip) 999 | ti := 2 * (math.Atan(math.Pow(p.r0/ri, 1/p.n)*math.Tan(math.Pi/4+p.phip/2)) - math.Pi/4) 1000 | ui := math.Asin(math.Cos(p.alphac)*math.Sin(ti) - math.Sin(p.alphac)*math.Cos(ti)*math.Cos(di)) 1001 | vi := math.Asin(math.Cos(ti) * math.Sin(di) / math.Cos(ui)) 1002 | 1003 | phi := ui 1004 | 1005 | for i := 0; i < 3; i++ { 1006 | phi = 2 * (math.Atan(math.Pow(p.t0, -1/p.b)*math.Pow(math.Tan(ui/2+math.Pi/4), 1/p.b)*math.Pow((1+s.E*math.Sin(phi))/(1-s.E*math.Sin(phi)), s.E/2)) - math.Pi/4) 1007 | } 1008 | 1009 | lambda := p.lambda0 - vi/p.b 1010 | 1011 | return degree(lambda), degree(phi), h 1012 | } 1013 | 1014 | func Swiss(lv95 bool) CRS { 1015 | return swiss{ 1016 | lv95: lv95, 1017 | base: EPSG(4326), 1018 | spheroid: NewSpheroid(6377397.155, 299.1528128), 1019 | } 1020 | } 1021 | 1022 | type swiss struct { 1023 | lv95 bool 1024 | base CRS 1025 | spheroid Spheroid 1026 | } 1027 | 1028 | func (p swiss) Base() CRS { 1029 | return p.base 1030 | } 1031 | 1032 | func (p swiss) Spheroid() Spheroid { 1033 | return p.spheroid 1034 | } 1035 | 1036 | func (p swiss) ToBase(east, north, h float64) (float64, float64, float64) { 1037 | if p.lv95 { 1038 | east, north = p.lv95ToLv03(east, north) 1039 | } 1040 | 1041 | return p.lv03ToWgs84(east, north, h) 1042 | } 1043 | 1044 | func (p swiss) FromBase(lon, lat, h float64) (float64, float64, float64) { 1045 | x, y, h2 := p.wgs84ToLv03(lon, lat, h) 1046 | 1047 | if p.lv95 { 1048 | x, y = p.lv03ToLv95(x, y) 1049 | } 1050 | 1051 | return x, y, h2 1052 | } 1053 | 1054 | func (p swiss) lv95ToLv03(east, north float64) (float64, float64) { 1055 | east -= 2000000.00 1056 | north -= 1000000.00 1057 | 1058 | return east, north 1059 | } 1060 | 1061 | func (p swiss) lv03ToLv95(east, north float64) (float64, float64) { 1062 | east += 2000000.00 1063 | north += 1000000.00 1064 | 1065 | return east, north 1066 | } 1067 | 1068 | // Convert decimal angle (° dec) to sexagesimal angle (dd.mmss,ss) 1069 | func (p swiss) decToSexAngle(dec float64) float64 { 1070 | deg := math.Floor(dec) 1071 | minute := math.Floor((dec - deg) * 60) 1072 | second := (((dec - deg) * 60) - minute) * 60 1073 | 1074 | return second + minute*60.0 + deg*3600.0 1075 | } 1076 | 1077 | func (p swiss) lv03ToWgs84(east float64, north float64, h float64) (float64, float64, float64) { 1078 | // Converts military to civil and to unit = 1000km: 1079 | // - Military (for LV03): normally used national coordinates. Origin of the coordinates 600'000/200'000 m in Bern (= LTOP ‘MI’). 1080 | // - Civil (for LV03): old format, currently still used in Liechtenstein. Origin of the coordinates 0/0 m in Bern (= LTOP ‘ZI’). 1081 | eastAux := (east - 600000) / 1000000 1082 | northAux := (north - 200000) / 1000000 1083 | 1084 | // Convert latitude (north, y) and longitude (east, x) 1085 | lat := (16.9023892 + (3.238272 * northAux)) - (0.270978 * math.Pow(eastAux, 2)) - (0.002528 * math.Pow(northAux, 2)) - (0.0447 * math.Pow(eastAux, 2) * northAux) - (0.0140 * math.Pow(northAux, 3)) 1086 | lon := (2.6779094 + (4.728982 * eastAux) + (0.791484 * eastAux * northAux) + (0.1306 * eastAux * math.Pow(northAux, 2))) - (0.0436 * math.Pow(eastAux, 3)) 1087 | 1088 | // Unit 10000" to 1 " and converts seconds to degrees (dec) 1089 | lat = (lat * 100) / 36 1090 | lon = (lon * 100) / 36 1091 | 1092 | // Convert height 1093 | h2 := (h + 49.55) - (12.60 * eastAux) - (22.64 * northAux) 1094 | 1095 | return lon, lat, h2 1096 | } 1097 | 1098 | func (p swiss) wgs84ToLv03(lon float64, lat float64, h float64) (float64, float64, float64) { 1099 | lat = p.decToSexAngle(lat) 1100 | lon = p.decToSexAngle(lon) 1101 | 1102 | // Auxiliary values (...Bern) 1103 | latAux := (lat - 169028.66) / 10000 1104 | lonAux := (lon - 26782.5) / 10000 1105 | 1106 | // Convert longitude (east, x) 1107 | x := (600072.37 + (211455.93 * lonAux)) - (10938.51 * lonAux * latAux) - (0.36 * lonAux * math.Pow(latAux, 2)) - (44.54 * math.Pow(lonAux, 3)) 1108 | 1109 | // Convert latitude (north, y) 1110 | y := ((200147.07 + (308807.95 * latAux) + (3745.25 * math.Pow(lonAux, 2)) + (76.63 * math.Pow(latAux, 2))) - (194.56 * math.Pow(lonAux, 2) * latAux)) + (119.79 * math.Pow(latAux, 3)) 1111 | 1112 | // Convert height 1113 | h = (h - 49.55) + (2.73 * lonAux) + (6.94 * latAux) 1114 | 1115 | return x, y, h 1116 | } 1117 | 1118 | func sin2(r float64) float64 { 1119 | return math.Pow(math.Sin(r), 2) 1120 | } 1121 | 1122 | func degree(r float64) float64 { 1123 | return r * 180 / math.Pi 1124 | } 1125 | 1126 | func radian(g float64) float64 { 1127 | return g * math.Pi / 180 1128 | } 1129 | -------------------------------------------------------------------------------- /wgs84_test.go: -------------------------------------------------------------------------------- 1 | package wgs84 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestTransform(t *testing.T) { 8 | tests := []struct { 9 | name string 10 | fromEpsg int 11 | inA float64 12 | inB float64 13 | inC float64 14 | toEpsg int 15 | wantA float64 16 | wantB float64 17 | wantC float64 18 | }{ 19 | {"Web Mercator: EPSG(4326) to EPSG(3857)", 4326, 10, 50, 0, 3857, 1.113194908e+06, 6.446275841e+06, 0}, 20 | {"OSGB: EPSG(4326) to EPSG(27700)", 4326, -2.25, 52.25, 0, 27700, 383029.296, 261341.615, 0}, 21 | } 22 | for _, tt := range tests { 23 | t.Run(tt.name, func(t *testing.T) { 24 | fromEPSG := EPSG(tt.fromEpsg) 25 | toEPSG := EPSG(tt.toEpsg) 26 | 27 | transform := Transform(fromEPSG, toEPSG).Round(3) 28 | 29 | gotA, gotB, gotC := transform(tt.inA, tt.inB, tt.inC) 30 | 31 | if gotA != tt.wantA { 32 | t.Errorf("Transform() A = %v, want %v", gotA, tt.wantA) 33 | } 34 | if gotB != tt.wantB { 35 | t.Errorf("Transform() B = %v, want %v", gotB, tt.wantB) 36 | } 37 | if gotC != tt.wantC { 38 | t.Errorf("Transform() C = %v, want %v", gotC, tt.wantC) 39 | } 40 | }) 41 | } 42 | } 43 | --------------------------------------------------------------------------------