├── .fleet └── settings.json ├── .github └── workflows │ ├── go-test.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── .whitesource ├── ACKNOWLEDGMENTS.md ├── LICENSE ├── README.md ├── assert └── assert.go ├── classification ├── classification.go └── classification_test.go ├── clustering ├── clustering.go └── clustering_test.go ├── constants └── constants.go ├── conversions ├── conversions.go └── conversions_test.go ├── docs.md ├── go.mod ├── go.sum ├── internal └── common │ └── common.go ├── invariant ├── invariant.go └── invariant_test.go ├── joins.go ├── joins_test.go ├── makefile ├── measurement ├── measurement.go └── measurement_test.go ├── meta ├── coordAll │ ├── coordAll.go │ └── coordAll_test.go └── coordEach │ ├── coordEach.go │ └── coordEach_test.go ├── projection ├── projection.go └── projection_test.go ├── random ├── random.go └── random_test.go ├── renovate.json ├── test-data ├── along-dc-line.json ├── area-feature-collection.json ├── area-geom-multipolgon.json ├── area-geom-polygon.json ├── area-multipolygon.json ├── area-polygon.json ├── bbox-featurecollection.json ├── bbox-geometry-multipolygon.json ├── bbox-linestring.json ├── bbox-multilinestring.json ├── bbox-multipoint.json ├── bbox-multipolygon.json ├── bbox-point.json ├── bbox-polygon-linestring.json ├── bbox-polygon.json ├── imbalanced-polygon.json ├── mercator.featurecollection.geojson ├── mercator.geometry.linestring.geojson ├── mercator.geometry.multilinestring.geojson ├── mercator.geometry.multipoint.geojson ├── mercator.geometry.multipolygon.geojson ├── mercator.geometry.point.geojson ├── mercator.geometry.polygon.geojson ├── mercator.geometrycollection.geojson ├── mercator.linestring.geojson ├── mercator.multilinestring.geojson ├── mercator.multipoint.geojson ├── mercator.multipolygon.geojson ├── mercator.passed180thmeridian.geojson ├── mercator.passed180thmeridian2.geojson ├── mercator.point.geojson ├── mercator.polygon.geojson ├── multiLineString.json ├── multipoly-with-hole.json ├── poly-with-hole.json ├── polygon.json ├── route1.json ├── route2.json ├── wgs84.featurecollection.geojson ├── wgs84.geometry.linestring.geojson ├── wgs84.geometry.multilinestring.geojson ├── wgs84.geometry.multipoint.geojson ├── wgs84.geometry.multipolygon.geojson ├── wgs84.geometry.point.geojson ├── wgs84.geometry.polygon.geojson ├── wgs84.geometrycollection.geojson ├── wgs84.linestring.geojson ├── wgs84.multilinestring.geojson ├── wgs84.multipoint.geojson ├── wgs84.multipolygon.geojson ├── wgs84.passed180thmeridian.geojson ├── wgs84.passed180thmeridian2.geojson ├── wgs84.point.geojson └── wgs84.polygon.geojson ├── test └── assert │ └── assert.go └── utils ├── common.go └── utils.go /.fleet/settings.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomchavakis/turf-go/c91bf2ab84507f27a318fe79c2b7e0a401fc2e60/.fleet/settings.json -------------------------------------------------------------------------------- /.github/workflows/go-test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: ["1.17.x", "1.18.x", "1.19.x"] 8 | os: ["ubuntu-latest"] 9 | runs-on: ${{ matrix.os }} 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v4 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | - name: Checkout code 16 | uses: actions/checkout@v3 17 | - name: Run Tests 18 | run: go test -v -coverprofile=profile.cov ./... 19 | - name: Coveralls 20 | uses: shogo82148/actions-goveralls@v1 21 | with: 22 | path-to-profile: profile.cov 23 | flag-name: Go-${{ matrix.go }} 24 | parallel: true 25 | 26 | # notifies that all test jobs are finished. 27 | finish: 28 | needs: test 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: shogo82148/actions-goveralls@v1 32 | with: 33 | parallel-finished: true 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Go project 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" # triggers only if push new tag version, like `v0.8.4` or else 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | - name: Set up Go 17 | uses: actions/setup-go@v4 18 | with: 19 | go-version: ">=1.17.0" 20 | - name: Run GoReleaser 21 | uses: goreleaser/goreleaser-action@v3 22 | with: 23 | distribution: goreleaser 24 | version: latest 25 | args: release --rm-dist 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | *.log 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | 18 | .idea/ 19 | .vscode/ -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod tidy 4 | builds: 5 | - skip: true 6 | changelog: 7 | sort: asc 8 | filters: 9 | exclude: 10 | - "^docs:" 11 | - "^test:" 12 | -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "scanSettings": { 3 | "baseBranches": [] 4 | }, 5 | "checkRunSettings": { 6 | "vulnerableCheckRunConclusionLevel": "failure", 7 | "displayMode": "diff", 8 | "useMendCheckNames": true 9 | }, 10 | "issueSettings": { 11 | "minSeverityLevel": "LOW", 12 | "issueType": "DEPENDENCY" 13 | } 14 | } -------------------------------------------------------------------------------- /ACKNOWLEDGMENTS.md: -------------------------------------------------------------------------------- 1 | # Acknowledgments 2 | 3 | The framework makes primarily use of the following projects: 4 | 5 | 6 | [mapbox-java](https://github.com/mapbox/mapbox-java) 7 | 8 | [turf](https://github.com/Turfjs/turf) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Morgan Herlocker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :hammer: [![release](https://badgen.net/github/release/tomchavakis/turf-go)](https://github.com/tomchavakis/turf-go/releases/latest) [![GoDoc](https://godoc.org/github.com/tomchavakis/turf-go?status.svg)](https://godoc.org/github.com/tomchavakis/turf-go) [![GitHub license](https://badgen.net/github/license/tomchavakis/turf-go)](https://github.com/tomchavakis/turf-go/blob/master/LICENSE) [![Go Report Card](https://goreportcard.com/badge/github.com/tomchavakis/turf-go)](https://goreportcard.com/report/github.com/tomchavakis/turf-go) [![Coverage Status](https://coveralls.io/repos/github/tomchavakis/turf-go/badge.svg?branch=master)](https://coveralls.io/github/tomchavakis/turf-go?branch=master) 2 | 3 | # turf-go 4 | A Go language port of [Turfjs](http://turfjs.org/docs/) 5 | 6 | ## Turf for Go 7 | 8 | Turf for Go is a ported library in GoLang ported from the Turf.js library. 9 | 10 | ## Extra modules 11 | This version also include the clustering module that doesn't exist in the official turf library. 12 | 13 | # Ported functions 14 | 15 | ## measurement 16 | 17 | - [x] along 18 | - [x] area 19 | - [x] bbox 20 | - [x] bboxPolygon 21 | - [x] bearing 22 | - [x] center 23 | - [ ] centerOfMass 24 | - [x] centroid 25 | - [x] destination 26 | - [x] distance 27 | - [x] envelope 28 | - [x] length 29 | - [x] midpoint 30 | - [ ] pointOnFeature 31 | - [ ] polygonTangents 32 | - [ ] pointToLineDistance 33 | - [x] rhumbBearing 34 | - [x] rhumbDestination 35 | - [x] rhumbDistance 36 | - [ ] square 37 | - [ ] greatCircle 38 | 39 | ## clustering 40 | - [x] kmeans 41 | 42 | ## Coordinate Mutation 43 | - [ ] cleanCoords 44 | - [ ] flip 45 | - [ ] rewind 46 | - [ ] round 47 | - [ ] truncate 48 | 49 | ## Transformation 50 | - [ ] bboxClip 51 | - [ ] bezierSpline 52 | - [ ] buffer 53 | - [ ] circle 54 | - [ ] clone 55 | - [ ] concave 56 | - [ ] convex 57 | - [ ] difference 58 | - [ ] dissolve 59 | - [ ] intersect 60 | - [ ] lineOffset 61 | - [ ] simplify 62 | - [ ] tesselate 63 | - [ ] transformRotate 64 | - [ ] transformTranslate 65 | - [ ] transformScale 66 | - [ ] union 67 | - [ ] voronoi 68 | 69 | ## Feature Conversion 70 | - [ ] combine 71 | - [ ] explode 72 | - [ ] flatten 73 | - [ ] lineToPolygon 74 | - [ ] polygonize 75 | - [ ] polygonToLine 76 | 77 | ## Misc 78 | - [ ] kinks 79 | - [ ] lineArc 80 | - [ ] lineChunk 81 | - [ ] lineIntersect 82 | - [ ] lineOverlap 83 | - [ ] lineSegment 84 | - [ ] lineSlice 85 | - [ ] lineSliceAlong 86 | - [ ] lineSplit 87 | - [ ] mask 88 | - [ ] nearestPointOnLine 89 | - [ ] sector 90 | - [ ] shortestPath 91 | - [ ] unkinkPolygon 92 | 93 | ## Helper 94 | - [x] featureCollection 95 | - [x] feature 96 | - [x] geometryCollection 97 | - [x] lineString 98 | - [x] multiLineString 99 | - [x] multiPoint 100 | - [x] multiPolygon 101 | - [x] point 102 | - [x] polygon 103 | 104 | ## Random 105 | - [x] randomPosition 106 | - [x] randomPoint 107 | - [x] randomLineString 108 | - [x] randomPolygon 109 | 110 | ## Data 111 | - [ ] sample 112 | 113 | ## Joins 114 | - [x] pointsWithinPolygon 115 | - [ ] tag 116 | 117 | ## Grids 118 | - [ ] hexGrid 119 | - [ ] pointGrid 120 | - [ ] squareGrid 121 | - [ ] triangleGrid 122 | 123 | ## Classification 124 | - [x] nearestPoint 125 | 126 | ## Aggregation 127 | - [ ] collect 128 | - [ ] clustersDbscan 129 | - [ ] clustersKmeans 130 | 131 | ## Meta - Invariant 132 | - [x] coordAll 133 | - [x] coordEach 134 | - [ ] coordReduce 135 | - [ ] featureEach 136 | - [ ] featureReduce 137 | - [ ] flattenEach 138 | - [ ] flattenReduce 139 | - [x] getCoord 140 | - [x] getCoords 141 | - [x] getGeom 142 | - [x] getType 143 | - [ ] geomEach 144 | - [ ] geomReduce 145 | - [ ] propEach 146 | - [ ] propReduce 147 | - [ ] segmentEach 148 | - [ ] segmentReduce 149 | - [ ] getCluster 150 | - [ ] clusterEach 151 | - [ ] clusterReduce 152 | 153 | ## Assertions 154 | - [ ] collectionOf 155 | - [ ] containsNumber 156 | - [ ] geojsonType 157 | - [ ] featureOf 158 | 159 | ## Booleans 160 | - [ ] booleanClockwise 161 | - [ ] booleanContains 162 | - [ ] booleanCrosses 163 | - [ ] booleanDisjoint 164 | - [ ] booleanEqual 165 | - [ ] booleanOverlap 166 | - [ ] booleanParallel 167 | - [x] booleanPointInPolygon 168 | - [ ] booleanPointOnLine 169 | - [ ] booleanWithin 170 | 171 | ## Unit Conversion 172 | - [x] bearingToAzimuth 173 | - [x] convertArea 174 | - [x] convertLength 175 | - [x] degreesToRadians 176 | - [x] lengthToRadians 177 | - [x] lengthToDegrees 178 | - [x] radiansToLength 179 | - [x] radiansToDegrees 180 | - [x] toMercator 181 | - [x] toWgs84 182 | 183 | 184 | 185 | 186 | 187 | ## References: 188 | 189 | https://github.com/mapbox/mapbox-java 190 | 191 | https://github.com/Turfjs/turf 192 | -------------------------------------------------------------------------------- /assert/assert.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | // Equal checks if values are equal 10 | func Equal(t *testing.T, a interface{}, b interface{}) { 11 | 12 | if a == nil || b == nil { 13 | if a == b { 14 | return 15 | } 16 | 17 | } 18 | 19 | exp, ok := a.([]byte) 20 | if !ok { 21 | if reflect.DeepEqual(a, b) { 22 | return 23 | } 24 | } 25 | 26 | act, ok := b.([]byte) 27 | if !ok { 28 | t.Errorf("Received %v (type %v), expected %v (type %v)", a, reflect.TypeOf(a), b, reflect.TypeOf(b)) 29 | } 30 | 31 | if exp == nil || act == nil { 32 | if exp == nil && act == nil { 33 | return 34 | } 35 | } 36 | 37 | if bytes.Equal(exp, act) { 38 | return 39 | } 40 | 41 | t.Errorf("Received %v (type %v), expected %v (type %v)", a, reflect.TypeOf(a), b, reflect.TypeOf(b)) 42 | } 43 | 44 | // True asserts that the specified value is true 45 | func True(t *testing.T, value bool, msgAndArgs ...interface{}) bool { 46 | if !value { 47 | t.Errorf("Received %v, expected %v", value, true) 48 | } 49 | return value 50 | } 51 | 52 | func containsKind(kinds []reflect.Kind, kind reflect.Kind) bool { 53 | for i := 0; i < len(kinds); i++ { 54 | if kind == kinds[i] { 55 | return true 56 | } 57 | } 58 | 59 | return false 60 | } 61 | 62 | func isNil(object interface{}) bool { 63 | if object == nil { 64 | return true 65 | } 66 | 67 | value := reflect.ValueOf(object) 68 | kind := value.Kind() 69 | isNilableKind := containsKind( 70 | []reflect.Kind{ 71 | reflect.Chan, reflect.Func, 72 | reflect.Interface, reflect.Map, 73 | reflect.Ptr, reflect.Slice}, 74 | kind) 75 | 76 | if isNilableKind && value.IsNil() { 77 | return true 78 | } 79 | 80 | return false 81 | } 82 | 83 | // Nil asserts that the specified object is nil. 84 | // 85 | // assert.Nil(t, err) 86 | func Nil(t *testing.T, object interface{}) bool { 87 | return isNil(object) 88 | } 89 | 90 | // NotNil asserts that the specified object is not nil. 91 | // 92 | // assert.NotNil(t, err) 93 | func NotNil(t *testing.T, object interface{}) bool { 94 | return !isNil(object) 95 | } 96 | -------------------------------------------------------------------------------- /classification/classification.go: -------------------------------------------------------------------------------- 1 | package classification 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/tomchavakis/geojson/geometry" 7 | "github.com/tomchavakis/turf-go/measurement" 8 | ) 9 | 10 | // NearestPoint takes a reference point and a list of points and returns the point from the point list closest to the reference. 11 | func NearestPoint(refPoint geometry.Point, points []geometry.Point, units string) (*geometry.Point, error) { 12 | if len(points) == 0 { 13 | return &refPoint, nil 14 | } 15 | 16 | p := points[0] 17 | minDist := math.MaxFloat64 18 | 19 | for _, point := range points { 20 | dist, err := measurement.PointDistance(refPoint, point, units) 21 | if err != nil { 22 | return nil, err 23 | } 24 | if dist < minDist { 25 | p = point 26 | minDist = dist 27 | } 28 | } 29 | 30 | return &p, nil 31 | } 32 | -------------------------------------------------------------------------------- /classification/classification_test.go: -------------------------------------------------------------------------------- 1 | package classification 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tomchavakis/geojson/geometry" 7 | "github.com/tomchavakis/turf-go/constants" 8 | ) 9 | 10 | func TestNearestPoint(t *testing.T) { 11 | 12 | p1 := geometry.Point{Lng: -75.33, Lat: 39.44} 13 | p2 := geometry.Point{Lng: -75.33, Lat: 39.45} 14 | p3 := geometry.Point{Lng: -75.31, Lat: 39.46} 15 | p4 := geometry.Point{Lng: -75.30, Lat: 39.46} 16 | 17 | var points []geometry.Point 18 | points = append(points, p1, p2, p3, p4) 19 | 20 | refPoint := geometry.Point{Lat: 39.50, Lng: -75.33} 21 | 22 | np, err := NearestPoint(refPoint, points, constants.UnitDefault) 23 | if err != nil { 24 | t.Errorf("nearest point error: %v", err) 25 | } 26 | if np != nil && *np != p3 { 27 | t.Errorf("nearestPoint = %f; want %f", np, p3) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /clustering/clustering.go: -------------------------------------------------------------------------------- 1 | package clustering 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math" 7 | "math/rand" 8 | "time" 9 | 10 | "github.com/tomchavakis/geojson/geometry" 11 | "github.com/tomchavakis/turf-go/measurement" 12 | meta "github.com/tomchavakis/turf-go/meta/coordAll" 13 | ) 14 | 15 | type Distance string 16 | 17 | const ( 18 | Euclidean Distance = "Euclidean" 19 | Haversine Distance = "Haversine" 20 | ) 21 | 22 | // Parameters for the KMean Clustering 23 | type Parameters struct { 24 | k int // number of clusters 25 | points []geometry.Point // pointSet 26 | distanceType Distance 27 | } 28 | 29 | // KMeans initialisation 30 | // http://ilpubs.stanford.edu:8090/778/1/2006-13.pdf 31 | func KMeans(params Parameters) (map[geometry.Point][]geometry.Point, error) { 32 | 33 | if params.k < 2 { 34 | return nil, fmt.Errorf("at least 2 centroids required") 35 | } 36 | 37 | if params.k > len(params.points) { 38 | return nil, fmt.Errorf("clusters can't be more than the length of the points") 39 | } 40 | 41 | return getClusters(params) 42 | 43 | } 44 | 45 | func initialisation(tmpCluster map[geometry.Point][]geometry.Point, centroids []geometry.Point, ctrIdx map[int]bool, params Parameters) (map[geometry.Point][]geometry.Point, error) { 46 | // create a cluster of points based on random centroids 47 | for i, p := range params.points { 48 | if _, isCentroid := ctrIdx[i]; isCentroid { 49 | //tmpCluster[p] = tmpCluster[p] 50 | continue 51 | } 52 | 53 | // get the distance from all the centroids 54 | ptCtrsDistances, err := getDistance(p, centroids, params.distanceType) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | // get the minimum distance index for this point 60 | minDistanceIdx := minDistanceIdx(ptCtrsDistances) 61 | nearestCentroid := centroids[minDistanceIdx] 62 | tmpCluster[nearestCentroid] = append(tmpCluster[nearestCentroid], p) 63 | } 64 | return tmpCluster, nil 65 | } 66 | 67 | func getClusters(params Parameters) (map[geometry.Point][]geometry.Point, error) { 68 | ctrIdx, centroids := getCentroids(params) 69 | 70 | tmpCluster := make(map[geometry.Point][]geometry.Point) 71 | meanCluster := make(map[geometry.Point][]geometry.Point) 72 | 73 | init := false 74 | for { 75 | tmpMeanCluster := make(map[geometry.Point][]geometry.Point) 76 | 77 | if !init { 78 | tmpCl, err := initialisation(tmpCluster, centroids, ctrIdx, params) 79 | if err != nil { 80 | return nil, err 81 | } 82 | tmpCluster = tmpCl 83 | } 84 | 85 | // calculate the mass mean of each cluster 86 | clusterMeanPoints := []geometry.Point{} 87 | for i, c := range tmpCluster { 88 | // median included 89 | if !init { 90 | c = append(c, i) 91 | init = true 92 | } 93 | meanClusterPoint, err := meanClusterPoint(c) 94 | if err != nil { 95 | return nil, err 96 | } 97 | clusterMeanPoints = append(clusterMeanPoints, *meanClusterPoint) 98 | } 99 | 100 | for _, p := range params.points { 101 | 102 | // get the distance from all the cluster mean 103 | meanCtrsDistances, err := getDistance(p, clusterMeanPoints, params.distanceType) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | // get the minimum distance index for this point 109 | minDistanceIdx := minDistanceIdx(meanCtrsDistances) 110 | nearestMean := clusterMeanPoints[minDistanceIdx] 111 | tmpMeanCluster[nearestMean] = append(tmpMeanCluster[nearestMean], p) 112 | } 113 | tmpCluster = tmpMeanCluster 114 | // exit point 115 | if isEqual(meanCluster, tmpMeanCluster) { 116 | break 117 | } 118 | 119 | meanCluster = tmpMeanCluster 120 | } 121 | 122 | //getDistance(d) 123 | return meanCluster, nil 124 | } 125 | 126 | // TODO: Find the centroid 127 | 128 | // https://sites.google.com/site/yangdingqi/home/foursquare-dataset 129 | // https://desktop.arcgis.com/en/arcmap/latest/tools/spatial-statistics-toolbox/h-how-mean-center-spatial-statistics-works.html 130 | // https://postgis.net/docs/ST_Centroid.html 131 | // https://cloud.google.com/bigquery/docs/reference/standard-sql/geography_functions#st_centroid 132 | // https://stackoverflow.com/questions/30299267/geometric-median-of-multidimensional-points 133 | // https://www.pnas.org/content/pnas/97/4/1423.full.pdf 134 | func meanClusterPoint(cluster []geometry.Point) (*geometry.Point, error) { 135 | var pointSet = []geometry.Point{} 136 | excludeWrapCoord := true 137 | for _, v := range cluster { 138 | coords, err := meta.CoordAll(&v, &excludeWrapCoord) 139 | if err != nil { 140 | return nil, err 141 | } 142 | pointSet = append(pointSet, coords...) 143 | } 144 | 145 | coordsLength := len(pointSet) 146 | if coordsLength < 1 { 147 | return nil, errors.New("no coordinates found") 148 | } 149 | 150 | xSum := 0.0 151 | ySum := 0.0 152 | 153 | for i := 0; i < coordsLength; i++ { 154 | xSum += pointSet[i].Lng 155 | ySum += pointSet[i].Lat 156 | } 157 | 158 | finalCenterLongtitude := xSum / float64(coordsLength) 159 | finalCenterLatitude := ySum / float64(coordsLength) 160 | 161 | return &geometry.Point{ 162 | Lat: finalCenterLatitude, 163 | Lng: finalCenterLongtitude, 164 | }, nil 165 | 166 | } 167 | 168 | func getCentroids(params Parameters) (map[int]bool, []geometry.Point) { 169 | var centroids []geometry.Point 170 | ctrIdx := make(map[int]bool) 171 | 172 | idx := getRandoms(len(params.points), params.k) 173 | for _, v := range idx { 174 | centroids = append(centroids, params.points[v]) 175 | ctrIdx[v] = true 176 | } 177 | 178 | return ctrIdx, centroids 179 | } 180 | 181 | // l length, k number of clusters 182 | func getRandoms(l int, k int) []int { 183 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 184 | return r.Perm(l)[:k] 185 | } 186 | 187 | func euclideanDistance(lat1 float64, lon1 float64, lat2 float64, lon2 float64) float64 { 188 | dist := math.Sqrt(math.Pow(lat2-lat1, 2) + math.Pow(lon2-lon1, 2)) 189 | return dist 190 | } 191 | 192 | // getDistance returns the distance between the point and the centroids 193 | func getDistance(p geometry.Point, centroids []geometry.Point, dt Distance) (map[int]float64, error) { 194 | ds := make(map[int]float64) 195 | 196 | if dt == Euclidean { 197 | for i, c := range centroids { 198 | d := euclideanDistance(p.Lat, p.Lng, c.Lat, c.Lng) 199 | ds[i] = d 200 | } 201 | } else { // haversine distance implemented 202 | for i, c := range centroids { 203 | d, err := measurement.Distance(p.Lng, p.Lat, c.Lng, c.Lat, "") 204 | if err != nil { 205 | return nil, err 206 | } 207 | ds[i] = d 208 | } 209 | } 210 | return ds, nil 211 | } 212 | 213 | func memoizeCluster(key geometry.Point, cluster []geometry.Point) map[string]bool { 214 | mr := make(map[string]bool) 215 | for _, v := range cluster { 216 | s := memoizationSignature(key, v) 217 | mr[s] = true 218 | } 219 | return mr 220 | } 221 | 222 | func memoizationSignature(key geometry.Point, p geometry.Point) string { 223 | return fmt.Sprintf("%f_%f_%f", key, p.Lat, p.Lng) 224 | } 225 | 226 | func mergeMaps(maps ...map[string]bool) map[string]bool { 227 | result := make(map[string]bool) 228 | for _, m := range maps { 229 | for k, v := range m { 230 | result[k] = v 231 | } 232 | } 233 | return result 234 | } 235 | 236 | func isEqual(clusterA map[geometry.Point][]geometry.Point, clusterB map[geometry.Point][]geometry.Point) bool { 237 | if len(clusterA) != len(clusterB) { 238 | return false 239 | } 240 | 241 | memo := make(map[string]bool) 242 | for i, v1 := range clusterA { 243 | tmp := memoizeCluster(i, v1) 244 | memo = mergeMaps(memo, tmp) 245 | } 246 | 247 | for j, arrB := range clusterB { 248 | for _, v := range arrB { 249 | if !memo[memoizationSignature(j, v)] { 250 | return false 251 | } 252 | } 253 | } 254 | 255 | return true 256 | } 257 | 258 | func minDistanceIdx(ptCtrsDistances map[int]float64) int { 259 | minDistanceIdx := 0 260 | minD := ptCtrsDistances[minDistanceIdx] 261 | for i, d := range ptCtrsDistances { 262 | if d < minD { 263 | minD = d 264 | minDistanceIdx = i 265 | } 266 | } 267 | return minDistanceIdx 268 | } 269 | -------------------------------------------------------------------------------- /clustering/clustering_test.go: -------------------------------------------------------------------------------- 1 | package clustering 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/tomchavakis/geojson/geometry" 8 | "github.com/tomchavakis/turf-go/assert" 9 | ) 10 | 11 | // SELECT ST_AsText(ST_Centroid('MULTIPOINT ( 10.0 20.0 , 10.0 25.0, 11.0 18.0, 10.0 18.0 )')); 12 | // SELECT ST_AsText(ST_Centroid('MULTIPOINT ( 10.0 60.0, 11.0 50.0 )')); 13 | 14 | func TestKMeansClusterLength(t *testing.T) { 15 | params := Parameters{ 16 | k: 5, 17 | points: []geometry.Point{{ 18 | Lat: 10.0, 19 | Lng: 1.0, 20 | }, { 21 | Lat: 11.0, 22 | Lng: 1.0, 23 | }, { 24 | Lat: 12.0, 25 | Lng: 1.0, 26 | }, 27 | }, 28 | distanceType: Haversine, 29 | } 30 | 31 | res, err := KMeans(params) 32 | assert.Equal(t, err, fmt.Errorf("clusters can't be more than the length of the points")) 33 | assert.Nil(t, res) 34 | } 35 | 36 | func TestKMeansLeastCentroid(t *testing.T) { 37 | params := Parameters{ 38 | k: 1, 39 | points: []geometry.Point{{ 40 | Lat: 10.0, 41 | Lng: 1.0, 42 | }, { 43 | Lat: 11.0, 44 | Lng: 1.0, 45 | }, { 46 | Lat: 12.0, 47 | Lng: 1.0, 48 | }, 49 | }, 50 | distanceType: Haversine, 51 | } 52 | 53 | res, err := KMeans(params) 54 | assert.Equal(t, err, fmt.Errorf("at least 2 centroids required")) 55 | assert.Nil(t, res) 56 | } 57 | 58 | func TestKMeans(t *testing.T) { 59 | params := Parameters{ 60 | k: 2, 61 | points: []geometry.Point{{Lat: 20.0, Lng: 10.0}, {Lat: 25.0, Lng: 10.0}, {Lat: 60.0, Lng: 10.0}, {Lat: 18.0, Lng: 11.0}, {Lat: 18.0, Lng: 10.0}, {Lat: 50.0, Lng: 11.0}}, 62 | distanceType: Haversine, 63 | } 64 | 65 | res, err := KMeans(params) 66 | assert.Nil(t, err) 67 | assert.NotNil(t, res) 68 | clusters := make(map[geometry.Point][]geometry.Point) 69 | p1 := geometry.Point{ 70 | Lat: 20.25, 71 | Lng: 10.25, 72 | } 73 | p1Cluster := []geometry.Point{} 74 | p1Cluster = append(p1Cluster, *geometry.NewPoint(20.0, 10.0)) 75 | p1Cluster = append(p1Cluster, *geometry.NewPoint(25.0, 10.0)) 76 | p1Cluster = append(p1Cluster, *geometry.NewPoint(18.0, 11.0)) 77 | p1Cluster = append(p1Cluster, *geometry.NewPoint(18.0, 10.0)) 78 | 79 | p2 := geometry.Point{ 80 | Lat: 55.0, 81 | Lng: 10.5, 82 | } 83 | p2Cluster := []geometry.Point{} 84 | p2Cluster = append(p2Cluster, *geometry.NewPoint(60.0, 10.0)) 85 | p2Cluster = append(p2Cluster, *geometry.NewPoint(50.0, 11.0)) 86 | 87 | clusters[p1] = p1Cluster 88 | clusters[p2] = p2Cluster 89 | 90 | assert.Equal(t, len(res), 2) 91 | assert.Equal(t, res, clusters) 92 | } 93 | -------------------------------------------------------------------------------- /constants/constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | // UnitAcres is a unit of land measurement in the British Imperial and United States Customary systems, equal to 43,560 square feet, or 4,840 square yards. One acre is equivalent to 0.4047 hectare (4,047 square metres 5 | UnitAcres = "acres" 6 | // UnitHectares is a unit of area in the metric system equal to 100 ares, or 10,000 square metres, and the equivalent of 2.471 acres in the British Imperial System and the United States Customary measure. The term is derived from the Latin area and from hect, an irregular contraction of the Greek word for hundred. 7 | UnitHectares = "hectares" 8 | // UnitMillimeters spelled as millimeter, unit of length equal to 0.001 metre in the metric system and the equivalent of 0.03937 inch 9 | UnitMillimeters = "millimeters" 10 | // UnitMillimetres spelled as millimetre, unit of length equal to 0.001 metre in the metric system and the equivalent of 0.03937 inch 11 | UnitMillimetres = "millimetres" 12 | // UnitMiles is an English unit of length of linear measure equal to 5.280 feet, or 1.760 yards, and standardised as exactly 1,609.344 meters by international agreement in 1959. 13 | UnitMiles = "miles" 14 | // UnitNauticalMiles us known as the knot. Nautical miles and knots are almost universally used for aeronautical and maritime navigation, because of their relationship with degrees and minutes of latitute 15 | UnitNauticalMiles = "nautical_miles" 16 | // UnitKilometers (American spelling) is a unit of length in the metric system, equal to one thousand meters. 17 | UnitKilometers = "kilometeres" 18 | // UnitRadians is the standard unit of angular measure, used in many areas of mathematics. 19 | UnitRadians = "radians" 20 | // UnitDegrees is a measurement of a plane angle, defined so that a full rotation is 360 degrees. 21 | UnitDegrees = "degrees" 22 | // UnitInches is a unit of length in the (British) imperial and United States customary systems of measurement now formally equal to 1/36th yard but usually understood as 1/12th of a foot. 23 | UnitInches = "inches" 24 | // UnitYards is an English unit of length, in both the British imperial and US customary systems of measurement, that comprises 3 feet or 36 inches. 25 | UnitYards = "yards" 26 | // UnitMeters is the base unit of length in the International System of Units (SI). 27 | UnitMeters = "meters" 28 | // UnitCentimeters is a unit of length in the metric system, equal to one hundredth of a meter. 29 | UnitCentimeters = "centimeters" 30 | // UnitCentimetres (international spelling) is a unit of length in the metric system, equal to one hundredth of a meter. 31 | UnitCentimetres = "centimetres" 32 | // UnitMetres (international spelling) is the base unit of length in the International System of Units (SI) 33 | UnitMetres = "metres" 34 | // UnitKimometres is a unit of length in the metric system, equal to one thousand metres. 35 | UnitKimometres = "kilometres" 36 | // UnitFeet is a unit of length in the imperial and US customary systems of measurement. 37 | UnitFeet = "feet" 38 | // UnitDefault us the default unit used in most Turf methods when no other unit is specified is kilometers 39 | UnitDefault = "kilometres" 40 | // EarthRadius is the radius of the earch in km 41 | // Approximate radius of the earth in meters. The radius at the equator is ~6378137 and at the poles is ~6356752. https://en.wikipedia.org/wiki/World_Geodetic_System#WGS84 42 | // 6371008.8 is one published "average radius" see https://en.wikipedia.org/wiki/Earth_radius#Mean_radius, or ftp://athena.fsv.cvut.cz/ZFG/grs80-Moritz.pdf p.4 43 | // https://github.com/Turfjs/turf/issues/635 44 | EarthRadius = 6371008.8 45 | ) 46 | -------------------------------------------------------------------------------- /conversions/conversions.go: -------------------------------------------------------------------------------- 1 | package conversions 2 | 3 | import ( 4 | "errors" 5 | "math" 6 | 7 | "github.com/tomchavakis/turf-go/constants" 8 | ) 9 | 10 | var factors = map[string]float64{ 11 | constants.UnitMiles: constants.EarthRadius / 1609.344, 12 | constants.UnitNauticalMiles: constants.EarthRadius / 1852.0, 13 | constants.UnitDegrees: constants.EarthRadius / 111325.0, 14 | constants.UnitRadians: 1.0, 15 | constants.UnitInches: constants.EarthRadius * 39.37, 16 | constants.UnitYards: constants.EarthRadius / 1.0936, 17 | constants.UnitMeters: constants.EarthRadius, 18 | constants.UnitCentimeters: constants.EarthRadius * 100.0, 19 | constants.UnitKilometers: constants.EarthRadius / 1000.0, 20 | constants.UnitFeet: constants.EarthRadius * 3.28084, 21 | constants.UnitCentimetres: constants.EarthRadius * 100.0, 22 | constants.UnitMetres: constants.EarthRadius, 23 | constants.UnitKimometres: constants.EarthRadius / 1000.0, 24 | } 25 | 26 | var areaFactors = map[string]float64{ 27 | constants.UnitAcres: 0.000247105, 28 | constants.UnitCentimeters: 10000.0, 29 | constants.UnitCentimetres: 10000.0, 30 | constants.UnitFeet: 10.763910417, 31 | constants.UnitHectares: 0.0001, 32 | constants.UnitInches: 1550.003100006, 33 | constants.UnitKilometers: 0.000001, 34 | constants.UnitKimometres: 0.000001, 35 | constants.UnitMeters: 1.0, 36 | constants.UnitMetres: 1.0, 37 | constants.UnitMiles: 3.86e-7, 38 | constants.UnitMillimeters: 1000000.0, 39 | constants.UnitMillimetres: 1000000.0, 40 | constants.UnitYards: 1.195990046, 41 | } 42 | 43 | // DegreesToRadians converts an angle in degrees to radians. 44 | // degrees angle between 0 and 360 45 | func DegreesToRadians(degrees float64) float64 { 46 | return degrees * math.Pi / 180 47 | } 48 | 49 | // RadiansToDegrees converts radians to degrees 50 | func RadiansToDegrees(radians float64) float64 { 51 | return radians * 180 / math.Pi 52 | } 53 | 54 | // ToKilometersPerHour converts knots to km/h 55 | func ToKilometersPerHour(knots float64) float64 { 56 | return knots * 1.852 57 | } 58 | 59 | // LengthToDegrees convert a distance measurement (assuming a spherical Earth) from a real-world unit into degrees 60 | // Valid units: miles, nauticalmiles, inches, yards, meters, metres, centimeters, kilometres, feet 61 | func LengthToDegrees(distance float64, units string) (float64, error) { 62 | if units == "" { 63 | units = constants.UnitDefault 64 | } 65 | 66 | ltr, err := LengthToRadians(distance, units) 67 | if err != nil { 68 | return 0.0, err 69 | } 70 | 71 | return RadiansToDegrees(ltr), nil 72 | } 73 | 74 | // LengthToRadians convert a distance measurement (assuming a spherical Earth) from a real-world unit into radians. 75 | func LengthToRadians(distance float64, units string) (float64, error) { 76 | if units == "" { 77 | units = constants.UnitDefault 78 | } 79 | if !validateUnit(units) { 80 | return 0.0, errors.New("invalid units") 81 | } 82 | 83 | return distance / factors[units], nil 84 | } 85 | 86 | // RadiansToLength convert a distance measurement (assuming a spherical Earth) from radians to a more friendly unit. 87 | func RadiansToLength(radians float64, units string) (float64, error) { 88 | if units == "" { 89 | units = constants.UnitDefault 90 | } 91 | 92 | if !validateUnit(units) { 93 | return 0.0, errors.New("invalid unit") 94 | } 95 | 96 | return radians * factors[units], nil 97 | } 98 | 99 | // ConvertLength converts a distance to a different unit specified. 100 | func ConvertLength(distance float64, originalUnits string, finalUnits string) (float64, error) { 101 | if originalUnits == "" { 102 | originalUnits = constants.UnitMeters 103 | } 104 | 105 | if finalUnits == "" { 106 | finalUnits = constants.UnitDefault 107 | } 108 | 109 | ltr, err := LengthToRadians(distance, originalUnits) 110 | 111 | if err != nil { 112 | return 0, err 113 | } 114 | return RadiansToLength(ltr, finalUnits) 115 | } 116 | 117 | // ConvertArea converts an area to the requested unit 118 | func ConvertArea(area float64, originalUnits string, finalUnits string) (float64, error) { 119 | if originalUnits == "" { 120 | originalUnits = constants.UnitMeters 121 | } 122 | 123 | if finalUnits == "" { 124 | finalUnits = constants.UnitKilometers 125 | } 126 | if area < 0 { 127 | return 0.0, errors.New("area must be a positive number") 128 | } 129 | 130 | if !validateAreaUnit(originalUnits) { 131 | return 0.0, errors.New("invalid original units") 132 | } 133 | 134 | if !validateAreaUnit(finalUnits) { 135 | return 0.0, errors.New("invalid finalUnits units") 136 | } 137 | startFactor := areaFactors[originalUnits] 138 | finalFactor := areaFactors[finalUnits] 139 | return (area / startFactor) * finalFactor, nil 140 | } 141 | 142 | func validateAreaUnit(units string) bool { 143 | _, ok := areaFactors[units] 144 | return ok 145 | } 146 | 147 | func validateUnit(units string) bool { 148 | _, ok := factors[units] 149 | return ok 150 | } 151 | -------------------------------------------------------------------------------- /conversions/conversions_test.go: -------------------------------------------------------------------------------- 1 | package conversions 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/tomchavakis/turf-go/assert" 8 | "github.com/tomchavakis/turf-go/constants" 9 | ) 10 | 11 | func TestRadiansToDistance(t *testing.T) { 12 | rtl, err := RadiansToLength(1, constants.UnitRadians) 13 | if err != nil { 14 | t.Errorf("RadiansToLength error: %v", err) 15 | } 16 | 17 | assert.Equal(t, rtl, 1.0) 18 | 19 | rtl, err = RadiansToLength(1, constants.UnitKilometers) 20 | if err != nil { 21 | t.Errorf("RadiansToLength error: %v", err) 22 | } 23 | 24 | assert.Equal(t, rtl, constants.EarthRadius/1000.0) 25 | 26 | rtl, err = RadiansToLength(1, constants.UnitMiles) 27 | if err != nil { 28 | t.Errorf("RadiansToLength error: %v", err) 29 | } 30 | 31 | assert.Equal(t, rtl, constants.EarthRadius/1609.344) 32 | } 33 | 34 | func TestDistanceToRadians(t *testing.T) { 35 | rtl, err := LengthToRadians(1.0, constants.UnitRadians) 36 | if err != nil { 37 | t.Errorf("RadiansToLength error: %v", err) 38 | } 39 | 40 | assert.Equal(t, rtl, 1.0) 41 | 42 | rtl, err = LengthToRadians(constants.EarthRadius/1000, constants.UnitKilometers) 43 | if err != nil { 44 | t.Errorf("RadiansToLength error: %v", err) 45 | } 46 | 47 | assert.Equal(t, rtl, 1.0) 48 | 49 | rtl, err = LengthToRadians(constants.EarthRadius/1609.344, constants.UnitMiles) 50 | if err != nil { 51 | t.Errorf("RadiansToLength error: %v", err) 52 | } 53 | 54 | assert.Equal(t, rtl, 1.0) 55 | } 56 | 57 | func TestDistanceToDegrees(t *testing.T) { 58 | rtl, err := LengthToDegrees(1.0, constants.UnitRadians) 59 | if err != nil { 60 | t.Errorf("RadiansToLength error: %v", err) 61 | } 62 | 63 | assert.Equal(t, rtl, 57.29577951308232) 64 | 65 | rtl, err = LengthToDegrees(100.0, constants.UnitKilometers) 66 | if err != nil { 67 | t.Errorf("RadiansToLength error: %v", err) 68 | } 69 | 70 | assert.Equal(t, rtl, 0.899320363724538) 71 | 72 | rtl, err = LengthToDegrees(10.0, constants.UnitMiles) 73 | if err != nil { 74 | t.Errorf("RadiansToLength error: %v", err) 75 | } 76 | 77 | assert.Equal(t, rtl, 0.14473158314379025) 78 | } 79 | 80 | func TestConvertLength(t *testing.T) { 81 | rtl, err := ConvertLength(1000.0, constants.UnitMeters, "") 82 | if err != nil { 83 | t.Errorf("RadiansToLength error: %v", err) 84 | } 85 | 86 | assert.Equal(t, rtl, 1.0) 87 | 88 | rtl, err = ConvertLength(1.0, constants.UnitKilometers, constants.UnitMiles) 89 | if err != nil { 90 | t.Errorf("RadiansToLength error: %v", err) 91 | } 92 | 93 | assert.Equal(t, rtl, 0.6213711922373341) 94 | 95 | rtl, err = ConvertLength(1.0, constants.UnitMiles, constants.UnitKilometers) 96 | if err != nil { 97 | t.Errorf("RadiansToLength error: %v", err) 98 | } 99 | 100 | assert.Equal(t, rtl, 1.6093439999999997) 101 | 102 | rtl, err = ConvertLength(1.0, constants.UnitNauticalMiles, "") 103 | if err != nil { 104 | t.Errorf("RadiansToLength error: %v", err) 105 | } 106 | 107 | assert.Equal(t, rtl, 1.8519999999999999) 108 | 109 | rtl, err = ConvertLength(1.0, constants.UnitMeters, constants.UnitCentimeters) 110 | if err != nil { 111 | t.Errorf("RadiansToLength error: %v", err) 112 | } 113 | 114 | assert.Equal(t, rtl, 100.00000000000001) 115 | 116 | } 117 | 118 | func TestConvertArea(t *testing.T) { 119 | a, err := ConvertArea(1000.0, constants.UnitMetres, constants.UnitKilometers) 120 | assert.Equal(t, err, nil) 121 | assert.Equal(t, a, 0.001) 122 | 123 | a2, err := ConvertArea(1000.0, constants.UnitMeters, constants.UnitKilometers) 124 | assert.Equal(t, err, nil) 125 | assert.Equal(t, a2, 0.001) 126 | 127 | b, err := ConvertArea(1, constants.UnitKilometers, constants.UnitMiles) 128 | assert.Equal(t, err, nil) 129 | assert.Equal(t, b, 0.386) 130 | 131 | c, err := ConvertArea(1, constants.UnitMiles, constants.UnitKilometers) 132 | assert.Equal(t, err, nil) 133 | assert.Equal(t, c, 2.5906735751295336) 134 | 135 | d, err := ConvertArea(1, constants.UnitMeters, constants.UnitCentimeters) 136 | assert.Equal(t, err, nil) 137 | assert.Equal(t, d, 10000.0) 138 | 139 | d2, err := ConvertArea(1, constants.UnitMeters, constants.UnitCentimetres) 140 | assert.Equal(t, err, nil) 141 | assert.Equal(t, d2, 10000.0) 142 | 143 | d3, err := ConvertArea(1, constants.UnitMetres, constants.UnitCentimetres) 144 | assert.Equal(t, err, nil) 145 | assert.Equal(t, d3, 10000.0) 146 | 147 | d4, err := ConvertArea(1, constants.UnitMetres, constants.UnitCentimeters) 148 | assert.Equal(t, err, nil) 149 | assert.Equal(t, d4, 10000.0) 150 | 151 | f, err := ConvertArea(100, constants.UnitMeters, constants.UnitAcres) 152 | assert.Equal(t, err, nil) 153 | assert.Equal(t, f, 0.0247105) 154 | 155 | f2, err := ConvertArea(100, constants.UnitMetres, constants.UnitAcres) 156 | assert.Equal(t, err, nil) 157 | assert.Equal(t, f2, 0.0247105) 158 | 159 | g, err := ConvertArea(100, "", constants.UnitYards) 160 | assert.Equal(t, err, nil) 161 | assert.Equal(t, g, 119.59900459999999) 162 | 163 | h, err := ConvertArea(100, constants.UnitMeters, constants.UnitFeet) 164 | assert.Equal(t, err, nil) 165 | assert.Equal(t, h, 1076.3910417) 166 | 167 | h2, err := ConvertArea(100, constants.UnitMetres, constants.UnitFeet) 168 | assert.Equal(t, err, nil) 169 | assert.Equal(t, h2, 1076.3910417) 170 | 171 | i, err := ConvertArea(100000, constants.UnitFeet, "") 172 | assert.Equal(t, err, nil) 173 | assert.Equal(t, i, 0.009290303999749462) 174 | 175 | j, err := ConvertArea(1, constants.UnitMeters, constants.UnitHectares) 176 | assert.Equal(t, err, nil) 177 | assert.Equal(t, j, 0.0001) 178 | 179 | j2, err := ConvertArea(1, constants.UnitMetres, constants.UnitHectares) 180 | assert.Equal(t, err, nil) 181 | assert.Equal(t, j2, 0.0001) 182 | 183 | _, err = ConvertArea(-1, constants.UnitMeters, constants.UnitMillimeters) 184 | assert.Equal(t, err.Error(), "area must be a positive number") 185 | 186 | _, err = ConvertArea(-1, constants.UnitMetres, constants.UnitMillimeters) 187 | assert.Equal(t, err.Error(), "area must be a positive number") 188 | 189 | _, err = ConvertArea(1, "foo", "bar") 190 | assert.Equal(t, err.Error(), "invalid original units") 191 | 192 | _, err = ConvertArea(1, constants.UnitMeters, "bar") 193 | assert.Equal(t, err.Error(), "invalid finalUnits units") 194 | } 195 | 196 | func TestDegreesToRadians(t *testing.T) { 197 | r := DegreesToRadians(180) 198 | 199 | if r != math.Pi { 200 | t.Errorf("degrees to radians = %f; want %f", r, math.Pi) 201 | } 202 | } 203 | 204 | func TestRadiansToDegrees(t *testing.T) { 205 | r := RadiansToDegrees(math.Pi) 206 | 207 | if r != float64(180) { 208 | t.Error("error converting radians to degrees") 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /docs.md: -------------------------------------------------------------------------------- 1 | Testing using VS Code 2 | 3 | ``` 4 | go test -v conversions_test.go conversions.go 5 | go test -v measurement_test.go measurement.go 6 | go test -v measurement_test.go measurement.go conversions.go constants.go point.go 7 | ``` -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tomchavakis/turf-go 2 | 3 | go 1.19 4 | 5 | require github.com/tomchavakis/geojson v0.0.5 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/tomchavakis/geojson v0.0.5 h1:4A2lVdwhKurcjxhozIkXIZLzQl4Cdhh4zyIBtvcW6oE= 2 | github.com/tomchavakis/geojson v0.0.5/go.mod h1:cV7rPpWhNXW+WnHlIo3od3KKfiekyVqe0gG2N+krlB4= 3 | -------------------------------------------------------------------------------- /internal/common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | // Float64Ptr returns the pointer to a float64. 4 | func Float64Ptr(v float64) *float64 { 5 | return &v 6 | } 7 | 8 | // Int64Ptr returns the pointer to an int64. 9 | func Int64Ptr(v int64) *int64 { 10 | return &v 11 | } 12 | 13 | // Int32Ptr returns the pointer to an int32. 14 | func Int32Ptr(v int32) *int32 { 15 | return &v 16 | } 17 | 18 | // IntPtr returns the pointer to an int 19 | func IntPtr(v int) *int { 20 | return &v 21 | } 22 | 23 | // StringPtr returns the pointer to a string. 24 | func StringPtr(v string) *string { 25 | return &v 26 | } 27 | 28 | // BoolPtr returns the pointer to a string. 29 | func BoolPtr(v bool) *bool { 30 | return &v 31 | } 32 | -------------------------------------------------------------------------------- /invariant/invariant.go: -------------------------------------------------------------------------------- 1 | package invariant 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/tomchavakis/geojson" 7 | "github.com/tomchavakis/geojson/feature" 8 | "github.com/tomchavakis/geojson/geometry" 9 | "github.com/tomchavakis/turf-go/utils" 10 | ) 11 | 12 | // GetCoord unwrap a coordinate from a point feature, geometry or a single coordinate. 13 | // coord is a GeoJSON point or an Array of numbers. 14 | // 15 | // Examples: 16 | // 17 | // Point Example: 18 | // 19 | // p := &geometry.Point{ 20 | // Lat: 34.6, 21 | // Lng: 23.5, 22 | // } 23 | // coord, err := GetCoord(p) 24 | // 25 | // Array Example 26 | // 27 | // p := []float64{ 28 | // 23.5, 29 | // 34.6, 30 | // } 31 | // coord, err := GetCoord(p) 32 | // 33 | // Feature Example 34 | // 35 | // p := &feature.Feature{ 36 | // Type: "Feature", 37 | // Properties: map[string]interface{}{}, 38 | // Bbox: []float64{}, 39 | // Geometry: geometry.Geometry{ 40 | // GeoJSONType: "Point", 41 | // Coordinates: []float64{44.34, 23.52}, 42 | // }, 43 | // } 44 | // coord, err := GetCoord(p) 45 | func GetCoord(coord interface{}) ([]float64, error) { 46 | if coord == nil { 47 | return nil, errors.New("coord is required") 48 | } 49 | result := []float64{} 50 | switch gtp := coord.(type) { 51 | case *feature.Feature: 52 | if gtp.Type == geojson.Feature && gtp.Geometry.GeoJSONType == "Point" { 53 | p, err := gtp.Geometry.ToPoint() 54 | if err != nil { 55 | return nil, err 56 | } 57 | result = append(result, p.Lng, p.Lat) 58 | return result, nil 59 | } 60 | case *geometry.Point: 61 | result = append(result, gtp.Lng, gtp.Lat) 62 | return result, nil 63 | case []float64: 64 | if utils.IsArray(gtp) && len(gtp) >= 2 && !utils.IsArray(gtp[0]) && !utils.IsArray(gtp[1]) { 65 | return gtp, nil 66 | } 67 | } 68 | 69 | return nil, errors.New("coord must be GeoJSON Point or an Array of numbers") 70 | } 71 | 72 | // GetCoords unwrap coordinates from a Feature, Geometry, Object or an Array 73 | // 74 | // Example: 75 | // 76 | // coords: &geometry.Point{ 77 | // Lat: 23.52, 78 | // Lng: 44.34, 79 | // }, 80 | // 81 | // coords,err := GetCoords(coords) 82 | // = []float64{44.34, 23.52} 83 | func GetCoords(coords interface{}) (interface{}, error) { 84 | if coords == nil { 85 | return nil, errors.New("coord is required") 86 | } 87 | switch gtp := coords.(type) { 88 | case *feature.Feature: // Feature 89 | if gtp.Type == geojson.Feature { 90 | switch gtp.Geometry.GeoJSONType { 91 | case geojson.Point: 92 | _, err := gtp.Geometry.ToPoint() 93 | if err != nil { 94 | return nil, err 95 | } 96 | return gtp.Geometry.Coordinates, nil 97 | case geojson.MultiPoint: 98 | _, err := gtp.Geometry.ToMultiPoint() 99 | if err != nil { 100 | return nil, err 101 | } 102 | return gtp.Geometry.Coordinates, nil 103 | case geojson.LineString: 104 | _, err := gtp.Geometry.ToLineString() 105 | if err != nil { 106 | return nil, err 107 | } 108 | return gtp.Geometry.Coordinates, nil 109 | case geojson.Polygon: 110 | _, err := gtp.Geometry.ToPolygon() 111 | if err != nil { 112 | return nil, err 113 | } 114 | return gtp.Geometry.Coordinates, nil 115 | case geojson.MultiLineString: 116 | _, err := gtp.Geometry.ToMultiLineString() 117 | if err != nil { 118 | return nil, err 119 | } 120 | return gtp.Geometry.Coordinates, nil 121 | case geojson.MultiPolygon: 122 | _, err := gtp.Geometry.ToMultiPolygon() 123 | if err != nil { 124 | return nil, err 125 | } 126 | return gtp.Geometry.Coordinates, nil 127 | } 128 | } 129 | // Geometry 130 | case *geometry.Polygon: 131 | result := [][][]float64{} 132 | for i := 0; i < len(gtp.Coordinates); i++ { 133 | coords := [][]float64{} 134 | for j := 0; j < len(gtp.Coordinates[i].Coordinates); j++ { 135 | coords = append(coords, []float64{gtp.Coordinates[i].Coordinates[j].Lng, gtp.Coordinates[i].Coordinates[j].Lat}) 136 | } 137 | result = append(result, coords) 138 | } 139 | return result, nil 140 | case *geometry.LineString: 141 | result := [][]float64{} 142 | for i := 0; i < len(gtp.Coordinates); i++ { 143 | result = append(result, []float64{gtp.Coordinates[i].Lng, gtp.Coordinates[i].Lat}) 144 | } 145 | return result, nil 146 | case *geometry.MultiLineString: 147 | result := [][][]float64{} 148 | for i := 0; i < len(gtp.Coordinates); i++ { 149 | tmp := [][]float64{} 150 | for j := 0; j < len(gtp.Coordinates[i].Coordinates); j++ { 151 | tmp = append(tmp, []float64{gtp.Coordinates[i].Coordinates[j].Lng, gtp.Coordinates[i].Coordinates[j].Lat}) 152 | } 153 | result = append(result, tmp) 154 | } 155 | return result, nil 156 | case *geometry.Point: 157 | result := []float64{} 158 | result = append(result, gtp.Lng, gtp.Lat) 159 | return result, nil 160 | case *geometry.MultiPoint: 161 | result := [][]float64{} 162 | for i := 0; i < len(gtp.Coordinates); i++ { 163 | tmp := []float64{} 164 | tmp = append(tmp, gtp.Coordinates[i].Lng, gtp.Coordinates[i].Lat) 165 | result = append(result, tmp) 166 | } 167 | return result, nil 168 | case *geometry.MultiPolygon: 169 | result := [][][][]float64{} 170 | for i := 0; i < len(gtp.Coordinates); i++ { 171 | tmp := [][][]float64{} 172 | for j := 0; j < len(gtp.Coordinates[i].Coordinates); j++ { 173 | tmPoly := [][]float64{} 174 | for k := 0; k < len(gtp.Coordinates[i].Coordinates[j].Coordinates); k++ { 175 | tmPoly = append(tmPoly, []float64{gtp.Coordinates[i].Coordinates[j].Coordinates[k].Lng, gtp.Coordinates[i].Coordinates[j].Coordinates[k].Lat}) 176 | } 177 | tmp = append(tmp, tmPoly) 178 | } 179 | result = append(result, tmp) 180 | } 181 | return result, nil 182 | case []float64: 183 | if utils.IsArray(gtp) && len(gtp) >= 2 { 184 | return gtp, nil 185 | } 186 | } 187 | return nil, errors.New("coord must be GeoJSON Point or an Array of numbers") 188 | } 189 | 190 | // GetType returns the GeoJSON object's type 191 | // 192 | // Examples: 193 | // 194 | // fp, err := feature.FromJSON("{ \"type\": \"Feature\", \"properties\": {}, \"geometry\": { \"type\": \"Point\", \"coordinates\": [102, 0.5] } }") 195 | // result := GetType(fp) 196 | // ="Point" 197 | func GetType(geojson interface{}) string { 198 | switch gtp := geojson.(type) { 199 | case *feature.Feature: 200 | return string(gtp.Geometry.GeoJSONType) 201 | case *feature.Collection: 202 | return string(gtp.Type) 203 | case *geometry.Collection: 204 | return string(gtp.Type) 205 | case *geometry.Geometry: 206 | return string(gtp.GeoJSONType) 207 | } 208 | return "invalid" 209 | } 210 | 211 | 212 | // GetGeom gets the geometry from Feature or Geometry Object 213 | // 214 | // Examples: 215 | // 216 | // fp, err := feature.FromJSON("{ \"type\": \"Feature\", \"properties\": {}, \"geometry\": { \"type\": \"Point\", \"coordinates\": [102, 0.5] } }") 217 | // result := GetGeom(fp) 218 | // = {"geometry\": { \"type\": \"Point\", \"coordinates\": [102, 0.5] } 219 | func GetGeom(geojson interface{}) (*geometry.Geometry, error) { 220 | switch gtp := geojson.(type) { 221 | case *feature.Feature: 222 | return >p.Geometry, nil 223 | case *geometry.Geometry: 224 | return gtp, nil 225 | } 226 | return nil, errors.New("invalid type") 227 | } -------------------------------------------------------------------------------- /invariant/invariant_test.go: -------------------------------------------------------------------------------- 1 | package invariant 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/tomchavakis/geojson" 9 | "github.com/tomchavakis/geojson/feature" 10 | "github.com/tomchavakis/geojson/geometry" 11 | ) 12 | 13 | func TestGetGeom(t *testing.T){ 14 | type args struct { 15 | coords interface{} 16 | } 17 | tests := map[string]struct { 18 | args args 19 | want *geometry.Geometry 20 | wantErr bool 21 | err error 22 | }{ 23 | "error - point": { 24 | args: args{ 25 | coords: &geometry.Point{ 26 | Lat: 23.52, 27 | Lng: 44.34, 28 | }, 29 | }, 30 | wantErr: true, 31 | err: errors.New("invalid type"), 32 | }, 33 | "geometry - point": { 34 | args: args{ 35 | coords: &geometry.Geometry{ 36 | GeoJSONType: geojson.Point, 37 | Coordinates: []float64{44.34, 23.52}, 38 | }, 39 | }, 40 | wantErr: false, 41 | want: &geometry.Geometry{ 42 | GeoJSONType: geojson.Point, 43 | Coordinates: []float64{44.34, 23.52}, 44 | }, 45 | }, 46 | "feature - point": { 47 | args: args{ 48 | coords: &feature.Feature{ 49 | ID: "", 50 | Type: geojson.Feature, 51 | Properties: map[string]interface{}{}, 52 | Bbox: []float64{}, 53 | Geometry: geometry.Geometry{ 54 | GeoJSONType: geojson.Polygon, 55 | Coordinates: [][][]float64{ 56 | {{2, 1}, {4, 3}}, {{6, 5}, {8, 7}}, 57 | }, 58 | }, 59 | }, 60 | }, 61 | wantErr: false, 62 | want: &geometry.Geometry{ 63 | GeoJSONType: geojson.Polygon, 64 | Coordinates: [][][]float64{ 65 | {{2, 1}, {4, 3}}, {{6, 5}, {8, 7}}, 66 | }, 67 | }, 68 | }, 69 | } 70 | for name, tt := range tests { 71 | t.Run(name, func(t *testing.T){ 72 | geom,err := GetGeom(tt.args.coords) 73 | if (err != nil) && tt.wantErr { 74 | if err.Error() != tt.err.Error() { 75 | t.Errorf("TestGetGeom() error = %v, wantErr %v", err.Error(), tt.err.Error()) 76 | return 77 | } 78 | } 79 | 80 | if got := geom; !reflect.DeepEqual(got, tt.want) { 81 | t.Errorf("TestGetGeom() = %v, want %v", got, tt.want) 82 | } 83 | }) 84 | } 85 | } 86 | 87 | func TestGetCoord(t *testing.T) { 88 | type args struct { 89 | coords interface{} 90 | } 91 | tests := map[string]struct { 92 | args args 93 | want []float64 94 | wantErr bool 95 | err error 96 | }{ 97 | "error - required coords": { 98 | args: args{ 99 | coords: nil, 100 | }, 101 | wantErr: true, 102 | want: nil, 103 | err: errors.New("coord is required"), 104 | }, 105 | "error - polygon interface": { 106 | args: args{ 107 | coords: feature.Feature{ 108 | ID: "", 109 | Type: "Feature", 110 | Properties: map[string]interface{}{}, 111 | Bbox: []float64{}, 112 | Geometry: geometry.Geometry{ 113 | GeoJSONType: "Polygon", 114 | Coordinates: nil, 115 | }, 116 | }, 117 | }, 118 | wantErr: true, 119 | want: nil, 120 | err: errors.New("coord must be GeoJSON Point or an Array of numbers"), 121 | }, 122 | "error array - single array ": { 123 | args: args{ 124 | []float64{44.34}, 125 | }, 126 | wantErr: true, 127 | want: nil, 128 | err: errors.New("coord must be GeoJSON Point or an Array of numbers"), 129 | }, 130 | "feature - point": { 131 | args: args{ 132 | coords: &feature.Feature{ 133 | ID: "", 134 | Type: "Feature", 135 | Properties: map[string]interface{}{}, 136 | Bbox: []float64{}, 137 | Geometry: geometry.Geometry{ 138 | GeoJSONType: "Point", 139 | Coordinates: []float64{44.34, 23.52}, 140 | }, 141 | }, 142 | }, 143 | wantErr: false, 144 | want: []float64{44.34, 23.52}, 145 | err: nil, 146 | }, 147 | "geometry - point": { 148 | args: args{ 149 | coords: &geometry.Point{ 150 | Lat: 23.52, 151 | Lng: 44.34, 152 | }, 153 | }, 154 | wantErr: false, 155 | want: []float64{44.34, 23.52}, 156 | err: nil, 157 | }, 158 | "array - point": { 159 | args: args{ 160 | []float64{44.34, 23.52}, 161 | }, 162 | wantErr: false, 163 | want: []float64{44.34, 23.52}, 164 | err: nil, 165 | }, 166 | } 167 | for name, tt := range tests { 168 | t.Run(name, func(t *testing.T) { 169 | geo, err := GetCoord(tt.args.coords) 170 | 171 | if (err != nil) && tt.wantErr { 172 | if err.Error() != tt.err.Error() { 173 | t.Errorf("TestGetCoord() error = %v, wantErr %v", err, tt.err.Error()) 174 | return 175 | } 176 | } 177 | 178 | if got := geo; !reflect.DeepEqual(got, tt.want) { 179 | t.Errorf("TestGetCoord() = %v, want %v", got, tt.want) 180 | } 181 | }) 182 | } 183 | } 184 | 185 | func TestGetCoords(t *testing.T) { 186 | type args struct { 187 | coords interface{} 188 | } 189 | tests := map[string]struct { 190 | args args 191 | want interface{} 192 | wantErr bool 193 | err error 194 | }{ 195 | "error - required coords": { 196 | args: args{ 197 | coords: nil, 198 | }, 199 | wantErr: true, 200 | want: nil, 201 | err: errors.New("coord is required"), 202 | }, 203 | "error array - single array ": { 204 | args: args{ 205 | []float64{44.34}, 206 | }, 207 | wantErr: true, 208 | want: nil, 209 | err: errors.New("coord must be GeoJSON Point or an Array of numbers"), 210 | }, 211 | "error feature - polygon": { 212 | args: args{ 213 | coords: &feature.Feature{ 214 | ID: "", 215 | Type: geojson.Feature, 216 | Properties: map[string]interface{}{}, 217 | Bbox: []float64{}, 218 | Geometry: geometry.Geometry{ 219 | GeoJSONType: geojson.Polygon, 220 | Coordinates: [][][]float64{ 221 | {{2, 1}, {4, 3}}, {{6, 5}, {8, 7}}, 222 | }, 223 | }, 224 | }, 225 | }, 226 | wantErr: true, 227 | want: nil, 228 | err: errors.New("cannot create a new polygon a polygon must have at least 4 positions"), 229 | }, 230 | "error - feature - point": { 231 | args: args{ 232 | coords: &feature.Feature{ 233 | ID: "", 234 | Type: geojson.Feature, 235 | Properties: map[string]interface{}{}, 236 | Bbox: []float64{}, 237 | Geometry: geometry.Geometry{ 238 | GeoJSONType: geojson.Point, 239 | Coordinates: [][]float64{{23.33, 33, 33}}, 240 | }, 241 | }, 242 | }, 243 | wantErr: true, 244 | want: nil, 245 | err: errors.New("cannot unmarshal object"), 246 | }, 247 | "error - feature - multiPoint": { 248 | args: args{ 249 | coords: &feature.Feature{ 250 | ID: "", 251 | Type: geojson.Feature, 252 | Properties: map[string]interface{}{}, 253 | Bbox: []float64{}, 254 | Geometry: geometry.Geometry{ 255 | GeoJSONType: geojson.MultiPoint, 256 | Coordinates: []float64{ 257 | 102, 258 | }, 259 | }, 260 | }, 261 | }, 262 | wantErr: true, 263 | err: errors.New("cannot unmarshal object"), 264 | want: nil, 265 | }, 266 | "error - feature - lineString": { 267 | args: args{ 268 | coords: &feature.Feature{ 269 | ID: "", 270 | Type: geojson.Feature, 271 | Properties: map[string]interface{}{}, 272 | Bbox: []float64{}, 273 | Geometry: geometry.Geometry{ 274 | GeoJSONType: geojson.LineString, 275 | Coordinates: []float64{ 276 | 44.34, 23.52, 277 | }, 278 | }, 279 | }, 280 | }, 281 | wantErr: true, 282 | want: nil, 283 | err: errors.New("cannot marshal object"), 284 | }, 285 | "error - feature - multiLineString": { 286 | args: args{ 287 | coords: &feature.Feature{ 288 | ID: "", 289 | Type: geojson.Feature, 290 | Properties: map[string]interface{}{}, 291 | Bbox: []float64{}, 292 | Geometry: geometry.Geometry{ 293 | GeoJSONType: geojson.MultiLineString, 294 | Coordinates: [][][]float64{ 295 | {{44.34, 23.52}, {33.33, 44.44}}, 296 | }, 297 | }, 298 | }, 299 | }, 300 | wantErr: true, 301 | want: nil, 302 | err: errors.New("can't create a new multiLineString"), 303 | }, 304 | "error feature - multiPolygon": { 305 | args: args{ 306 | coords: &feature.Feature{ 307 | ID: "", 308 | Type: geojson.Feature, 309 | Properties: map[string]interface{}{}, 310 | Bbox: []float64{}, 311 | Geometry: geometry.Geometry{ 312 | GeoJSONType: geojson.MultiPolygon, 313 | Coordinates: [][][]float64{ 314 | { 315 | {44.34}, {23.52}, {33.33}, {44.44}, 316 | }, 317 | }, 318 | }, 319 | }, 320 | }, 321 | wantErr: true, 322 | want: nil, 323 | err: errors.New("cannot marshal object"), 324 | }, 325 | "feature - point": { 326 | args: args{ 327 | coords: &feature.Feature{ 328 | ID: "", 329 | Type: geojson.Feature, 330 | Properties: map[string]interface{}{}, 331 | Bbox: []float64{}, 332 | Geometry: geometry.Geometry{ 333 | GeoJSONType: geojson.Point, 334 | Coordinates: []float64{44.34, 23.52}, 335 | }, 336 | }, 337 | }, 338 | wantErr: false, 339 | want: []float64{44.34, 23.52}, 340 | err: nil, 341 | }, 342 | "feature - multiPoint": { 343 | args: args{ 344 | coords: &feature.Feature{ 345 | ID: "", 346 | Type: geojson.Feature, 347 | Properties: map[string]interface{}{}, 348 | Bbox: []float64{}, 349 | Geometry: geometry.Geometry{ 350 | GeoJSONType: geojson.MultiPoint, 351 | Coordinates: [][]float64{ 352 | {102, -10}, 353 | {103, 1}, 354 | {104, 0}, 355 | {130, 4}, 356 | }, 357 | }, 358 | }, 359 | }, 360 | wantErr: false, 361 | err: nil, 362 | want: [][]float64{ 363 | {102, -10}, 364 | {103, 1}, 365 | {104, 0}, 366 | {130, 4}, 367 | }, 368 | }, 369 | "feature - lineString": { 370 | args: args{ 371 | coords: &feature.Feature{ 372 | ID: "", 373 | Type: geojson.Feature, 374 | Properties: map[string]interface{}{}, 375 | Bbox: []float64{}, 376 | Geometry: geometry.Geometry{ 377 | GeoJSONType: geojson.LineString, 378 | Coordinates: [][]float64{ 379 | {44.34, 23.52}, {33.33, 44.44}, 380 | }, 381 | }, 382 | }, 383 | }, 384 | wantErr: false, 385 | want: [][]float64{ 386 | {44.34, 23.52}, {33.33, 44.44}, 387 | }, 388 | err: nil, 389 | }, 390 | "feature - multiLineString": { 391 | args: args{ 392 | coords: &feature.Feature{ 393 | ID: "", 394 | Type: geojson.Feature, 395 | Properties: map[string]interface{}{}, 396 | Bbox: []float64{}, 397 | Geometry: geometry.Geometry{ 398 | GeoJSONType: geojson.MultiLineString, 399 | Coordinates: [][][]float64{ 400 | {{44.34, 23.52}, {33.33, 44.44}}, 401 | {{45.34, 23.52}, {35.33, 46.44}}, 402 | }, 403 | }, 404 | }, 405 | }, 406 | wantErr: false, 407 | want: [][][]float64{ 408 | {{44.34, 23.52}, {33.33, 44.44}}, 409 | {{45.34, 23.52}, {35.33, 46.44}}, 410 | }, 411 | err: nil, 412 | }, 413 | "feature - multiPolygon": { 414 | args: args{ 415 | coords: &feature.Feature{ 416 | ID: "", 417 | Type: geojson.Feature, 418 | Properties: map[string]interface{}{}, 419 | Bbox: []float64{}, 420 | Geometry: geometry.Geometry{ 421 | GeoJSONType: geojson.MultiPolygon, 422 | Coordinates: [][][][]float64{ 423 | { 424 | { 425 | {44.34, 23.52}, {33.33, 44.44}, 426 | }, 427 | { 428 | {45.34, 23.52}, {33.33, 44.44}, 429 | }, 430 | { 431 | {46.34, 23.52}, {33.33, 44.44}, 432 | }, 433 | { 434 | {47.34, 23.52}, {33.33, 44.44}, 435 | }, 436 | }, 437 | { 438 | { 439 | {48.34, 23.52}, {34.33, 44.44}, 440 | }, 441 | { 442 | {49.34, 23.52}, {35.33, 44.44}, 443 | }, 444 | { 445 | {50.34, 23.52}, {36.33, 44.44}, 446 | }, 447 | { 448 | {51.34, 23.52}, {37.33, 44.44}, 449 | }, 450 | }, 451 | }, 452 | }, 453 | }, 454 | }, 455 | wantErr: false, 456 | want: [][][][]float64{ 457 | { 458 | { 459 | {44.34, 23.52}, {33.33, 44.44}, 460 | }, 461 | { 462 | {45.34, 23.52}, {33.33, 44.44}, 463 | }, 464 | { 465 | {46.34, 23.52}, {33.33, 44.44}, 466 | }, 467 | { 468 | {47.34, 23.52}, {33.33, 44.44}, 469 | }, 470 | }, 471 | { 472 | { 473 | {48.34, 23.52}, {34.33, 44.44}, 474 | }, 475 | { 476 | {49.34, 23.52}, {35.33, 44.44}, 477 | }, 478 | { 479 | {50.34, 23.52}, {36.33, 44.44}, 480 | }, 481 | { 482 | {51.34, 23.52}, {37.33, 44.44}, 483 | }, 484 | }, 485 | }, 486 | err: nil, 487 | }, 488 | "feature - polygon": { 489 | args: args{ 490 | coords: &feature.Feature{ 491 | ID: "", 492 | Type: geojson.Feature, 493 | Properties: map[string]interface{}{}, 494 | Bbox: []float64{}, 495 | Geometry: geometry.Geometry{ 496 | GeoJSONType: geojson.Polygon, 497 | Coordinates: [][][]float64{ 498 | { 499 | {101, 0}, 500 | {101, 1}, 501 | {100, 1}, 502 | {100, 0}, 503 | {101, 0}, 504 | }, 505 | }, 506 | }, 507 | }, 508 | }, 509 | wantErr: false, 510 | want: [][][]float64{ 511 | { 512 | {101, 0}, 513 | {101, 1}, 514 | {100, 1}, 515 | {100, 0}, 516 | {101, 0}, 517 | }, 518 | }, 519 | err: nil, 520 | }, 521 | "geometry - polygon": { 522 | args: args{ 523 | coords: &geometry.Polygon{ 524 | Coordinates: []geometry.LineString{ 525 | { 526 | Coordinates: []geometry.Point{ 527 | { 528 | Lat: 1.0, 529 | Lng: 2.0, 530 | }, 531 | { 532 | Lat: 3.0, 533 | Lng: 4.0, 534 | }, 535 | }, 536 | }, 537 | { 538 | Coordinates: []geometry.Point{ 539 | { 540 | Lat: 5.0, 541 | Lng: 6.0, 542 | }, 543 | { 544 | Lat: 7.0, 545 | Lng: 8.0, 546 | }, 547 | }, 548 | }, 549 | }, 550 | }, 551 | }, 552 | wantErr: false, 553 | want: [][][]float64{ 554 | {{2, 1}, {4, 3}}, {{6, 5}, {8, 7}}, 555 | }, 556 | err: nil, 557 | }, 558 | "geometry - lineString": { 559 | args: args{ 560 | coords: &geometry.LineString{ 561 | Coordinates: []geometry.Point{ 562 | { 563 | Lat: 1.0, 564 | Lng: 2.0, 565 | }, 566 | { 567 | Lat: 3.0, 568 | Lng: 4.0, 569 | }, 570 | }, 571 | }, 572 | }, 573 | wantErr: false, 574 | want: [][]float64{ 575 | {2, 1}, {4, 3}, 576 | }, 577 | err: nil, 578 | }, 579 | "geometry - point": { 580 | args: args{ 581 | coords: &geometry.Point{ 582 | Lat: 23.52, 583 | Lng: 44.34, 584 | }, 585 | }, 586 | wantErr: false, 587 | want: []float64{44.34, 23.52}, 588 | err: nil, 589 | }, 590 | "geometry - multiPoint": { 591 | args: args{ 592 | coords: &geometry.MultiPoint{ 593 | Coordinates: []geometry.Point{ 594 | { 595 | Lat: 23.44, 596 | Lng: 43.33, 597 | }, 598 | { 599 | Lat: 25.44, 600 | Lng: 44.33, 601 | }, 602 | { 603 | Lat: 26.46, 604 | Lng: 45.33, 605 | }, 606 | }, 607 | }, 608 | }, 609 | wantErr: false, 610 | err: nil, 611 | want: [][]float64{ 612 | {43.33, 23.44}, 613 | {44.33, 25.44}, 614 | {45.33, 26.46}, 615 | }, 616 | }, 617 | "geometry - multiLineString": { 618 | args: args{ 619 | coords: &geometry.MultiLineString{ 620 | Coordinates: []geometry.LineString{ 621 | { 622 | Coordinates: []geometry.Point{ 623 | { 624 | Lat: 23.44, 625 | Lng: 43.33, 626 | }, 627 | { 628 | Lat: 25.44, 629 | Lng: 44.33, 630 | }, 631 | { 632 | Lat: 26.46, 633 | Lng: 45.33, 634 | }, 635 | }, 636 | }, 637 | { 638 | Coordinates: []geometry.Point{ 639 | { 640 | Lat: 29.44, 641 | Lng: 48.33, 642 | }, 643 | { 644 | Lat: 36.46, 645 | Lng: 55.33, 646 | }, 647 | }, 648 | }, 649 | }, 650 | }, 651 | }, 652 | wantErr: false, 653 | err: nil, 654 | want: [][][]float64{ 655 | { 656 | {43.33, 23.44}, {44.33, 25.44}, {45.33, 26.46}, 657 | }, 658 | { 659 | {48.33, 29.44}, {55.33, 36.46}, 660 | }, 661 | }, 662 | }, 663 | "geometry - multiPolygon": { 664 | args: args{ 665 | coords: &geometry.MultiPolygon{ 666 | Coordinates: []geometry.Polygon{ 667 | { 668 | Coordinates: []geometry.LineString{ 669 | { 670 | Coordinates: []geometry.Point{ 671 | { 672 | Lat: 23.55, 673 | Lng: 43.66, 674 | }, 675 | { 676 | Lat: 24.55, 677 | Lng: 44.67, 678 | }, 679 | { 680 | Lat: 25.55, 681 | Lng: 45.68, 682 | }, 683 | }, 684 | }, 685 | { 686 | Coordinates: []geometry.Point{ 687 | { 688 | Lat: 25.55, 689 | Lng: 43.66, 690 | }, 691 | { 692 | Lat: 26.55, 693 | Lng: 44.67, 694 | }, 695 | { 696 | Lat: 27.55, 697 | Lng: 45.68, 698 | }, 699 | }, 700 | }, 701 | }, 702 | }, 703 | { 704 | Coordinates: []geometry.LineString{ 705 | { 706 | Coordinates: []geometry.Point{ 707 | { 708 | Lat: 30.55, 709 | Lng: 43.66, 710 | }, 711 | { 712 | Lat: 34.55, 713 | Lng: 44.67, 714 | }, 715 | { 716 | Lat: 35.55, 717 | Lng: 45.68, 718 | }, 719 | }, 720 | }, 721 | { 722 | Coordinates: []geometry.Point{ 723 | { 724 | Lat: 45.55, 725 | Lng: 43.66, 726 | }, 727 | { 728 | Lat: 46.55, 729 | Lng: 44.67, 730 | }, 731 | { 732 | Lat: 47.55, 733 | Lng: 45.68, 734 | }, 735 | }, 736 | }, 737 | }, 738 | }, 739 | }, 740 | }, 741 | }, 742 | wantErr: false, 743 | err: nil, 744 | want: [][][][]float64{ 745 | { 746 | { 747 | {43.66, 23.55}, {44.67, 24.55}, {45.68, 25.55}, 748 | }, 749 | { 750 | {43.66, 25.55}, {44.67, 26.55}, {45.68, 27.55}, 751 | }, 752 | }, 753 | { 754 | { 755 | {43.66, 30.55}, {44.67, 34.55}, {45.68, 35.55}, 756 | }, 757 | { 758 | {43.66, 45.55}, {44.67, 46.55}, {45.68, 47.55}, 759 | }, 760 | }, 761 | }, 762 | }, 763 | "array - point": { 764 | args: args{ 765 | []float64{44.34, 23.52}, 766 | }, 767 | wantErr: false, 768 | want: []float64{44.34, 23.52}, 769 | err: nil, 770 | }, 771 | } 772 | for name, tt := range tests { 773 | t.Run(name, func(t *testing.T) { 774 | geo, err := GetCoords(tt.args.coords) 775 | 776 | if (err != nil) && tt.wantErr { 777 | if err.Error() != tt.err.Error() { 778 | t.Errorf("TestGetCoords() error = %v, wantErr %v", err.Error(), tt.err.Error()) 779 | return 780 | } 781 | } 782 | 783 | if got := geo; !reflect.DeepEqual(got, tt.want) { 784 | t.Errorf("TestGetCoords() = %v, want %v", got, tt.want) 785 | } 786 | }) 787 | } 788 | } 789 | 790 | func TestGetType(t *testing.T) { 791 | type args struct { 792 | geojson interface{} 793 | } 794 | fp, _ := feature.FromJSON("{ \"type\": \"Feature\", \"properties\": {}, \"geometry\": { \"type\": \"Point\", \"coordinates\": [102, 0.5] } }") 795 | g, _ := geometry.FromJSON("{ \"type\": \"MultiPolygon\", \"coordinates\": [ [ [ [-116, -45], [-90, -45], [-90, -56], [-116, -56], [-116, -45] ] ], [ [ [-90.351563, 9.102097], [-77.695312, -3.513421], [-65.039063, 12.21118], [-65.742188, 21.616579], [-84.023437, 24.527135], [-90.351563, 9.102097] ] ] ] }") 796 | fcl, _ := feature.CollectionFromJSON("{ \"type\": \"FeatureCollection\", \"features\": [ { \"type\": \"Feature\", \"properties\": {}, \"geometry\": { \"type\": \"Polygon\", \"coordinates\": [ [ [-12913060.93202, 7967317.535016], [-10018754.171395, 7967317.535016], [-10018754.171395, 9876845.895795], [-12913060.93202, 9876845.895795], [-12913060.93202, 7967317.535016] ] ] } }, { \"type\": \"Feature\", \"properties\": {}, \"geometry\": { \"type\": \"LineString\", \"coordinates\": [ [-2269873.991957, 3991847.410438], [-391357.58482, 6026906.856034], [1565430.33928, 1917652.163291] ] } }, { \"type\": \"Feature\", \"properties\": {}, \"geometry\": { \"type\": \"Point\", \"coordinates\": [-8492459.534936, 430493.386177] } } ] }") 797 | gcl, _ := geometry.CollectionFromJSON("{ \"TYPE\": \"GeometryCollection\", \"geometries\": [ { \"TYPE\": \"Point\", \"coordinates\": [-71.0, 40.99999999999998] }, { \"TYPE\": \"LineString\", \"coordinates\": [ [-20.39062500000365, 33.72434000000235], [-3.5156249999990803, 47.51720099999992], [14.062499999996321, 16.97274100000141] ] } ] }") 798 | 799 | tests := map[string]struct { 800 | args args 801 | want string 802 | }{ 803 | "error - required coords": { 804 | args: args{ 805 | geojson: "", 806 | }, 807 | want: "invalid", 808 | }, 809 | "feature - point": { 810 | args: args{ 811 | geojson: fp, 812 | }, 813 | want: string(geojson.Point), 814 | }, 815 | "feature - collection": { 816 | args: args{ 817 | geojson: fcl, 818 | }, 819 | want: string(geojson.FeatureCollection), 820 | }, 821 | "geometry - collection": { 822 | args: args{ 823 | geojson: gcl, 824 | }, 825 | want: string(geojson.GeometryCollection), 826 | }, 827 | "geometry - multipolygon": { 828 | args: args{ 829 | geojson: g, 830 | }, 831 | want: string(geojson.MultiPolygon), 832 | }, 833 | } 834 | for name, tt := range tests { 835 | t.Run(name, func(t *testing.T) { 836 | geo := GetType(tt.args.geojson) 837 | 838 | if got := geo; !reflect.DeepEqual(got, tt.want) { 839 | t.Errorf("TestGetCoord() = %v, want %v", got, tt.want) 840 | } 841 | }) 842 | } 843 | } 844 | -------------------------------------------------------------------------------- /joins.go: -------------------------------------------------------------------------------- 1 | package turf 2 | 3 | import ( 4 | "github.com/tomchavakis/geojson" 5 | "github.com/tomchavakis/geojson/geometry" 6 | ) 7 | 8 | // PointInPolygon takes a Point and a Polygon and determines if the point resides inside the polygon 9 | func PointInPolygon(point geometry.Point, polygon geometry.Polygon) (bool, error) { 10 | 11 | pArr := []geometry.Polygon{} 12 | pArr = append(pArr, polygon) 13 | 14 | mp, err := geometry.NewMultiPolygon(pArr) 15 | if err != nil { 16 | return false, err 17 | } 18 | 19 | return PointInMultiPolygon(point, *mp), nil 20 | } 21 | 22 | // PointInMultiPolygon takes a Point and a MultiPolygon and determines if the point resides inside the polygon 23 | func PointInMultiPolygon(p geometry.Point, mp geometry.MultiPolygon) bool { 24 | 25 | insidePoly := false 26 | polys := mp.Coordinates 27 | 28 | for i := 0; i < len(polys) && !insidePoly; i++ { 29 | //check if it is in the outer ring first 30 | if inRing(p, polys[i].Coordinates[0].Coordinates) { 31 | inHole := false 32 | temp := 1 33 | // check for the point in any of the holes 34 | for temp < len(polys[i].Coordinates) && !inHole { 35 | if inRing(p, polys[i].Coordinates[temp].Coordinates) { 36 | inHole = true 37 | } 38 | temp++ 39 | } 40 | if !inHole { 41 | insidePoly = true 42 | } 43 | } 44 | } 45 | 46 | return insidePoly 47 | } 48 | 49 | // InBBOX returns true if the point is within the Bounding Box 50 | func InBBOX(pt geometry.Point, bbox geojson.BBOX) bool { 51 | return bbox.West <= pt.Lng && 52 | bbox.South <= pt.Lat && 53 | bbox.East >= pt.Lng && 54 | bbox.North >= pt.Lat 55 | } 56 | 57 | func inRing(pt geometry.Point, ring []geometry.Point) bool { 58 | 59 | isInside := false 60 | j := 0 61 | for i := 0; i < len(ring); i++ { 62 | 63 | xi := ring[i].Lng 64 | yi := ring[i].Lat 65 | xj := ring[j].Lng 66 | yj := ring[j].Lat 67 | 68 | intersect := (yi > pt.Lat) != (yj > pt.Lat) && (pt.Lng < (xj-xi)*(pt.Lat-yi)/(yj-yi)+xi) 69 | 70 | if intersect { 71 | isInside = !isInside 72 | } 73 | 74 | j = i 75 | } 76 | return isInside 77 | } 78 | -------------------------------------------------------------------------------- /joins_test.go: -------------------------------------------------------------------------------- 1 | package turf 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/tomchavakis/geojson" 8 | "github.com/tomchavakis/geojson/feature" 9 | "github.com/tomchavakis/geojson/geometry" 10 | "github.com/tomchavakis/turf-go/assert" 11 | "github.com/tomchavakis/turf-go/utils" 12 | ) 13 | 14 | const PolyWithHoleFixture = "test-data/poly-with-hole.json" 15 | const MultiPolyWithHoleFixture = "test-data/multipoly-with-hole.json" 16 | 17 | func TestPointInPolygon(t *testing.T) { 18 | type args struct { 19 | point geometry.Point 20 | polygon geometry.Polygon 21 | } 22 | 23 | poly := geometry.Polygon{ 24 | Coordinates: []geometry.LineString{ 25 | { 26 | Coordinates: []geometry.Point{ 27 | { 28 | Lat: 36.171278341935434, 29 | Lng: -86.76624298095703, 30 | }, 31 | { 32 | Lat: 36.170862616662134, 33 | Lng: -86.74238204956055, 34 | }, 35 | { 36 | Lat: 36.19607929145354, 37 | Lng: -86.74100875854492, 38 | }, 39 | { 40 | Lat: 36.2014818084173, 41 | Lng: -86.77362442016602, 42 | }, 43 | { 44 | Lat: 36.171278341935434, 45 | Lng: -86.76624298095703, 46 | }, 47 | }, 48 | }, 49 | }, 50 | } 51 | 52 | tests := map[string]struct { 53 | args args 54 | want bool 55 | wantErr bool 56 | }{ 57 | "point in Polygon": { 58 | args: args{ 59 | point: geometry.Point{ 60 | Lat: 36.185411688981105, 61 | Lng: -86.76074981689453, 62 | }, 63 | polygon: poly, 64 | }, 65 | want: true, 66 | wantErr: false, 67 | }, 68 | "point in Polygon 2": { 69 | args: args{ 70 | point: geometry.Point{ 71 | Lat: 36.19393203374786, 72 | Lng: -86.75946235656737, 73 | }, 74 | polygon: poly, 75 | }, 76 | want: true, 77 | wantErr: false, 78 | }, 79 | "point out of Polygon": { 80 | args: args{ 81 | point: geometry.Point{ 82 | Lat: 36.18416473150645, 83 | Lng: -86.73036575317383, 84 | }, 85 | polygon: poly, 86 | }, 87 | want: false, 88 | wantErr: false, 89 | }, 90 | "point out of Polygon - really close to polygon": { 91 | args: args{ 92 | point: geometry.Point{ 93 | Lat: 36.18200632243299, 94 | Lng: -86.74175441265106, 95 | }, 96 | polygon: poly, 97 | }, 98 | want: false, 99 | wantErr: false, 100 | }, 101 | "point in Polygon - on boundary": { 102 | args: args{ 103 | point: geometry.Point{ 104 | Lat: 36.171278341935434, 105 | Lng: -86.76624298095703, 106 | }, 107 | polygon: poly, 108 | }, 109 | want: true, 110 | wantErr: false, 111 | }, 112 | } 113 | for name, tt := range tests { 114 | t.Run(name, func(t *testing.T) { 115 | got, err := PointInPolygon(tt.args.point, tt.args.polygon) 116 | if (err != nil) != tt.wantErr { 117 | t.Errorf("PointInPolygon() error = %v, wantErr %v", err, tt.wantErr) 118 | return 119 | } 120 | if got != tt.want { 121 | t.Errorf("PointInPolygon() = %v, want %v", got, tt.want) 122 | } 123 | }) 124 | } 125 | } 126 | 127 | func TestFeatureCollection(t *testing.T) { 128 | // test for a simple Polygon 129 | coords := []geometry.Point{ 130 | { 131 | Lat: 0, 132 | Lng: 0, 133 | }, 134 | { 135 | Lat: 0, 136 | Lng: 100, 137 | }, 138 | { 139 | Lat: 100, 140 | Lng: 100, 141 | }, 142 | { 143 | Lat: 100, 144 | Lng: 0, 145 | }, 146 | { 147 | Lat: 0, 148 | Lng: 0, 149 | }, 150 | } 151 | 152 | ml := []geometry.LineString{} 153 | ln, err := geometry.NewLineString(coords) 154 | if err != nil { 155 | t.Errorf("NewLineString error %v", err) 156 | } 157 | 158 | ml = append(ml, *ln) 159 | 160 | poly, err := geometry.NewPolygon(ml) 161 | if err != nil { 162 | t.Errorf("NewPolygon error %v", err) 163 | } 164 | 165 | ptIn := geometry.Point{ 166 | Lat: 50, 167 | Lng: 50, 168 | } 169 | 170 | ptOut := geometry.Point{ 171 | Lat: 140, 172 | Lng: 150, 173 | } 174 | 175 | pip, err := PointInPolygon(ptIn, *poly) 176 | if err != nil { 177 | t.Errorf("PointInPolygon error %v", err) 178 | } 179 | 180 | if !pip { 181 | t.Error("Point is not in Polygon") 182 | } 183 | 184 | pop, err := PointInPolygon(ptOut, *poly) 185 | if err != nil { 186 | t.Errorf("PointInPolygon error %v", err) 187 | } 188 | if pop { 189 | t.Error("Point is not in Polygon") 190 | } 191 | } 192 | 193 | func TestPolyWithHole(t *testing.T) { 194 | ptInHole := geometry.Point{ 195 | Lat: 36.20373274711739, 196 | Lng: -86.69208526611328, 197 | } 198 | ptInPoly := geometry.Point{ 199 | Lat: 36.20258997094334, 200 | Lng: -86.72229766845702, 201 | } 202 | ptOutsidePoly := geometry.Point{ 203 | Lat: 36.18527313913089, 204 | Lng: -86.75079345703125, 205 | } 206 | 207 | fix, err := utils.LoadJSONFixture(PolyWithHoleFixture) 208 | if err != nil { 209 | t.Errorf("LoadJSONFixture error %v", err) 210 | } 211 | 212 | f, err := feature.FromJSON(fix) 213 | if err != nil { 214 | t.Errorf("FromJSON error %v", err) 215 | } 216 | if f == nil { 217 | t.Error("feature cannot be nil") 218 | } 219 | 220 | if f != nil { 221 | assert.Equal(t, f.Type, geojson.Feature) 222 | } 223 | props := map[string]interface{}{ 224 | "name": "Poly with Hole", 225 | "value": float64(3), 226 | "filename": "poly-with-hole.json", 227 | } 228 | if !reflect.DeepEqual(f.Properties, props) { 229 | t.Error("Properties are not equal") 230 | } 231 | if !reflect.DeepEqual(f.Bbox, []float64{-86.73980712890625, 36.173495506147, -86.67303085327148, 36.23084281427824}) { 232 | t.Error("BBOX error") 233 | } 234 | 235 | assert.Equal(t, f.Geometry.GeoJSONType, geojson.Polygon) 236 | 237 | poly, err := f.ToPolygon() 238 | if err != nil { 239 | t.Errorf("ToPolygon error: %v", err) 240 | } 241 | 242 | pih, err := PointInPolygon(ptInHole, *poly) 243 | if err != nil { 244 | t.Errorf("PointInPolygon error: %v", err) 245 | } 246 | if pih { 247 | t.Error("Point in hole is not in Polygon") 248 | } 249 | 250 | pip, err := PointInPolygon(ptInPoly, *poly) 251 | if err != nil { 252 | t.Errorf("PointInPolygon error: %v", err) 253 | } 254 | if !pip { 255 | t.Error("Point in poly is not in Polygon") 256 | } 257 | 258 | pop, err := PointInPolygon(ptOutsidePoly, *poly) 259 | if err != nil { 260 | t.Errorf("PointInPolygon error: %v", err) 261 | } 262 | if pop { 263 | t.Error("Point is not in Polygon") 264 | } 265 | } 266 | 267 | func TestMultiPolyWithHole(t *testing.T) { 268 | ptInHole := geometry.Point{ 269 | Lat: 36.20373274711739, 270 | Lng: -86.69208526611328, 271 | } 272 | ptInPoly := geometry.Point{ 273 | Lat: 36.20258997094334, 274 | Lng: -86.72229766845702, 275 | } 276 | ptInPoly2 := geometry.Point{ 277 | Lat: 36.18527313913089, 278 | Lng: -86.75079345703125, 279 | } 280 | ptOutsidePoly := geometry.Point{ 281 | Lat: 36.23015046460186, 282 | Lng: -86.75302505493164, 283 | } 284 | 285 | fixture, err := utils.LoadJSONFixture(MultiPolyWithHoleFixture) 286 | if err != nil { 287 | t.Errorf("LoadJSONFixture error: %v", err) 288 | } 289 | 290 | f, err := feature.FromJSON(fixture) 291 | if err != nil { 292 | t.Errorf("FromJSON error: %v", err) 293 | } 294 | if f == nil { 295 | t.Error("Feature cannot be nil") 296 | } 297 | if f != nil { 298 | assert.Equal(t, f.Type, geojson.Feature) 299 | props := map[string]interface{}{ 300 | "name": "Poly with Hole", 301 | "value": float64(3), 302 | "filename": "poly-with-hole.json", 303 | } 304 | if !reflect.DeepEqual(f.Properties, props) { 305 | t.Error("Properties are not equal") 306 | } 307 | if !reflect.DeepEqual(f.Bbox, []float64{-86.77362442016602, 36.170862616662134, -86.67303085327148, 36.23084281427824}) { 308 | t.Error("BBOX error") 309 | } 310 | assert.Equal(t, f.Geometry.GeoJSONType, geojson.MultiPolygon) 311 | 312 | poly, err := f.ToMultiPolygon() 313 | if err != nil { 314 | t.Errorf("ToMultiPolygon error: %v", err) 315 | } 316 | 317 | pih := PointInMultiPolygon(ptInHole, *poly) 318 | if pih { 319 | t.Error("Point in hole is not in MultiPolygon") 320 | } 321 | 322 | pip := PointInMultiPolygon(ptInPoly, *poly) 323 | if !pip { 324 | t.Error("Point in poly is not in MultiPolygon") 325 | } 326 | 327 | pip2 := PointInMultiPolygon(ptInPoly2, *poly) 328 | if !pip2 { 329 | t.Error("Point in poly is not in MultiPolygon") 330 | } 331 | 332 | pop := PointInMultiPolygon(ptOutsidePoly, *poly) 333 | if pop { 334 | t.Error("Point in not in MultiPolygon") 335 | } 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | help: 2 | @echo "Please use 'make ' where is one of the following:" 3 | @echo " lint to run the linter." 4 | @echo " test to run the tests." 5 | 6 | .PHONY: tidy 7 | tidy: 8 | go mod tidy 9 | 10 | lint: 11 | golangci-lint run --timeout=2m0s 12 | 13 | test: 14 | go test -v ./... 15 | 16 | .PHONY: cover 17 | cover: 18 | go test -race -coverprofile=coverage.out -coverpkg=./... ./... 19 | go tool cover -html=coverage.out 20 | 21 | fmt: 22 | go fmt ./... 23 | 24 | .PHONY: test -------------------------------------------------------------------------------- /measurement/measurement.go: -------------------------------------------------------------------------------- 1 | package measurement 2 | 3 | import ( 4 | "errors" 5 | "math" 6 | 7 | "github.com/tomchavakis/geojson" 8 | "github.com/tomchavakis/geojson/feature" 9 | "github.com/tomchavakis/geojson/geometry" 10 | "github.com/tomchavakis/turf-go/constants" 11 | "github.com/tomchavakis/turf-go/conversions" 12 | "github.com/tomchavakis/turf-go/internal/common" 13 | "github.com/tomchavakis/turf-go/invariant" 14 | meta "github.com/tomchavakis/turf-go/meta/coordAll" 15 | ) 16 | 17 | // Distance calculates the distance between two points in kilometers. This uses the Haversine formula 18 | func Distance(lon1 float64, lat1 float64, lon2 float64, lat2 float64, units string) (float64, error) { 19 | 20 | dLat := conversions.DegreesToRadians(lat2 - lat1) 21 | dLng := conversions.DegreesToRadians(lon2 - lon1) 22 | lat1R := conversions.DegreesToRadians(lat1) 23 | lat2R := conversions.DegreesToRadians(lat2) 24 | 25 | a := math.Pow(math.Sin(dLat/2), 2) + math.Pow(math.Sin(dLng/2), 2)*math.Cos(lat1R)*math.Cos(lat2R) 26 | c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a)) 27 | // d := constants.EarthRadius * c 28 | 29 | return conversions.RadiansToLength(c, units) 30 | } 31 | 32 | // PointDistance calculates the distance between two points 33 | func PointDistance(p1 geometry.Point, p2 geometry.Point, units string) (float64, error) { 34 | return Distance(p1.Lng, p1.Lat, p2.Lng, p2.Lat, units) 35 | } 36 | 37 | // Bearing finds the geographic bearing between two given points. 38 | func Bearing(lon1 float64, lat1 float64, lon2 float64, lat2 float64) float64 { 39 | dLng := conversions.DegreesToRadians(lon2 - lon1) 40 | lat1R := conversions.DegreesToRadians(lat1) 41 | lat2R := conversions.DegreesToRadians(lat2) 42 | y := math.Sin(dLng) * math.Cos(lat2R) 43 | x := math.Cos(lat1R)*math.Sin(lat2R) - math.Sin(lat1R)*math.Cos(lat2R)*math.Cos(dLng) 44 | 45 | // convert to degrees 46 | bd := conversions.RadiansToDegrees(math.Atan2(y, x)) 47 | 48 | if bd < 0.0 { 49 | bd += 360.0 50 | } 51 | 52 | if bd >= 360.0 { 53 | bd -= 360.0 54 | 55 | } 56 | return bd 57 | 58 | } 59 | 60 | // PointBearing finds the geographic bearing between two points. 61 | func PointBearing(p1 geometry.Point, p2 geometry.Point) float64 { 62 | return Bearing(p1.Lng, p1.Lat, p2.Lng, p2.Lat) 63 | } 64 | 65 | // MidPoint finds the point midway between them. 66 | func MidPoint(p1 geometry.Point, p2 geometry.Point) geometry.Point { 67 | dLon := conversions.DegreesToRadians(p2.Lng - p1.Lng) 68 | lat1R := conversions.DegreesToRadians(p1.Lat) 69 | lon1R := conversions.DegreesToRadians(p1.Lng) 70 | lat2R := conversions.DegreesToRadians(p2.Lat) 71 | Bx := math.Cos(lat2R) * math.Cos(dLon) 72 | By := math.Cos(lat2R) * math.Sin(dLon) 73 | midLat := math.Atan2(math.Sin(lat1R)+math.Sin(lat2R), math.Sqrt((math.Cos(lat1R)+Bx)*(math.Cos(lat1R)+Bx)+By*By)) 74 | midLng := lon1R + math.Atan2(By, math.Cos(lat1R)+Bx) 75 | 76 | return geometry.Point{Lat: conversions.RadiansToDegrees(midLat), Lng: conversions.RadiansToDegrees(midLng)} 77 | } 78 | 79 | // Destination returns a destination point according to a reference point, a distance in km and a bearing in degrees from True North. 80 | func Destination(p1 geometry.Point, distance float64, bearing float64, units string) (*geometry.Point, error) { 81 | lonR := conversions.DegreesToRadians(p1.Lng) 82 | latR := conversions.DegreesToRadians(p1.Lat) 83 | bR := conversions.DegreesToRadians(bearing) 84 | radians, err := conversions.LengthToRadians(distance, units) 85 | if err != nil { 86 | return nil, err 87 | } 88 | dLat := math.Asin(math.Sin(latR)*math.Cos(radians) + math.Cos(latR)*math.Sin(radians)*math.Cos(bR)) 89 | dLng := lonR + math.Atan2(math.Sin(bR)*math.Sin(radians)*math.Cos(latR), math.Cos(radians)-math.Sin(latR)*math.Sin(dLat)) 90 | 91 | return &geometry.Point{Lat: conversions.RadiansToDegrees(dLat), Lng: conversions.RadiansToDegrees(dLng)}, nil 92 | } 93 | 94 | // Length measures the length of a geometry. 95 | func Length(t interface{}, units string) (float64, error) { 96 | 97 | result := 0.0 98 | var err error 99 | var l float64 100 | switch gtp := t.(type) { 101 | case []geometry.Point: 102 | l, err = length(gtp, units) 103 | result = l 104 | case geometry.LineString: 105 | l, err = length(gtp.Coordinates, units) 106 | result = l 107 | case geometry.MultiLineString: 108 | coords := gtp.Coordinates // []LineString 109 | for _, c := range coords { 110 | l, err = length(c.Coordinates, units) 111 | if err != nil { 112 | break 113 | } 114 | result += l 115 | } 116 | case geometry.Polygon: 117 | for _, c := range gtp.Coordinates { 118 | l, err = length(c.Coordinates, units) 119 | if err != nil { 120 | break 121 | } 122 | result += l 123 | } 124 | case geometry.MultiPolygon: 125 | coords := gtp.Coordinates 126 | for _, coord := range coords { 127 | for _, pl := range coord.Coordinates { 128 | l, err = length(pl.Coordinates, units) 129 | if err != nil { 130 | break 131 | } 132 | result += l 133 | } 134 | } 135 | } 136 | return result, err 137 | } 138 | 139 | // http://turfjs.org/docs/#linedistance 140 | func length(coords []geometry.Point, units string) (float64, error) { 141 | travelled := 0.0 142 | prevCoords := coords[0] 143 | var currentCoords geometry.Point 144 | for i := 1; i < len(coords); i++ { 145 | currentCoords = coords[i] 146 | pd, err := PointDistance(prevCoords, currentCoords, units) 147 | if err != nil { 148 | return 0.0, err 149 | } 150 | travelled += pd 151 | prevCoords = currentCoords 152 | } 153 | return travelled, nil 154 | } 155 | 156 | // Area takes a geometry type and returns its area in square meters 157 | func Area(t interface{}) (float64, error) { 158 | switch gtp := t.(type) { 159 | case *feature.Feature: 160 | return calculateArea(gtp.Geometry) 161 | case *feature.Collection: 162 | features := gtp.Features 163 | total := 0.0 164 | if len(features) > 0 { 165 | for _, f := range features { 166 | ar, err := calculateArea(f.Geometry) 167 | if err != nil { 168 | return 0, err 169 | } 170 | total += ar 171 | } 172 | } 173 | return total, nil 174 | case *geometry.Geometry: 175 | return calculateArea(*gtp) 176 | case *geometry.Polygon: 177 | return polygonArea(gtp.Coordinates), nil 178 | case *geometry.MultiPolygon: 179 | total := 0.0 180 | for i := 0; i < len(gtp.Coordinates); i++ { 181 | total += polygonArea(gtp.Coordinates[i].Coordinates) 182 | } 183 | return total, nil 184 | } 185 | return 0.0, nil 186 | } 187 | 188 | func calculateArea(g geometry.Geometry) (float64, error) { 189 | total := 0.0 190 | if g.GeoJSONType == geojson.Polygon { 191 | 192 | poly, err := g.ToPolygon() 193 | if err != nil { 194 | return 0.0, errors.New("cannot convert geometry to Polygon") 195 | } 196 | return polygonArea(poly.Coordinates), nil 197 | } else if g.GeoJSONType == geojson.MultiPolygon { 198 | multiPoly, err := g.ToMultiPolygon() 199 | if err != nil { 200 | return 0.0, errors.New("cannot convert geometry to MultiPolygon") 201 | } 202 | for i := 0; i < len(multiPoly.Coordinates); i++ { 203 | total += polygonArea(multiPoly.Coordinates[i].Coordinates) 204 | } 205 | 206 | return total, nil 207 | } else { 208 | // area should be 0 for Point, MultiPoint, LineString and MultiLineString 209 | return total, nil 210 | } 211 | } 212 | 213 | func polygonArea(coords []geometry.LineString) float64 { 214 | total := 0.0 215 | if len(coords) > 0 { 216 | total += math.Abs(ringArea(coords[0].Coordinates)) 217 | for i := 1; i < len(coords); i++ { 218 | total -= math.Abs(ringArea(coords[i].Coordinates)) 219 | } 220 | } 221 | return total 222 | } 223 | 224 | // calculate the approximate area of the polygon were it projected onto the earth. 225 | // Note that this area will be positive if ring is oriented clockwise, otherwise 226 | // it will be negative. 227 | // 228 | // Reference: 229 | // Robert. G. Chamberlain and William H. Duquette, "Some Algorithms for Polygons on a Sphere", 230 | // JPL Publication 07-03, Jet Propulsion 231 | // Laboratory, Pasadena, CA, June 2007 https://trs.jpl.nasa.gov/handle/2014/41271 232 | func ringArea(coords []geometry.Point) float64 { 233 | var p1 geometry.Point 234 | var p2 geometry.Point 235 | var p3 geometry.Point 236 | var lowerIndex int 237 | var middleIndex int 238 | var upperIndex int 239 | total := 0.0 240 | coordsLength := len(coords) 241 | 242 | if coordsLength > 2 { 243 | for i := 0; i < coordsLength; i++ { 244 | if i == coordsLength-2 { // i = N-2 245 | lowerIndex = coordsLength - 2 246 | middleIndex = coordsLength - 1 247 | upperIndex = 0 248 | } else if i == coordsLength-1 { //i = N-1 249 | lowerIndex = coordsLength - 1 250 | middleIndex = 0 251 | upperIndex = 1 252 | } else { // i =0 to N-3 253 | lowerIndex = i 254 | middleIndex = i + 1 255 | upperIndex = i + 2 256 | } 257 | p1 = coords[lowerIndex] 258 | p2 = coords[middleIndex] 259 | p3 = coords[upperIndex] 260 | total += (conversions.DegreesToRadians(p3.Lng) - conversions.DegreesToRadians(p1.Lng)) * math.Sin(conversions.DegreesToRadians(p2.Lat)) 261 | } 262 | total = total * constants.EarthRadius * constants.EarthRadius / 2.0 263 | } 264 | return total 265 | } 266 | 267 | // BBox takes a set of features, calculates the bbox of all input features, and returns a bounding box. 268 | func BBox(t interface{}) ([]float64, error) { 269 | return bboxGeom(t, false) 270 | } 271 | 272 | func bboxGeom(t interface{}, excludeWrapCoord bool) ([]float64, error) { 273 | coords, err := meta.CoordAll(t, &excludeWrapCoord) 274 | if err != nil { 275 | return nil, errors.New("cannot get coords") 276 | } 277 | 278 | return bboxCalculator(coords), nil 279 | } 280 | 281 | // Along Takes a line and returns a point at a specified distance along the line. 282 | func Along(ln geometry.LineString, distance float64, units string) (*geometry.Point, error) { 283 | travelled := 0.0 284 | for i := 0; i < len(ln.Coordinates); i++ { 285 | if distance >= travelled && i == len(ln.Coordinates)-1 { 286 | break 287 | } else if travelled >= distance { 288 | overshot := distance - travelled 289 | if overshot == 0 { 290 | return &ln.Coordinates[i], nil 291 | } 292 | direction := PointBearing(ln.Coordinates[i], ln.Coordinates[i-1]) - 180 293 | 294 | d, err := Destination(ln.Coordinates[i], overshot, direction, units) 295 | if err != nil { 296 | return nil, err 297 | } 298 | return d, nil 299 | } else { 300 | pd, err := PointDistance(ln.Coordinates[i], ln.Coordinates[i+1], units) 301 | if err != nil { 302 | return nil, err 303 | } 304 | travelled += pd 305 | } 306 | } 307 | 308 | return &ln.Coordinates[len(ln.Coordinates)-1], nil 309 | } 310 | 311 | // BBoxPolygon takes a BoundingBox and returns an equivalent polygon. 312 | func BBoxPolygon(bbox geojson.BBOX, id string) (*feature.Feature, error) { 313 | 314 | var cds [][][]float64 315 | coords := [][]float64{ 316 | { 317 | bbox.South, 318 | bbox.West, 319 | }, 320 | { 321 | bbox.South, 322 | bbox.East, 323 | }, 324 | { 325 | bbox.North, 326 | bbox.East, 327 | }, 328 | { 329 | bbox.North, 330 | bbox.West, 331 | }, 332 | { 333 | bbox.South, 334 | bbox.West, 335 | }, 336 | } 337 | cds = append(cds, coords) 338 | bbbox, err := BBox(bbox) 339 | if err != nil { 340 | return nil, err 341 | } 342 | geom := geometry.Geometry{ 343 | GeoJSONType: geojson.Polygon, 344 | Coordinates: cds, 345 | } 346 | 347 | f, err := feature.New(geom, bbbox, nil, id) 348 | if err != nil { 349 | return nil, err 350 | } 351 | 352 | return f, nil 353 | } 354 | 355 | func bboxCalculator(coords []geometry.Point) []float64 { 356 | var bbox []float64 357 | bbox = append(bbox, math.Inf(+1)) 358 | bbox = append(bbox, math.Inf(+1)) 359 | bbox = append(bbox, math.Inf(-1)) 360 | bbox = append(bbox, math.Inf(-1)) 361 | 362 | for _, p := range coords { 363 | if bbox[0] > p.Lng { 364 | bbox[0] = p.Lng 365 | } 366 | if bbox[1] > p.Lat { 367 | bbox[1] = p.Lat 368 | } 369 | if bbox[2] < p.Lng { 370 | bbox[2] = p.Lng 371 | } 372 | if bbox[3] < p.Lat { 373 | bbox[3] = p.Lat 374 | } 375 | } 376 | return bbox 377 | } 378 | 379 | // CenterFeature takes a Feature and returns the absolute center of the Feature. Return a Feature with a Point geometry type. 380 | func CenterFeature(f feature.Feature, properties map[string]interface{}, id string) (*feature.Feature, error) { 381 | fs := []feature.Feature{} 382 | fs = append(fs, f) 383 | fc, err := feature.NewFeatureCollection(fs) 384 | if err != nil { 385 | return nil, err 386 | } 387 | return CenterFeatureCollection(*fc, properties, id) 388 | } 389 | 390 | // CenterFeatureCollection takes a FeatureCollection and returns the absolute center of the Feature(s) in the FeatureCollection. 391 | func CenterFeatureCollection(fc feature.Collection, properties map[string]interface{}, id string) (*feature.Feature, error) { 392 | ext, err := BBox(&fc) 393 | if err != nil { 394 | return nil, err 395 | } 396 | 397 | finalCenterLongtitude := (ext[0] + ext[2]) / 2 398 | finalCenterLatitude := (ext[1] + ext[3]) / 2 399 | 400 | coords := []float64{finalCenterLongtitude, finalCenterLatitude} 401 | g := geometry.Geometry{ 402 | GeoJSONType: geojson.Point, 403 | Coordinates: coords, 404 | } 405 | f, err := feature.New(g, ext, properties, id) 406 | if err != nil { 407 | return nil, err 408 | } 409 | return f, nil 410 | } 411 | 412 | // Envelope takes a FeatureCollection and returns a rectangular Polygon than encompasses all vertices. 413 | func Envelope(fc feature.Collection) (*feature.Feature, error) { 414 | excludeWrapCoord := false 415 | coords, err := meta.CoordAll(&fc, &excludeWrapCoord) 416 | if err != nil { 417 | return nil, errors.New("cannot get coords") 418 | } 419 | 420 | return calcEnvelopeCoords(coords) 421 | } 422 | 423 | func calcEnvelopeCoords(coords []geometry.Point) (*feature.Feature, error) { 424 | if len(coords) == 0 { 425 | return nil, errors.New("Empty coordinates") 426 | } 427 | northStar := coords[0] 428 | southStar := coords[0] 429 | westStar := coords[0] 430 | eastStar := coords[0] 431 | 432 | for _, p := range coords { 433 | 434 | if northStar.Lat < p.Lat { 435 | northStar = p 436 | } 437 | 438 | if southStar.Lat > p.Lat { 439 | southStar = p 440 | } 441 | 442 | if westStar.Lng > p.Lng { 443 | westStar = p 444 | } 445 | 446 | if eastStar.Lng < p.Lng { 447 | eastStar = p 448 | } 449 | } 450 | 451 | var cds [][][]float64 452 | polygonCoords := [][]float64{ 453 | { 454 | northStar.Lat, 455 | westStar.Lng, 456 | }, 457 | { 458 | southStar.Lat, 459 | westStar.Lng, 460 | }, 461 | { 462 | southStar.Lat, 463 | eastStar.Lng, 464 | }, 465 | { 466 | northStar.Lat, 467 | eastStar.Lng, 468 | }, 469 | { 470 | northStar.Lat, 471 | westStar.Lng, 472 | }, 473 | } 474 | 475 | stars := []geometry.Point{ 476 | northStar, 477 | southStar, 478 | eastStar, 479 | westStar, 480 | } 481 | 482 | cds = append(cds, polygonCoords) 483 | bbox := bboxCalculator(stars) 484 | bbbox, err := BBox(bbox) 485 | if err != nil { 486 | return nil, err 487 | } 488 | geom := geometry.Geometry{ 489 | GeoJSONType: geojson.Polygon, 490 | Coordinates: cds, 491 | } 492 | 493 | f, err := feature.New(geom, bbbox, nil, "") 494 | if err != nil { 495 | return nil, err 496 | } 497 | 498 | return f, nil 499 | } 500 | 501 | // CentroidFeature takes a Feature and returns the centroid of the Feature. Return a Feature with a Point geometry type. 502 | func CentroidFeature(f feature.Feature, properties map[string]interface{}, id string) (*feature.Feature, error) { 503 | fs := []feature.Feature{} 504 | fs = append(fs, f) 505 | fc, err := feature.NewFeatureCollection(fs) 506 | if err != nil { 507 | return nil, err 508 | } 509 | return CentroidFeatureCollection(*fc, properties, id) 510 | } 511 | 512 | // CentroidFeatureCollection takes a FeatureCollection and returns the centroid of the Feature(s) in the FeatureCollection. 513 | func CentroidFeatureCollection(fc feature.Collection, properties map[string]interface{}, id string) (*feature.Feature, error) { 514 | excludeWrapCoord := true 515 | coords, err := meta.CoordAll(&fc, &excludeWrapCoord) 516 | 517 | if err != nil { 518 | return nil, errors.New("cannot get coords") 519 | } 520 | 521 | coordsLength := len(coords) 522 | if coordsLength < 1 { 523 | return nil, errors.New("no coordinates found") 524 | } 525 | 526 | xSum := 0.0 527 | ySum := 0.0 528 | 529 | for i := 0; i < coordsLength; i++ { 530 | xSum += coords[i].Lng 531 | ySum += coords[i].Lat 532 | } 533 | 534 | finalCenterLongtitude := xSum / float64(coordsLength) 535 | finalCenterLatitude := ySum / float64(coordsLength) 536 | 537 | coordinates := []float64{finalCenterLongtitude, finalCenterLatitude} 538 | g := geometry.Geometry{ 539 | GeoJSONType: geojson.Point, 540 | Coordinates: coordinates, 541 | } 542 | f, err := feature.New(g, bboxCalculator(coords), properties, id) 543 | if err != nil { 544 | return nil, err 545 | } 546 | return f, nil 547 | } 548 | 549 | // RhumbBearing takes two points and finds the bearing angle between them along a Rhumb line 550 | // final option calculates the final bearing if true 551 | // returns a bearing from north in decimal degrees, between -180 and 180 degrees (positive clockwise) 552 | // i.e the angle measured in degrees start the north line (0 degrees) 553 | // https://en.wikipedia.org/wiki/Rhumb_line 554 | // In navigation, a rhumb line or loxodrome is an arc crossing all meridians of longitude at the same angle, that is, a path with constant 555 | // bearing as measured relative to true north. 556 | func RhumbBearing(start geometry.Point, end geometry.Point, final bool) (*float64, error) { 557 | var bear360 float64 558 | e, err := invariant.GetCoord(&end) 559 | if err != nil { 560 | return nil, err 561 | } 562 | s, err := invariant.GetCoord(&start) 563 | if err != nil { 564 | return nil, err 565 | } 566 | 567 | if final { 568 | bear360 = calculateRhumbBearing(e, s) 569 | } else { 570 | bear360 = calculateRhumbBearing(s, e) 571 | } 572 | var bear180 float64 573 | if bear360 > 180.0 { 574 | bear180 = -(360.0 - bear360) 575 | } else { 576 | bear180 = bear360 577 | } 578 | return &bear180, nil 579 | } 580 | 581 | // returns the bearing from 'this' point to destination point along a rhumb line. 582 | // adapted from geodesy https://github.com/chrisveness/geodesy/blob/master/latlon-spherical.js 583 | func calculateRhumbBearing(from []float64, to []float64) float64 { 584 | φ := conversions.DegreesToRadians(from[1]) 585 | φ2 := conversions.DegreesToRadians(to[1]) 586 | Δλ := conversions.DegreesToRadians(to[0] - from[0]) 587 | // if Δλ is over 180° take shorter rhumb line across the anti-meridian: 588 | if Δλ > math.Pi { 589 | Δλ -= 2 * math.Pi 590 | } 591 | if Δλ < -math.Pi { 592 | Δλ += 2 * math.Pi 593 | } 594 | 595 | Δψ := math.Log(math.Tan(φ2/2+math.Pi/4) / math.Tan(φ/2+math.Pi/4)) 596 | θ := math.Atan2(Δλ, Δψ) 597 | tmp := conversions.RadiansToDegrees(θ) + 360.0 598 | return math.Mod(tmp, 360) 599 | } 600 | 601 | // RhumbDestination returns the destination having travelled the given distance along a Rhumb line from the origin Point with the (varant) given bearing. 602 | // If you maintain a constant bearing along a rhumb line, you will gradually spiral towards one of the poles. ref. http://www.movable-type.co.uk/scripts/latlong.html#rhumblines 603 | func RhumbDestination(origin geometry.Point, distance float64, bearing float64, units string, properties map[string]interface{}) (*feature.Feature, error) { 604 | wasNegativeDistance := distance < 0 605 | distanceInMeters, err := conversions.ConvertLength(math.Abs(distance), units, constants.UnitMeters) 606 | if err != nil { 607 | return nil, err 608 | } 609 | 610 | if wasNegativeDistance { 611 | distanceInMeters = -math.Abs(distanceInMeters) 612 | } 613 | coords, err := invariant.GetCoord(&origin) 614 | if err != nil { 615 | return nil, err 616 | } 617 | 618 | destination := calculateRhumbDestination(coords, distanceInMeters, bearing, nil) 619 | 620 | // compensate the crossing of the 180th meridian (https://macwright.org/2016/09/26/the-180th-meridian.html) 621 | // solution from https://github.com/mapbox/mapbox-gl-js/issues/3250#issuecomment-294887678 622 | 623 | if destination[0]-coords[0] > 180.0 { 624 | destination[0] += -360.0 625 | } else { 626 | if coords[0]-destination[0] > 180.0 { 627 | destination[0] += 360 628 | } else { 629 | destination[0] += 0 630 | } 631 | } 632 | 633 | result := feature.Feature{ 634 | Type: "Feature", 635 | Properties: properties, 636 | Bbox: []float64{}, 637 | Geometry: geometry.Geometry{ 638 | GeoJSONType: "Point", 639 | Coordinates: destination, 640 | }, 641 | } 642 | 643 | return &result, nil 644 | } 645 | 646 | // Adapted from Geodesy: http://www.movable-type.co.uk/scripts/latlong.html#rhumblines 647 | func calculateRhumbDestination(origin []float64, distance float64, bearing float64, radius *float64) []float64 { 648 | if radius == nil { 649 | radius = common.Float64Ptr(constants.EarthRadius) 650 | } 651 | 652 | // angular distance in radians 653 | δ := distance / *radius 654 | // to radians, but without normalize to π 655 | λ1 := (origin[0] * math.Pi) / 180.0 656 | φ1 := conversions.DegreesToRadians(origin[1]) 657 | θ := conversions.DegreesToRadians(bearing) 658 | 659 | Δφ := δ * math.Cos(θ) 660 | φ2 := φ1 + Δφ 661 | 662 | if math.Abs(φ2) > (math.Pi / 2) { 663 | if φ2 > 0 { 664 | φ2 = math.Pi - φ2 665 | } else { 666 | φ2 = -math.Pi - φ2 667 | } 668 | } 669 | 670 | Δψ := math.Log(math.Tan((φ2/2)+(math.Pi/4)) / math.Tan((φ1/2)+(math.Pi/4))) 671 | q := 0.0 672 | if math.Abs(Δψ) > (10e-12) { 673 | q = Δφ / Δψ 674 | } else { 675 | q = math.Cos(φ1) 676 | } 677 | 678 | Δλ := (δ * math.Sin(θ)) / q 679 | λ2 := λ1 + Δλ 680 | 681 | // normalise to -180...+180 682 | return []float64{(math.Mod(((λ2*180.0)/math.Pi+540), 360) - 180.0), (φ2 * 180.0) / math.Pi} 683 | } 684 | 685 | // RhumbDistance calculates the distance along a rhumb line between two points. 686 | func RhumbDistance(from geometry.Point, to geometry.Point, units string) (*float64, error) { 687 | origin, err := invariant.GetCoord(&from) 688 | if err != nil { 689 | return nil, err 690 | } 691 | destination, err := invariant.GetCoord(&to) 692 | if err != nil { 693 | return nil, err 694 | } 695 | // compensate the crossing of the 180th meridian (https://macwright.org/2016/09/26/the-180th-meridian.html) 696 | // solution from https://github.com/mapbox/mapbox-gl-js/issues/3250#issuecomment-294887678 697 | 698 | if destination[0]-origin[0] > 180.0 { 699 | destination[0] += -360.0 700 | } else { 701 | if origin[0]-destination[0] > 180.0 { 702 | destination[0] += 360 703 | } else { 704 | destination[0] += 0 705 | } 706 | } 707 | distanceInMeters := calculateRhumbDistance(origin, destination, nil) 708 | distance, err := conversions.ConvertLength(distanceInMeters, constants.UnitMeters, units) 709 | if err != nil { 710 | return nil, err 711 | } 712 | return &distance, nil 713 | } 714 | 715 | // returns the distance travelling from 'this' point to destination point along a rhumb line. 716 | // adapted from Geodesy: https://github.com/chrisveness/geodesy/blob/master/latlon-spherical.js 717 | func calculateRhumbDistance(origin []float64, destination []float64, radius *float64) float64 { 718 | if radius == nil { 719 | radius = common.Float64Ptr(constants.EarthRadius) 720 | } 721 | 722 | φ1 := (origin[1] * math.Pi) / 180.0 723 | φ2 := (destination[1] * math.Pi) / 180.0 724 | Δφ := φ2 - φ1 725 | Δλ := (math.Abs(destination[0]-origin[0]) * math.Pi) / 180.0 726 | // if dLon over 180° take shorter rhumb line across the anti-meridian: 727 | if Δλ > math.Pi { 728 | Δλ -= 2 * math.Pi 729 | } 730 | 731 | // on Mercator projection, longitude distances shrink by latitude; q is the 'stretch factor' 732 | // q becomes ill-conditioned along E-W line (0/0); use empirical tolerance to avoid it 733 | Δψ := math.Log(math.Tan(φ2/2+math.Pi/4) / math.Tan(φ1/2+math.Pi/4)) 734 | 735 | q := 0.0 736 | if math.Abs(Δψ) > 10e-12 { 737 | q = Δφ / Δψ 738 | } else { 739 | q = math.Cos(φ1) 740 | } 741 | // distance is pythagoras on 'stretched' Mercator projection 742 | Δ := math.Sqrt(Δφ*Δφ + q*q*Δλ*Δλ) // angular distance in radians 743 | dist := Δ * *radius 744 | 745 | return dist 746 | } 747 | -------------------------------------------------------------------------------- /meta/coordAll/coordAll.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/tomchavakis/geojson" 7 | "github.com/tomchavakis/geojson/feature" 8 | "github.com/tomchavakis/geojson/geometry" 9 | ) 10 | 11 | // CoordAll get all coordinates from any GeoJSON object. 12 | func CoordAll(t interface{}, excludeWrapCoord *bool) ([]geometry.Point, error) { 13 | switch gtp := t.(type) { 14 | case *geometry.Point: 15 | return coordAllPoint(*gtp), nil 16 | case *geometry.MultiPoint: 17 | return coordAllMultiPoint(*gtp), nil 18 | case *geometry.LineString: 19 | return coordAllLineString(*gtp), nil 20 | case *geometry.Polygon: 21 | if excludeWrapCoord == nil { 22 | return nil, errors.New("exclude wrap coord can't be null") 23 | } 24 | return coordAllPolygon(*gtp, *excludeWrapCoord), nil 25 | case *geometry.MultiLineString: 26 | return coordAllMultiLineString(*gtp), nil 27 | case *geometry.MultiPolygon: 28 | if excludeWrapCoord == nil { 29 | return nil, errors.New("exclude wrap coord can't be null") 30 | } 31 | return coordAllMultiPolygon(*gtp, *excludeWrapCoord), nil 32 | case *feature.Feature: 33 | return coordAllFeature(*gtp, *excludeWrapCoord) 34 | case *feature.Collection: 35 | if excludeWrapCoord == nil { 36 | return nil, errors.New("exclude wrap coord can't be null") 37 | } 38 | return coordAllFeatureCollection(*gtp, *excludeWrapCoord) 39 | case *geometry.Collection: 40 | pts := []geometry.Point{} 41 | for _, gmt := range gtp.Geometries { 42 | snl, _ := coordsAllFromSingleGeometry(pts, gmt, *excludeWrapCoord) 43 | pts = append(pts, snl...) 44 | } 45 | return pts, nil 46 | } 47 | 48 | return nil, nil 49 | } 50 | 51 | func coordAllPoint(p geometry.Point) []geometry.Point { 52 | var coords []geometry.Point 53 | coords = append(coords, p) 54 | return coords 55 | } 56 | 57 | func coordAllMultiPoint(m geometry.MultiPoint) []geometry.Point { 58 | return appendCoordsToMultiPoint([]geometry.Point{}, m) 59 | } 60 | 61 | func appendCoordsToMultiPoint(coords []geometry.Point, m geometry.MultiPoint) []geometry.Point { 62 | coords = append(coords, m.Coordinates...) 63 | return coords 64 | } 65 | 66 | func coordAllLineString(m geometry.LineString) []geometry.Point { 67 | return appendCoordsToLineString([]geometry.Point{}, m) 68 | } 69 | 70 | func appendCoordsToLineString(coords []geometry.Point, l geometry.LineString) []geometry.Point { 71 | coords = append(coords, l.Coordinates...) 72 | return coords 73 | } 74 | 75 | func coordAllPolygon(p geometry.Polygon, excludeWrapCoord bool) []geometry.Point { 76 | return appendCoordsToPolygon([]geometry.Point{}, p, excludeWrapCoord) 77 | } 78 | 79 | func appendCoordsToPolygon(coords []geometry.Point, p geometry.Polygon, excludeWrapCoord bool) []geometry.Point { 80 | wrapShrink := 0 81 | if excludeWrapCoord { 82 | wrapShrink = 1 83 | } 84 | for i := 0; i < len(p.Coordinates); i++ { 85 | for j := 0; j < len(p.Coordinates[i].Coordinates)-wrapShrink; j++ { 86 | coords = append(coords, p.Coordinates[i].Coordinates[j]) 87 | } 88 | } 89 | return coords 90 | } 91 | 92 | func coordAllMultiLineString(m geometry.MultiLineString) []geometry.Point { 93 | return appendCoordToMultiLineString([]geometry.Point{}, m) 94 | } 95 | 96 | func appendCoordToMultiLineString(coords []geometry.Point, m geometry.MultiLineString) []geometry.Point { 97 | for i := 0; i < len(m.Coordinates); i++ { 98 | coords = append(coords, m.Coordinates[i].Coordinates...) 99 | } 100 | return coords 101 | } 102 | 103 | func coordAllMultiPolygon(mp geometry.MultiPolygon, excludeWrapCoord bool) []geometry.Point { 104 | 105 | return appendCoordToMultiPolygon([]geometry.Point{}, mp, excludeWrapCoord) 106 | } 107 | 108 | func appendCoordToMultiPolygon(coords []geometry.Point, mp geometry.MultiPolygon, excludeWrapCoord bool) []geometry.Point { 109 | wrapShrink := 0 110 | if excludeWrapCoord { 111 | wrapShrink = 1 112 | } 113 | 114 | for i := 0; i < len(mp.Coordinates); i++ { 115 | for j := 0; j < len(mp.Coordinates[i].Coordinates); j++ { 116 | for k := 0; k < len(mp.Coordinates[i].Coordinates[j].Coordinates)-wrapShrink; k++ { 117 | coords = append(coords, mp.Coordinates[i].Coordinates[j].Coordinates[k]) 118 | } 119 | } 120 | } 121 | return coords 122 | } 123 | 124 | func coordAllFeature(f feature.Feature, excludeWrapCoord bool) ([]geometry.Point, error) { 125 | return appendCoordToFeature([]geometry.Point{}, f, excludeWrapCoord) 126 | } 127 | 128 | func coordAllFeatureCollection(c feature.Collection, excludeWrapCoord bool) ([]geometry.Point, error) { 129 | var finalCoordsList []geometry.Point 130 | for _, f := range c.Features { 131 | finalCoordsList, _ = appendCoordToFeature(finalCoordsList, f, excludeWrapCoord) 132 | } 133 | return finalCoordsList, nil 134 | } 135 | 136 | func appendCoordToFeature(pointList []geometry.Point, f feature.Feature, excludeWrapCoord bool) ([]geometry.Point, error) { 137 | 138 | coords, err := coordsAllFromSingleGeometry(pointList, f.Geometry, excludeWrapCoord) 139 | if err != nil { 140 | return nil, err 141 | } 142 | return coords, nil 143 | } 144 | 145 | func coordsAllFromSingleGeometry(pointList []geometry.Point, g geometry.Geometry, excludeWrapCoord bool) ([]geometry.Point, error) { 146 | 147 | if g.GeoJSONType == geojson.Point { 148 | p, err := g.ToPoint() 149 | if err != nil { 150 | return nil, err 151 | } 152 | pointList = append(pointList, *p) 153 | } 154 | 155 | if g.GeoJSONType == geojson.MultiPoint { 156 | mp, err := g.ToMultiPoint() 157 | if err != nil { 158 | return nil, err 159 | } 160 | pointList = appendCoordsToMultiPoint(pointList, *mp) 161 | } 162 | 163 | if g.GeoJSONType == geojson.LineString { 164 | ln, err := g.ToLineString() 165 | if err != nil { 166 | return nil, err 167 | } 168 | pointList = appendCoordsToLineString(pointList, *ln) 169 | } 170 | 171 | if g.GeoJSONType == geojson.MultiLineString { 172 | mln, err := g.ToMultiLineString() 173 | if err != nil { 174 | return nil, err 175 | } 176 | pointList = appendCoordToMultiLineString(pointList, *mln) 177 | } 178 | 179 | if g.GeoJSONType == geojson.Polygon { 180 | poly, err := g.ToPolygon() 181 | if err != nil { 182 | return nil, err 183 | } 184 | return appendCoordsToPolygon(pointList, *poly, excludeWrapCoord), nil 185 | 186 | } 187 | 188 | if g.GeoJSONType == geojson.MultiPolygon { 189 | multiPoly, err := g.ToMultiPolygon() 190 | if err != nil { 191 | return nil, err 192 | } 193 | return appendCoordToMultiPolygon(pointList, *multiPoly, excludeWrapCoord), nil 194 | } 195 | 196 | return pointList, nil 197 | } 198 | 199 | // GetCoord unwrap a coordinate from a Feature with a Point geometry. 200 | func GetCoord(obj feature.Feature) (*geometry.Point, error) { 201 | if obj.Geometry.GeoJSONType == geojson.Point { 202 | return obj.Geometry.ToPoint() 203 | } 204 | return nil, errors.New("invalid feature") 205 | } 206 | -------------------------------------------------------------------------------- /meta/coordAll/coordAll_test.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tomchavakis/geojson/feature" 7 | "github.com/tomchavakis/geojson/geometry" 8 | "github.com/tomchavakis/turf-go/assert" 9 | ) 10 | 11 | func TestCoordAllPont(t *testing.T) { 12 | json := "{ \"type\": \"Point\", \"coordinates\": [23.0, 54.0]}" 13 | geometry, err := geometry.FromJSON(json) 14 | if err != nil { 15 | t.Errorf("geometry error %v", err) 16 | } 17 | 18 | pt, err := geometry.ToPoint() 19 | if err != nil { 20 | t.Errorf("convert to Point error %v", err) 21 | } 22 | 23 | pts, err := CoordAll(pt, nil) 24 | if err != nil { 25 | t.Errorf("CoordAll err %v", err) 26 | } 27 | 28 | assert.Equal(t, len(pts), 1) 29 | assert.Equal(t, pts[0].Lat, 54.0) 30 | assert.Equal(t, pts[0].Lng, 23.0) 31 | } 32 | 33 | func TestCoordAllLineString(t *testing.T) { 34 | json := "{ \"type\": \"LineString\", \"coordinates\": [[0.0, 0.0], [1.0, 1.0]]}" 35 | geometry, err := geometry.FromJSON(json) 36 | if err != nil { 37 | t.Errorf("geometry error %v", err) 38 | } 39 | 40 | ln, err := geometry.ToLineString() 41 | if err != nil { 42 | t.Errorf("convert to LineString error %v", err) 43 | } 44 | 45 | pts, err := CoordAll(ln, nil) 46 | if err != nil { 47 | t.Errorf("CoordAll err %v", err) 48 | } 49 | assert.Equal(t, len(pts), 2) 50 | 51 | assert.Equal(t, pts[0].Lat, 0.0) 52 | assert.Equal(t, pts[0].Lng, 0.0) 53 | 54 | assert.Equal(t, pts[1].Lat, 1.0) 55 | assert.Equal(t, pts[1].Lng, 1.0) 56 | } 57 | 58 | func TestCoordAllPolygon(t *testing.T) { 59 | json := "{ \"type\": \"Polygon\", \"coordinates\": [[[0.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]]]}" 60 | geometry, err := geometry.FromJSON(json) 61 | if err != nil { 62 | t.Errorf("geometry error %v", err) 63 | } 64 | 65 | poly, err := geometry.ToPolygon() 66 | if err != nil { 67 | t.Errorf("convert to Polygon error %v", err) 68 | } 69 | 70 | f := false 71 | pts, err := CoordAll(poly, &f) 72 | if err != nil { 73 | t.Errorf("CoordAll err %v", err) 74 | } 75 | assert.Equal(t, len(pts), 4) 76 | 77 | assert.Equal(t, pts[0].Lat, 0.0) 78 | assert.Equal(t, pts[0].Lng, 0.0) 79 | 80 | assert.Equal(t, pts[1].Lat, 1.0) 81 | assert.Equal(t, pts[1].Lng, 1.0) 82 | 83 | assert.Equal(t, pts[2].Lat, 1.0) 84 | assert.Equal(t, pts[2].Lng, 0.0) 85 | 86 | assert.Equal(t, pts[3].Lat, 0.0) 87 | assert.Equal(t, pts[3].Lng, 0.0) 88 | } 89 | 90 | func TestCoordExclueWrapCoord(t *testing.T) { 91 | json := "{ \"type\": \"Polygon\", \"coordinates\": [[[0.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]]]}" 92 | geometry, err := geometry.FromJSON(json) 93 | if err != nil { 94 | t.Errorf("geometry error %v", err) 95 | } 96 | 97 | poly, err := geometry.ToPolygon() 98 | if err != nil { 99 | t.Errorf("convert to Polygon error %v", err) 100 | } 101 | 102 | f := true 103 | pts, err := CoordAll(poly, &f) 104 | if err != nil { 105 | t.Errorf("CoordAll err %v", err) 106 | } 107 | 108 | assert.Equal(t, len(pts), 3) 109 | 110 | assert.Equal(t, pts[0].Lat, 0.0) 111 | assert.Equal(t, pts[0].Lng, 0.0) 112 | 113 | assert.Equal(t, pts[1].Lat, 1.0) 114 | assert.Equal(t, pts[1].Lng, 1.0) 115 | 116 | assert.Equal(t, pts[2].Lat, 1.0) 117 | assert.Equal(t, pts[2].Lng, 0.0) 118 | } 119 | 120 | func TestCoordMultiPolygon(t *testing.T) { 121 | json := "{ \"type\": \"MultiPolygon\", \"coordinates\": [[[[0, 0], [1, 1], [0, 1], [0, 0]]]]}" 122 | geometry, err := geometry.FromJSON(json) 123 | if err != nil { 124 | t.Errorf("geometry error %v", err) 125 | } 126 | 127 | multiPoly, err := geometry.ToMultiPolygon() 128 | if err != nil { 129 | t.Errorf("convert to MultiPolygon error %v", err) 130 | } 131 | 132 | f := false 133 | pts, err := CoordAll(multiPoly, &f) 134 | if err != nil { 135 | t.Errorf("CoordAll err %v", err) 136 | } 137 | 138 | assert.Equal(t, len(pts), 4) 139 | 140 | assert.Equal(t, pts[0].Lat, 0.0) 141 | assert.Equal(t, pts[0].Lng, 0.0) 142 | 143 | assert.Equal(t, pts[1].Lat, 1.0) 144 | assert.Equal(t, pts[1].Lng, 1.0) 145 | 146 | assert.Equal(t, pts[2].Lat, 1.0) 147 | assert.Equal(t, pts[2].Lng, 0.0) 148 | 149 | assert.Equal(t, pts[3].Lat, 0.0) 150 | assert.Equal(t, pts[3].Lng, 0.0) 151 | } 152 | 153 | func TestInvariantGetCoord(t *testing.T) { 154 | json := "{ \"type\": \"Feature\", \"geometry\": { \"type\":\"Point\", \"coordinates\":[1,2]}}" 155 | feature, err := feature.FromJSON(json) 156 | if err != nil { 157 | t.Errorf("FromJSON error %v", err) 158 | } 159 | 160 | g, err := GetCoord(*feature) 161 | if err != nil { 162 | t.Errorf("GetCoord error %v", err) 163 | } 164 | 165 | assert.Equal(t, g.Lat, 2.0) 166 | assert.Equal(t, g.Lng, 1.0) 167 | } 168 | 169 | func TestCoordAllFeatureCollection(t *testing.T) { 170 | json := "{\"type\": \"FeatureCollection\", \"features\": [{\"type\": \"Feature\",\"properties\": {\"population\": 200},\"geometry\": {\"type\": \"Point\",\"coordinates\": [-112.0372, 46.608058]}}]}" 171 | c, err := feature.CollectionFromJSON(json) 172 | if err != nil { 173 | t.Errorf("CollectionFromJSON error %v", err) 174 | } 175 | 176 | if c == nil { 177 | t.Error("feature collection can't be nil") 178 | } 179 | 180 | exclude := true 181 | pts, err := CoordAll(c, &exclude) 182 | if err != nil { 183 | t.Errorf("CoordAll error %v", err) 184 | } 185 | 186 | assert.Equal(t, len(pts), 1) 187 | assert.Equal(t, pts[0].Lat, 46.608058) 188 | assert.Equal(t, pts[0].Lng, -112.0372) 189 | } 190 | 191 | // TODO: Write Test after BBOX 192 | func TestCoordAllGeometryCollection(t *testing.T) { 193 | 194 | } 195 | -------------------------------------------------------------------------------- /meta/coordEach/coordEach.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/tomchavakis/geojson" 7 | "github.com/tomchavakis/geojson/feature" 8 | "github.com/tomchavakis/geojson/geometry" 9 | ) 10 | 11 | // CoordEach iterate over coordinates in any Geojson object and apply the callbackFn 12 | // geojson can be a FeatureCollection | Feature | Geometry 13 | // callbackFn is a method that takes a point and returns a point 14 | // excludeWrapCoord whether or not to include the final coordinate of LinearRings that wraps the ring in its iteration. 15 | func CoordEach(geojson interface{}, callbackFn func(geometry.Point) geometry.Point, excludeWrapCoord *bool) ([]geometry.Point, error) { 16 | if geojson == nil { 17 | return nil, errors.New("geojson is empty") 18 | } 19 | switch gtp := geojson.(type) { 20 | case nil: 21 | break 22 | case *geometry.Point: 23 | return callbackEachPoint(gtp, callbackFn), nil 24 | case *geometry.MultiPoint: 25 | return coordEachMultiPoint(gtp, callbackFn), nil 26 | case *geometry.LineString: 27 | return coordEachLineString(gtp, callbackFn), nil 28 | case *geometry.Polygon: 29 | if excludeWrapCoord == nil { 30 | return coordEachPolygon(gtp, false, callbackFn), nil 31 | } 32 | return coordEachPolygon(gtp, *excludeWrapCoord, callbackFn), nil 33 | case *geometry.MultiLineString: 34 | return coordEachMultiLineString(gtp, callbackFn), nil 35 | case *geometry.MultiPolygon: 36 | if excludeWrapCoord == nil { 37 | return coordEachMultiPolygon(gtp, false, callbackFn), nil 38 | } 39 | return coordEachMultiPolygon(gtp, *excludeWrapCoord, callbackFn), nil 40 | case *feature.Feature: 41 | if excludeWrapCoord == nil { 42 | return coordEachFeature(gtp, false, callbackFn) 43 | } 44 | return coordEachFeature(gtp, *excludeWrapCoord, callbackFn) 45 | case *feature.Collection: 46 | if excludeWrapCoord == nil { 47 | return coordEachFeatureCollection(gtp, false, callbackFn) 48 | } 49 | return coordEachFeatureCollection(gtp, *excludeWrapCoord, callbackFn) 50 | case *geometry.Collection: 51 | if excludeWrapCoord == nil { 52 | return coordEachGeometryCollection(gtp, false, callbackFn) 53 | } 54 | return coordEachGeometryCollection(gtp, *excludeWrapCoord, callbackFn) 55 | } 56 | 57 | return nil, nil 58 | } 59 | 60 | func callbackEachPoint(p *geometry.Point, callbackFn func(geometry.Point) geometry.Point) []geometry.Point { 61 | var coords []geometry.Point 62 | np := callbackFn(*p) 63 | // Conversion assignment 64 | p.Lat = np.Lat 65 | p.Lng = np.Lng 66 | coords = append(coords, np) 67 | return coords 68 | } 69 | 70 | func coordEachMultiPoint(m *geometry.MultiPoint, callbackFn func(geometry.Point) geometry.Point) []geometry.Point { 71 | return appendCoordsToMultiPoint([]geometry.Point{}, m, callbackFn) 72 | } 73 | 74 | func appendCoordsToMultiPoint(coords []geometry.Point, m *geometry.MultiPoint, callbackFn func(geometry.Point) geometry.Point) []geometry.Point { 75 | for _, v := range m.Coordinates { 76 | np := callbackFn(v) 77 | coords = append(coords, np) 78 | } 79 | m.Coordinates = coords 80 | return coords 81 | } 82 | 83 | func coordEachLineString(m *geometry.LineString, callbackFn func(geometry.Point) geometry.Point) []geometry.Point { 84 | return appendCoordsToLineString([]geometry.Point{}, m, callbackFn) 85 | } 86 | 87 | func appendCoordsToLineString(coords []geometry.Point, l *geometry.LineString, callbackFn func(geometry.Point) geometry.Point) []geometry.Point { 88 | for _, v := range l.Coordinates { 89 | np := callbackFn(v) 90 | coords = append(coords, np) 91 | } 92 | 93 | l.Coordinates = coords 94 | return coords 95 | } 96 | 97 | func coordEachMultiLineString(m *geometry.MultiLineString, callbackFn func(geometry.Point) geometry.Point) []geometry.Point { 98 | return appendCoordToMultiLineString([]geometry.Point{}, m, callbackFn) 99 | } 100 | 101 | func appendCoordToMultiLineString(coords []geometry.Point, m *geometry.MultiLineString, callbackFn func(geometry.Point) geometry.Point) []geometry.Point { 102 | for i := 0; i < len(m.Coordinates); i++ { 103 | for j := 0; j < len(m.Coordinates[i].Coordinates); j++ { 104 | np := callbackFn(m.Coordinates[i].Coordinates[j]) 105 | m.Coordinates[i].Coordinates[j] = np 106 | coords = append(coords, np) 107 | } 108 | 109 | } 110 | return coords 111 | } 112 | 113 | func coordEachPolygon(p *geometry.Polygon, excludeWrapCoord bool, callbackFn func(geometry.Point) geometry.Point) []geometry.Point { 114 | return appendCoordsToPolygon([]geometry.Point{}, p, excludeWrapCoord, callbackFn) 115 | } 116 | 117 | func appendCoordsToPolygon(coords []geometry.Point, p *geometry.Polygon, excludeWrapCoord bool, callbackFn func(geometry.Point) geometry.Point) []geometry.Point { 118 | wrapShrink := 0 119 | if excludeWrapCoord { 120 | wrapShrink = 1 121 | } 122 | for i := 0; i < len(p.Coordinates); i++ { 123 | for j := 0; j < len(p.Coordinates[i].Coordinates)-wrapShrink; j++ { 124 | np := callbackFn(p.Coordinates[i].Coordinates[j]) 125 | p.Coordinates[i].Coordinates[j] = np 126 | coords = append(coords, np) 127 | } 128 | } 129 | return coords 130 | } 131 | 132 | func coordEachMultiPolygon(mp *geometry.MultiPolygon, excludeWrapCoord bool, callbackFn func(geometry.Point) geometry.Point) []geometry.Point { 133 | 134 | return appendCoordToMultiPolygon([]geometry.Point{}, mp, excludeWrapCoord, callbackFn) 135 | } 136 | 137 | func appendCoordToMultiPolygon(coords []geometry.Point, mp *geometry.MultiPolygon, excludeWrapCoord bool, callbackFn func(geometry.Point) geometry.Point) []geometry.Point { 138 | wrapShrink := 0 139 | if excludeWrapCoord { 140 | wrapShrink = 1 141 | } 142 | 143 | for i := 0; i < len(mp.Coordinates); i++ { 144 | for j := 0; j < len(mp.Coordinates[i].Coordinates); j++ { 145 | for k := 0; k < len(mp.Coordinates[i].Coordinates[j].Coordinates)-wrapShrink; k++ { 146 | np := callbackFn(mp.Coordinates[i].Coordinates[j].Coordinates[k]) 147 | mp.Coordinates[i].Coordinates[j].Coordinates[k] = np 148 | coords = append(coords, np) 149 | } 150 | } 151 | } 152 | return coords 153 | } 154 | 155 | func coordEachFeature(f *feature.Feature, excludeWrapCoord bool, callbackFn func(geometry.Point) geometry.Point) ([]geometry.Point, error) { 156 | return appendCoordToFeature([]geometry.Point{}, f, excludeWrapCoord, callbackFn) 157 | } 158 | 159 | func appendCoordToFeature(pointList []geometry.Point, f *feature.Feature, excludeWrapCoord bool, callbackFn func(geometry.Point) geometry.Point) ([]geometry.Point, error) { 160 | 161 | coords, err := coordsEachFromSingleGeometry(pointList, &f.Geometry, excludeWrapCoord, callbackFn) 162 | if err != nil { 163 | return nil, err 164 | } 165 | return coords, nil 166 | } 167 | 168 | func coordsEachFromSingleGeometry(pointList []geometry.Point, g *geometry.Geometry, excludeWrapCoord bool, callbackFn func(geometry.Point) geometry.Point) ([]geometry.Point, error) { 169 | 170 | if g.GeoJSONType == geojson.Point { 171 | p, err := g.ToPoint() 172 | if err != nil { 173 | return nil, err 174 | } 175 | np := callbackFn(*p) 176 | pointList = append(pointList, np) 177 | g.Coordinates = np 178 | } 179 | 180 | if g.GeoJSONType == geojson.MultiPoint { 181 | mp, err := g.ToMultiPoint() 182 | if err != nil { 183 | return nil, err 184 | } 185 | pointList = appendCoordsToMultiPoint(pointList, mp, callbackFn) 186 | 187 | g.Coordinates = mp.Coordinates 188 | } 189 | 190 | if g.GeoJSONType == geojson.LineString { 191 | ln, err := g.ToLineString() 192 | if err != nil { 193 | return nil, err 194 | } 195 | pointList = appendCoordsToLineString(pointList, ln, callbackFn) 196 | g.Coordinates = ln.Coordinates 197 | } 198 | 199 | if g.GeoJSONType == geojson.MultiLineString { 200 | mln, err := g.ToMultiLineString() 201 | if err != nil { 202 | return nil, err 203 | } 204 | pointList = appendCoordToMultiLineString(pointList, mln, callbackFn) 205 | g.Coordinates = mln.Coordinates 206 | } 207 | 208 | if g.GeoJSONType == geojson.Polygon { 209 | poly, err := g.ToPolygon() 210 | if err != nil { 211 | return nil, err 212 | } 213 | pointList = appendCoordsToPolygon(pointList, poly, excludeWrapCoord, callbackFn) 214 | g.Coordinates = poly.Coordinates 215 | } 216 | 217 | if g.GeoJSONType == geojson.MultiPolygon { 218 | multiPoly, err := g.ToMultiPolygon() 219 | if err != nil { 220 | return nil, err 221 | } 222 | pointList = appendCoordToMultiPolygon(pointList, multiPoly, excludeWrapCoord, callbackFn) 223 | g.Coordinates = multiPoly.Coordinates 224 | } 225 | 226 | return pointList, nil 227 | } 228 | 229 | func coordEachFeatureCollection(c *feature.Collection, excludeWrapCoord bool, callbackFn func(geometry.Point) geometry.Point) ([]geometry.Point, error) { 230 | var finalCoordsList []geometry.Point 231 | 232 | for i := 0; i < len(c.Features); i++ { 233 | var tempCoordsList []geometry.Point 234 | tempCoordsList, _ = appendCoordToFeature(tempCoordsList, &c.Features[i], excludeWrapCoord, callbackFn) 235 | finalCoordsList = append(finalCoordsList, tempCoordsList...) 236 | } 237 | 238 | return finalCoordsList, nil 239 | } 240 | 241 | func coordEachGeometryCollection(g *geometry.Collection, excludeWrapCoord bool, callbackFn func(geometry.Point) geometry.Point) ([]geometry.Point, error) { 242 | var finalCoordsList []geometry.Point 243 | 244 | for i := 0; i < len(g.Geometries); i++ { 245 | var tempCoordsList []geometry.Point 246 | tempCoordsList, _ = coordsEachFromSingleGeometry(tempCoordsList, &g.Geometries[i], excludeWrapCoord, callbackFn) 247 | finalCoordsList = append(finalCoordsList, tempCoordsList...) 248 | } 249 | 250 | return finalCoordsList, nil 251 | } 252 | -------------------------------------------------------------------------------- /meta/coordEach/coordEach_test.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tomchavakis/geojson/feature" 7 | "github.com/tomchavakis/geojson/geometry" 8 | "github.com/tomchavakis/turf-go/assert" 9 | ) 10 | 11 | func TestCoordEachPoint(t *testing.T) { 12 | json := "{ \"type\": \"Point\", \"coordinates\": [23.0, 54.0]}" 13 | geom, err := geometry.FromJSON(json) 14 | if err != nil { 15 | t.Errorf("geometry error %v", err) 16 | } 17 | 18 | pt, err := geom.ToPoint() 19 | if err != nil { 20 | t.Errorf("convert to Point error %v", err) 21 | } 22 | 23 | fnc := func(p geometry.Point) geometry.Point { 24 | return p 25 | } 26 | 27 | pts, err := CoordEach(pt, fnc, nil) 28 | 29 | if err != nil { 30 | t.Errorf("CoordEach err %v", err) 31 | } 32 | 33 | assert.Equal(t, len(pts), 1) 34 | assert.Equal(t, pts[0].Lat, 54.0) 35 | assert.Equal(t, pts[0].Lng, 23.0) 36 | } 37 | 38 | func TestCoordEachMultiPoint(t *testing.T) { 39 | json := "{ \"type\": \"MultiPoint\", \"coordinates\": [[102, -10],[103, 1],[104, 0],[130, 4]]}" 40 | geom, err := geometry.FromJSON(json) 41 | if err != nil { 42 | t.Errorf("geometry error %v", err) 43 | } 44 | 45 | pt, err := geom.ToMultiPoint() 46 | if err != nil { 47 | t.Errorf("convert to MultiPoint error %v", err) 48 | } 49 | 50 | fnc := func(p geometry.Point) geometry.Point { 51 | return p 52 | } 53 | 54 | pts, err := CoordEach(pt, fnc, nil) 55 | 56 | if err != nil { 57 | t.Errorf("CoordEach err %v", err) 58 | } 59 | 60 | assert.Equal(t, len(pts), 4) 61 | assert.Equal(t, pts[0].Lat, -10.0) 62 | assert.Equal(t, pts[0].Lng, 102.0) 63 | 64 | assert.Equal(t, pts[1].Lat, 1.0) 65 | assert.Equal(t, pts[1].Lng, 103.0) 66 | 67 | assert.Equal(t, pts[2].Lat, 0.0) 68 | assert.Equal(t, pts[2].Lng, 104.0) 69 | 70 | assert.Equal(t, pts[3].Lat, 4.0) 71 | assert.Equal(t, pts[3].Lng, 130.0) 72 | } 73 | 74 | func TestCoordEachLineString(t *testing.T) { 75 | json := "{ \"type\": \"LineString\", \"coordinates\": [[102, -10],[103, 1],[104, 0],[130, 4]]}" 76 | geom, err := geometry.FromJSON(json) 77 | if err != nil { 78 | t.Errorf("geometry error %v", err) 79 | } 80 | 81 | pt, err := geom.ToLineString() 82 | if err != nil { 83 | t.Errorf("convert to LineString error %v", err) 84 | } 85 | 86 | fnc := func(p geometry.Point) geometry.Point { 87 | return p 88 | } 89 | 90 | pts, err := CoordEach(pt, fnc, nil) 91 | 92 | if err != nil { 93 | t.Errorf("CoordEach err %v", err) 94 | } 95 | 96 | assert.Equal(t, len(pts), 4) 97 | assert.Equal(t, pts[0].Lat, -10.0) 98 | assert.Equal(t, pts[0].Lng, 102.0) 99 | 100 | assert.Equal(t, pts[1].Lat, 1.0) 101 | assert.Equal(t, pts[1].Lng, 103.0) 102 | 103 | assert.Equal(t, pts[2].Lat, 0.0) 104 | assert.Equal(t, pts[2].Lng, 104.0) 105 | 106 | assert.Equal(t, pts[3].Lat, 4.0) 107 | assert.Equal(t, pts[3].Lng, 130.0) 108 | } 109 | 110 | func TestCoordEachMultiLineString(t *testing.T) { 111 | json := "{ \"type\": \"MultiLineString\", \"coordinates\": [[[100, 0],[101, 1]],[[102, 2],[103, 3]]]}" 112 | geom, err := geometry.FromJSON(json) 113 | if err != nil { 114 | t.Errorf("geometry error %v", err) 115 | } 116 | 117 | pt, err := geom.ToMultiLineString() 118 | if err != nil { 119 | t.Errorf("convert to MultiLineString error %v", err) 120 | } 121 | 122 | fnc := func(p geometry.Point) geometry.Point { 123 | p.Lat = p.Lat + 1 124 | p.Lng = p.Lng + 1 125 | return p 126 | } 127 | 128 | pts, err := CoordEach(pt, fnc, nil) 129 | 130 | if err != nil { 131 | t.Errorf("CoordEach err %v", err) 132 | } 133 | 134 | assert.Equal(t, len(pts), 4) 135 | assert.Equal(t, pts[0].Lat, 1.0) 136 | assert.Equal(t, pts[0].Lng, 101.0) 137 | 138 | assert.Equal(t, pts[1].Lat, 2.0) 139 | assert.Equal(t, pts[1].Lng, 102.0) 140 | 141 | assert.Equal(t, pts[2].Lat, 3.0) 142 | assert.Equal(t, pts[2].Lng, 103.0) 143 | 144 | assert.Equal(t, pts[3].Lat, 4.0) 145 | assert.Equal(t, pts[3].Lng, 104.0) 146 | } 147 | 148 | func TestCoordEachPolygon(t *testing.T) { 149 | json := "{ \"type\": \"Polygon\", \"coordinates\": [[[125.0,-15.0],[113.0,-22.0],[117.0,-37.0],[130.0,-33.0],[148.0,-39.0],[154.0,-27.0],[144.0,-15.0],[125.0,-15.0]]]}" 150 | geom, err := geometry.FromJSON(json) 151 | if err != nil { 152 | t.Errorf("geometry error %v", err) 153 | } 154 | 155 | poly, err := geom.ToPolygon() 156 | if err != nil { 157 | t.Errorf("convert to Polygon error %v", err) 158 | } 159 | 160 | fnc := func(p geometry.Point) geometry.Point { 161 | return p 162 | } 163 | extractWrapPolygon := false 164 | pts, err := CoordEach(poly, fnc, &extractWrapPolygon) 165 | 166 | if err != nil { 167 | t.Errorf("CoordEach err %v", err) 168 | } 169 | 170 | assert.Equal(t, len(pts), 8) 171 | assert.Equal(t, pts[0].Lat, -15.0) 172 | assert.Equal(t, pts[0].Lng, 125.0) 173 | 174 | assert.Equal(t, pts[1].Lat, -22.0) 175 | assert.Equal(t, pts[1].Lng, 113.0) 176 | 177 | assert.Equal(t, pts[2].Lat, -37.0) 178 | assert.Equal(t, pts[2].Lng, 117.0) 179 | 180 | assert.Equal(t, pts[3].Lat, -33.0) 181 | assert.Equal(t, pts[3].Lng, 130.0) 182 | 183 | assert.Equal(t, pts[4].Lat, -39.0) 184 | assert.Equal(t, pts[4].Lng, 148.0) 185 | 186 | assert.Equal(t, pts[5].Lat, -27.0) 187 | assert.Equal(t, pts[5].Lng, 154.0) 188 | 189 | assert.Equal(t, pts[6].Lat, -15.0) 190 | assert.Equal(t, pts[6].Lng, 144.0) 191 | 192 | assert.Equal(t, pts[7].Lat, -15.0) 193 | assert.Equal(t, pts[7].Lng, 125.0) 194 | } 195 | 196 | func TestCoordEachPolygonExcludeWrapCoord(t *testing.T) { 197 | json := "{ \"type\": \"Polygon\", \"coordinates\": [[[125.0,-15.0],[113.0,-22.0],[117.0,-37.0],[130.0,-33.0],[148.0,-39.0],[154.0,-27.0],[144.0,-15.0],[125.0,-15.0]]]}" 198 | geom, err := geometry.FromJSON(json) 199 | if err != nil { 200 | t.Errorf("geometry error %v", err) 201 | } 202 | 203 | poly, err := geom.ToPolygon() 204 | if err != nil { 205 | t.Errorf("convert to Polygon error %v", err) 206 | } 207 | 208 | fnc := func(p geometry.Point) geometry.Point { 209 | return p 210 | } 211 | extractWrapPolygon := true 212 | pts, err := CoordEach(poly, fnc, &extractWrapPolygon) 213 | 214 | if err != nil { 215 | t.Errorf("CoordAll err %v", err) 216 | } 217 | 218 | assert.Equal(t, len(pts), 7) 219 | assert.Equal(t, pts[0].Lat, -15.0) 220 | assert.Equal(t, pts[0].Lng, 125.0) 221 | 222 | assert.Equal(t, pts[1].Lat, -22.0) 223 | assert.Equal(t, pts[1].Lng, 113.0) 224 | 225 | assert.Equal(t, pts[2].Lat, -37.0) 226 | assert.Equal(t, pts[2].Lng, 117.0) 227 | 228 | assert.Equal(t, pts[3].Lat, -33.0) 229 | assert.Equal(t, pts[3].Lng, 130.0) 230 | 231 | assert.Equal(t, pts[4].Lat, -39.0) 232 | assert.Equal(t, pts[4].Lng, 148.0) 233 | 234 | assert.Equal(t, pts[5].Lat, -27.0) 235 | assert.Equal(t, pts[5].Lng, 154.0) 236 | 237 | assert.Equal(t, pts[6].Lat, -15.0) 238 | assert.Equal(t, pts[6].Lng, 144.0) 239 | 240 | } 241 | 242 | func TestCoordEachMultiPolygon(t *testing.T) { 243 | json := "{ \"type\": \"MultiPolygon\", \"coordinates\": [[[[102, 2],[103, 2],[103, 3],[102, 3],[102, 2]]],[[[100, 0],[101, 0],[101, 1],[100, 1],[100, 0]]]]}" 244 | geom, err := geometry.FromJSON(json) 245 | if err != nil { 246 | t.Errorf("geometry error %v", err) 247 | } 248 | 249 | poly, err := geom.ToMultiPolygon() 250 | if err != nil { 251 | t.Errorf("convert to MultiPolygon error %v", err) 252 | } 253 | 254 | fnc := func(p geometry.Point) geometry.Point { 255 | return p 256 | } 257 | extractWrapPolygon := false 258 | pts, err := CoordEach(poly, fnc, &extractWrapPolygon) 259 | 260 | if err != nil { 261 | t.Errorf("CoordAll err %v", err) 262 | } 263 | 264 | assert.Equal(t, len(pts), 10) 265 | assert.Equal(t, pts[0].Lat, 2.0) 266 | assert.Equal(t, pts[0].Lng, 102.0) 267 | 268 | assert.Equal(t, pts[1].Lat, 2.0) 269 | assert.Equal(t, pts[1].Lng, 103.0) 270 | 271 | assert.Equal(t, pts[2].Lat, 3.0) 272 | assert.Equal(t, pts[2].Lng, 103.0) 273 | 274 | assert.Equal(t, pts[3].Lat, 3.0) 275 | assert.Equal(t, pts[3].Lng, 102.0) 276 | 277 | assert.Equal(t, pts[4].Lat, 2.0) 278 | assert.Equal(t, pts[4].Lng, 102.0) 279 | 280 | assert.Equal(t, pts[5].Lat, 0.0) 281 | assert.Equal(t, pts[5].Lng, 100.0) 282 | 283 | assert.Equal(t, pts[6].Lat, 0.0) 284 | assert.Equal(t, pts[6].Lng, 101.0) 285 | 286 | assert.Equal(t, pts[7].Lat, 1.0) 287 | assert.Equal(t, pts[7].Lng, 101.0) 288 | 289 | assert.Equal(t, pts[8].Lat, 1.0) 290 | assert.Equal(t, pts[8].Lng, 100.0) 291 | 292 | assert.Equal(t, pts[9].Lat, 0.0) 293 | assert.Equal(t, pts[9].Lng, 100.0) 294 | } 295 | 296 | func TestCoordAllFeatureCollection(t *testing.T) { 297 | json := "{\"type\": \"FeatureCollection\", \"features\": [{\"type\": \"Feature\",\"properties\": {\"population\": 200},\"geometry\": {\"type\": \"Point\",\"coordinates\": [-112.0372, 46.608058]}}]}" 298 | c, err := feature.CollectionFromJSON(json) 299 | if err != nil { 300 | t.Errorf("CollectionFromJSON error %v", err) 301 | } 302 | 303 | if c == nil { 304 | t.Error("feature collection can't be nil") 305 | } 306 | 307 | fnc := func(p geometry.Point) geometry.Point { 308 | p.Lat = p.Lat + 0.1 309 | p.Lng = p.Lng + 0.1 310 | return p 311 | } 312 | 313 | exclude := true 314 | pts, err := CoordEach(c, fnc, &exclude) 315 | if err != nil { 316 | t.Errorf("CoordEach error %v", err) 317 | } 318 | 319 | assert.Equal(t, len(pts), 1) 320 | assert.Equal(t, pts[0].Lat, 46.708058) 321 | assert.Equal(t, pts[0].Lng, -111.9372) 322 | } 323 | -------------------------------------------------------------------------------- /projection/projection.go: -------------------------------------------------------------------------------- 1 | package projection 2 | 3 | import ( 4 | "errors" 5 | "math" 6 | 7 | "github.com/tomchavakis/geojson/geometry" 8 | meta "github.com/tomchavakis/turf-go/meta/coordEach" 9 | ) 10 | 11 | const a = 6378137.0 12 | 13 | // ToMercator converts a WGS84 GeoJSON object into Mercator (EPSG:900913) projection 14 | func ToMercator(geojson interface{}) (interface{}, error) { 15 | return Convert(geojson, "mercator") 16 | } 17 | 18 | // ToWgs84 Converts a Mercator (EPSG:900913) GeoJSON object into WGS84 projection 19 | func ToWgs84(geojson interface{}) (interface{}, error) { 20 | return Convert(geojson, "wgs84") 21 | } 22 | 23 | // ConvertToMercator converts a WGS84 lonlat to Mercator (EPSG:3857 sometimes knows as EPSG:900913) projection 24 | // https://spatialreference.org/ref/sr-org/epsg3857-wgs84-web-mercator-auxiliary-sphere/ 25 | func ConvertToMercator(lonlat []float64) []float64 { 26 | rad := math.Pi / 180.0 27 | maxExtend := 2 * math.Pi * a / 2.0 // 20037508.342789244 28 | 29 | // longitudes passing the 180th meridian 30 | var adjusted float64 31 | if math.Abs(lonlat[0]) <= 180.0 { 32 | adjusted = lonlat[0] 33 | } else { 34 | adjusted = lonlat[0] - float64(sign(lonlat[0]))*360.0 35 | } 36 | 37 | xy := []float64{ 38 | a * adjusted * rad, 39 | a * math.Log(math.Tan(math.Pi*0.25+0.5*lonlat[1]*rad)), 40 | } 41 | 42 | if xy[0] > maxExtend { 43 | xy[0] = maxExtend 44 | } 45 | if xy[0] < -maxExtend { 46 | xy[0] = -maxExtend 47 | } 48 | if xy[1] > maxExtend { 49 | xy[1] = maxExtend 50 | } 51 | if xy[1] < -maxExtend { 52 | xy[1] = -maxExtend 53 | } 54 | return xy 55 | } 56 | 57 | // ConvertToWgs84 convert 900913 x/y values to lon/lat 58 | func ConvertToWgs84(xy []float64) []float64 { 59 | dgs := 180.0 / math.Pi 60 | 61 | return []float64{ 62 | xy[0] * dgs / a, 63 | (math.Pi*0.5 - 2.0*math.Atan(math.Exp(-xy[1]/a))) * dgs, 64 | } 65 | } 66 | 67 | // Convert converts a GeoJSON coordinates to the defined projection 68 | // gjson is GeoJSON Feature or geometry 69 | // projection defines the projection system to convert the coordinates to 70 | func Convert(geojson interface{}, projection string) (interface{}, error) { 71 | // Validation 72 | if geojson == nil { 73 | return nil, errors.New("geojson is required") 74 | } 75 | 76 | _, err := meta.CoordEach(geojson, func(p geometry.Point) geometry.Point { 77 | if projection == "mercator" { 78 | res := ConvertToMercator([]float64{p.Lng, p.Lat}) 79 | p.Lng = res[0] 80 | p.Lat = res[1] 81 | } else { 82 | res := ConvertToWgs84([]float64{p.Lng, p.Lat}) 83 | p.Lng = res[0] 84 | p.Lat = res[1] 85 | } 86 | 87 | return p 88 | }, nil) 89 | 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | return geojson, nil 95 | } 96 | 97 | func sign(x float64) int { 98 | if x < 0 { 99 | return -1 100 | } else if x > 0 { 101 | return 1 102 | } 103 | return 0 104 | } 105 | -------------------------------------------------------------------------------- /random/random.go: -------------------------------------------------------------------------------- 1 | package random 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math" 7 | "math/rand" 8 | 9 | "github.com/tomchavakis/geojson" 10 | "github.com/tomchavakis/geojson/feature" 11 | "github.com/tomchavakis/geojson/geometry" 12 | "github.com/tomchavakis/turf-go/internal/common" 13 | ) 14 | 15 | // LineStringOptions ... 16 | type LineStringOptions struct { 17 | // BBox is the bounding box of which geometries are placed. 18 | BBox geojson.BBOX 19 | // NumVertices is how many coordinates each LineString will contain. 10 is the default value 20 | NumVertices *int 21 | // MaxLength is the maximum number of decimal degrees that a vertex can be from its predecessor. 0.0001 is the default value 22 | MaxLength *float64 23 | // MaxRotation is the maximum number of radians that a line segment can turn from the previous segment. math.Pi / 8 is the default value 24 | MaxRotation *float64 25 | } 26 | 27 | // PolygonOptions ... 28 | type PolygonOptions struct { 29 | // BBox is the bounding box of which geometries are placed. 30 | BBox geojson.BBOX 31 | // NumVertices is how many coordinates each Polygon will contain. 10 is the default value 32 | NumVertices *int 33 | // MaxRadialLength is the maximum number of decimal degrees latitude or longitude that a vertex can reach out of the center of a Polygon. 34 | MaxRadialLength *float64 35 | } 36 | 37 | // Position returns a random position within a bounding box 38 | // 39 | // Examples: 40 | // 41 | // random.Position( 42 | // geojson.BBOX{ 43 | // West: -180, 44 | // South: -90, 45 | // East: 180, 46 | // North: 90, 47 | // }) 48 | func Position(bbox geojson.BBOX) geometry.Position { 49 | pos := coordInBBox(bbox) 50 | res := geometry.NewPosition(nil, pos[0], pos[1]) 51 | return *res 52 | } 53 | 54 | // Point returns a GeoJSON FeatureCollection of random Point within a bounding box. 55 | // 56 | // count is how many geometries will be generated. default = 1 57 | // 58 | // Examples: 59 | // 60 | // random.Point(0, 61 | // geojson.BBOX{ 62 | // West: -180, 63 | // South: -90, 64 | // East: 180, 65 | // North: 90, 66 | // }) 67 | // 68 | // random.Point(10, 69 | // geojson.BBOX{ 70 | // West: -180, 71 | // South: -90, 72 | // East: 180, 73 | // North: 90, 74 | // }) 75 | func Point(count int, bbox geojson.BBOX) (*feature.Collection, error) { 76 | if count == 0 { 77 | count = 1 78 | } 79 | fc := []feature.Feature{} 80 | 81 | for i := 0; i < count; i++ { 82 | p := coordInBBox(bbox) 83 | coords := []float64{p[0], p[1]} 84 | g := geometry.Geometry{ 85 | GeoJSONType: geojson.Point, 86 | Coordinates: coords, 87 | } 88 | f, err := feature.New(g, []float64{bbox.North, bbox.West, bbox.East, bbox.South}, nil, "") 89 | if err != nil { 90 | return nil, fmt.Errorf("cannot create a new Feature with error: %v", err) 91 | } 92 | fc = append(fc, *f) 93 | } 94 | 95 | if len(fc) > 0 { 96 | return feature.NewFeatureCollection(fc) 97 | } 98 | 99 | return nil, errors.New("can't generate a random point") 100 | } 101 | 102 | // LineString returns a GeoJSON FeatureCollection of random LineString within a bounding box. 103 | // 104 | // count=1 how many geometries will be generated 105 | // 106 | // Examples: 107 | // 108 | // random.LineString(10,LineStringOptions{ 109 | // BBox:geojson.BBOX{ 110 | // West: -180, 111 | // South: -90, 112 | // East: 180, 113 | // North: 90, 114 | // }, 115 | // NumVertices: nil, 116 | // MaxLength: nil, 117 | // MaxRotation: nil, 118 | // }) 119 | func LineString(count int, options LineStringOptions) (*feature.Collection, error) { 120 | if count == 0 { 121 | count = 1 122 | } 123 | 124 | if options.NumVertices == nil || *options.NumVertices < 2 { 125 | options.NumVertices = common.IntPtr(10) 126 | } 127 | 128 | if options.MaxLength == nil { 129 | options.MaxLength = common.Float64Ptr(0.0001) 130 | } 131 | 132 | if options.MaxRotation == nil { 133 | options.MaxRotation = common.Float64Ptr(math.Pi / 8.0) 134 | } 135 | 136 | fc := []feature.Feature{} 137 | 138 | for i := 0; i < count; i++ { 139 | 140 | startingPoint := Position(options.BBox) 141 | vertices := [][]float64{} 142 | vertices = append(vertices, []float64{startingPoint.ToPoint().Lng, startingPoint.ToPoint().Lat}) 143 | 144 | for j := 0; j < *options.NumVertices-1; j++ { 145 | var priorAngle float64 146 | if j == 0 { 147 | priorAngle = rand.Float64() * 2 * math.Pi 148 | } else { 149 | priorAngle = math.Tan((vertices[j][1] - vertices[j-1][1]) / (vertices[j][0] - vertices[j-1][0])) 150 | } 151 | angle := priorAngle + (rand.Float64()-0.5)*(*options.MaxRotation)*2 152 | distance := rand.Float64() * (*options.MaxLength) 153 | vv := []float64{vertices[j][0] + distance*math.Cos(angle), vertices[j][1] + distance*math.Sin(angle)} 154 | vertices = append(vertices, vv) 155 | } 156 | 157 | g := geometry.Geometry{ 158 | GeoJSONType: geojson.LineString, 159 | Coordinates: vertices, 160 | } 161 | f, err := feature.New(g, []float64{options.BBox.North, options.BBox.West, options.BBox.East, options.BBox.South}, nil, "") 162 | if err != nil { 163 | return nil, fmt.Errorf("cannot create a new Feature with error: %v", err) 164 | } 165 | fc = append(fc, *f) 166 | } 167 | 168 | if len(fc) > 0 { 169 | return feature.NewFeatureCollection(fc) 170 | } 171 | 172 | return nil, errors.New("can't generate a random LineString") 173 | } 174 | 175 | // Polygon returns a GeoJSON FeatureCollection of random Polygon. 176 | // 177 | // count=1 how many geometries will be generated 178 | // 179 | // Examples: 180 | // 181 | // random.Polygon(10, PolygonOptions{ 182 | // BBox:geojson.BBOX{ 183 | // West: -180, 184 | // South: -90, 185 | // East: 180, 186 | // North: 90, 187 | // }, 188 | // NumVertices: nil, 189 | // MaxRadialLength: nil, 190 | // }) 191 | func Polygon(count int, options PolygonOptions) (*feature.Collection, error) { 192 | 193 | if count == 0 { 194 | count = 1 195 | } 196 | 197 | if options.NumVertices == nil { 198 | options.NumVertices = common.IntPtr(10) 199 | } 200 | 201 | if options.MaxRadialLength == nil { 202 | options.MaxRadialLength = common.Float64Ptr(10.0) 203 | } 204 | 205 | fc := []feature.Feature{} 206 | 207 | for i := 0; i < count; i++ { 208 | 209 | vts := [][][]float64{} 210 | vertices := [][]float64{} 211 | circleOffsets := make([]float64, *options.NumVertices+1) 212 | 213 | // sum Offsets 214 | for i := 0; i < len(circleOffsets); i++ { 215 | if i == 0 { 216 | circleOffsets[i] = rand.Float64() 217 | } else { 218 | circleOffsets[i] = rand.Float64() + circleOffsets[i-1] 219 | } 220 | } 221 | 222 | // scale offsets 223 | for j := 0; j < len(circleOffsets); j++ { 224 | cur := (circleOffsets[j] * 2 * math.Pi) / circleOffsets[len(circleOffsets)-1] 225 | radialScaler := rand.Float64() 226 | vertices = append(vertices, []float64{radialScaler * (*options.MaxRadialLength) * math.Sin(cur), radialScaler * (*options.MaxRadialLength) * math.Cos(cur)}) 227 | } 228 | 229 | // close the ring 230 | vertices[len(vertices)-1] = vertices[0] 231 | 232 | // center the polygon around something 233 | res := vertexToCoordinate(vertices, options.BBox) 234 | vts = append(vts, res) 235 | 236 | g := geometry.Geometry{ 237 | GeoJSONType: geojson.Polygon, 238 | Coordinates: vts, 239 | } 240 | f, err := feature.New(g, []float64{options.BBox.North, options.BBox.West, options.BBox.East, options.BBox.South}, nil, "") 241 | if err != nil { 242 | return nil, fmt.Errorf("cannot create a new Feature with error: %v", err) 243 | } 244 | fc = append(fc, *f) 245 | } 246 | 247 | if len(fc) > 0 { 248 | return feature.NewFeatureCollection(fc) 249 | } 250 | 251 | return nil, errors.New("can't generate a random LineString") 252 | } 253 | 254 | func vertexToCoordinate(vtc [][]float64, bbox geojson.BBOX) [][]float64 { 255 | res := [][]float64{} 256 | p := Position(bbox) 257 | for i := 0; i < len(vtc); i++ { 258 | tmp := []float64{} 259 | tmp = append(tmp, vtc[i][0]+p.Longitude, vtc[i][1]+p.Latitude) 260 | res = append(res, tmp) 261 | } 262 | return res 263 | } 264 | 265 | func coordInBBox(bbox geojson.BBOX) []float64 { 266 | res := make([]float64, 2) 267 | res[0] = rand.Float64()*(bbox.East-bbox.South) + bbox.South 268 | res[1] = rand.Float64()*(bbox.West-bbox.North) + bbox.North 269 | 270 | return res 271 | } 272 | -------------------------------------------------------------------------------- /random/random_test.go: -------------------------------------------------------------------------------- 1 | package random 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tomchavakis/geojson" 7 | "github.com/tomchavakis/geojson/feature" 8 | "github.com/tomchavakis/turf-go" 9 | "github.com/tomchavakis/turf-go/assert" 10 | "github.com/tomchavakis/turf-go/internal/common" 11 | ) 12 | 13 | func TestRandomPosition(t *testing.T) { 14 | bbox := geojson.NewBBox(-10.0, -10.0, 10.0, 10.0) 15 | rndPos := Position(*bbox) 16 | assert.True(t, turf.InBBOX(rndPos.ToPoint(), *bbox), "point is not within the Bounding Box") 17 | } 18 | 19 | func TestRandomPoint0(t *testing.T) { 20 | fc, err := Point(0, *geojson.NewBBox(0, 0, 0, 0)) 21 | assert.Nil(t, err) 22 | assert.Equal(t, string(fc.Type), "FeatureCollection") 23 | assert.NotNil(t, fc.Features) 24 | assert.Equal(t, len(fc.Features), 1) 25 | assert.Equal(t, string(fc.Features[0].Geometry.GeoJSONType), "Point") 26 | assert.Equal(t, fc.Features[0].Geometry.Coordinates, interface{}([]float64{0.0, 0.0})) 27 | } 28 | 29 | func TestRandomPoint(t *testing.T) { 30 | fc, err := Point(10, *geojson.NewBBox(0, 0, 0, 0)) 31 | assert.Nil(t, err) 32 | assert.Equal(t, string(fc.Type), "FeatureCollection") 33 | assert.NotNil(t, fc.Features) 34 | assert.Equal(t, len(fc.Features), 10) 35 | assert.Equal(t, string(fc.Features[0].Geometry.GeoJSONType), "Point") 36 | assert.Equal(t, fc.Features[0].Geometry.Coordinates, interface{}([]float64{0.0, 0.0})) 37 | } 38 | 39 | func TestRandomPointInBBox(t *testing.T) { 40 | bbox := geojson.NewBBox(-10.0, -10.0, 10.0, 10.0) 41 | fc, err := Point(10, *bbox) 42 | assert.Nil(t, err) 43 | assert.Equal(t, string(fc.Type), "FeatureCollection") 44 | assert.NotNil(t, fc.Features) 45 | assert.Equal(t, len(fc.Features), 10) 46 | assert.Equal(t, string(fc.Features[0].Geometry.GeoJSONType), "Point") 47 | p, err := fc.Features[0].Geometry.ToPoint() 48 | assert.Nil(t, err) 49 | assert.True(t, turf.InBBOX(*p, *bbox)) 50 | } 51 | 52 | func TestRandomLineString(t *testing.T) { 53 | bbox := geojson.NewBBox(-10.0, -10.0, 10.0, 10.0) 54 | options := LineStringOptions{ 55 | BBox: *bbox, 56 | NumVertices: nil, 57 | MaxLength: nil, 58 | MaxRotation: nil, 59 | } 60 | fc, err := LineString(10, options) 61 | assert.Nil(t, err) 62 | assert.Equal(t, string(fc.Type), "FeatureCollection") 63 | assert.NotNil(t, fc.Features) 64 | assert.Equal(t, len(fc.Features), 10) 65 | assert.Equal(t, string(fc.Features[0].Geometry.GeoJSONType), "LineString") 66 | checkFeaturesInBBox(t, bbox, fc.Features) 67 | } 68 | 69 | func TestRandomLineString_Vertex_1(t *testing.T) { 70 | bbox := geojson.NewBBox(-10.0, -10.0, 10.0, 10.0) 71 | options := LineStringOptions{ 72 | BBox: *bbox, 73 | NumVertices: common.IntPtr(1), 74 | MaxLength: nil, 75 | MaxRotation: nil, 76 | } 77 | fc, err := LineString(10, options) 78 | assert.Nil(t, err) 79 | assert.Equal(t, string(fc.Type), "FeatureCollection") 80 | assert.NotNil(t, fc.Features) 81 | assert.Equal(t, len(fc.Features), 10) 82 | assert.Equal(t, string(fc.Features[0].Geometry.GeoJSONType), "LineString") 83 | checkFeaturesInBBox(t, bbox, fc.Features) 84 | } 85 | 86 | func TestRandomLineString_Count_0(t *testing.T) { 87 | bbox := geojson.NewBBox(-10.0, -10.0, 10.0, 10.0) 88 | options := LineStringOptions{ 89 | BBox: *bbox, 90 | NumVertices: common.IntPtr(1), 91 | MaxLength: nil, 92 | MaxRotation: nil, 93 | } 94 | fc, err := LineString(0, options) 95 | assert.Nil(t, err) 96 | assert.Equal(t, string(fc.Type), "FeatureCollection") 97 | assert.NotNil(t, fc.Features) 98 | assert.Equal(t, len(fc.Features), 1) 99 | assert.Equal(t, string(fc.Features[0].Geometry.GeoJSONType), "LineString") 100 | checkFeaturesInBBox(t, bbox, fc.Features) 101 | } 102 | 103 | func TestRandomPolygon(t *testing.T) { 104 | bbox := geojson.NewBBox(-20.0, -20.0, 20.0, 20.0) 105 | options := PolygonOptions{ 106 | BBox: *bbox, 107 | NumVertices: nil, 108 | MaxRadialLength: nil, 109 | } 110 | fc, err := Polygon(1, options) 111 | assert.Nil(t, err) 112 | assert.Equal(t, string(fc.Type), "FeatureCollection") 113 | assert.NotNil(t, fc.Features) 114 | assert.Equal(t, len(fc.Features), 1) 115 | assert.Equal(t, string(fc.Features[0].Geometry.GeoJSONType), "Polygon") 116 | } 117 | 118 | func TestRandomPolygon_Features_ShouldBe_10(t *testing.T) { 119 | bbox := geojson.NewBBox(-20.0, -20.0, 20.0, 20.0) 120 | options := PolygonOptions{ 121 | BBox: *bbox, 122 | NumVertices: nil, 123 | MaxRadialLength: nil, 124 | } 125 | fc, err := Polygon(10, options) 126 | assert.Nil(t, err) 127 | assert.Equal(t, string(fc.Type), "FeatureCollection") 128 | assert.NotNil(t, fc.Features) 129 | assert.Equal(t, len(fc.Features), 10) 130 | assert.Equal(t, string(fc.Features[0].Geometry.GeoJSONType), "Polygon") 131 | } 132 | 133 | func TestRandomPolygon_Vertices_ShouldBe_NumVerticesPlusOne(t *testing.T) { 134 | bbox := geojson.NewBBox(-20.0, -20.0, 20.0, 20.0) 135 | options := PolygonOptions{ 136 | BBox: *bbox, 137 | NumVertices: common.IntPtr(20), 138 | MaxRadialLength: nil, 139 | } 140 | fc, err := Polygon(10, options) 141 | assert.Nil(t, err) 142 | assert.Equal(t, string(fc.Type), "FeatureCollection") 143 | assert.NotNil(t, fc.Features) 144 | assert.Equal(t, len(fc.Features), 10) 145 | assert.Equal(t, string(fc.Features[0].Geometry.GeoJSONType), "Polygon") 146 | poly, err := fc.Features[0].Geometry.ToPolygon() 147 | assert.Nil(t, err) 148 | assert.Equal(t, len(poly.Coordinates[0].Coordinates), *options.NumVertices+1) 149 | } 150 | 151 | func checkFeaturesInBBox(t *testing.T, bbox *geojson.BBOX, fc []feature.Feature) { 152 | for i := 0; i < len(fc); i++ { 153 | ln, err := fc[i].Geometry.ToLineString() 154 | assert.Nil(t, err) 155 | for _, v := range ln.Coordinates { 156 | assert.True(t, turf.InBBOX(v, *bbox)) 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /test-data/along-dc-line.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "LineString", 6 | "coordinates": [ 7 | [-77.0316696166992, 38.878605901789236], 8 | [-77.02960968017578, 38.88194668656296], 9 | [-77.02033996582031, 38.88408470638821], 10 | [-77.02566146850586, 38.885821800123196], 11 | [-77.02188491821289, 38.88956308852534], 12 | [-77.01982498168944, 38.89236892551996], 13 | [-77.02291488647461, 38.89370499941828], 14 | [-77.02291488647461, 38.89958342598271], 15 | [-77.01896667480469, 38.90011780426885], 16 | [-77.01845169067383, 38.90733151751689], 17 | [-77.02291488647461, 38.907865837489105], 18 | [-77.02377319335936, 38.91200668090932], 19 | [-77.02995300292969, 38.91254096569048], 20 | [-77.03338623046875, 38.91708222394378], 21 | [-77.03784942626953, 38.920821865485834], 22 | [-77.03115463256836, 38.92830055730587], 23 | [-77.03596115112305, 38.931505469602044] 24 | ] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test-data/area-feature-collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "properties": {}, 7 | "geometry": { 8 | "type": "Polygon", 9 | "coordinates": [ 10 | [ 11 | [-2.109375, 47.040182144806664], 12 | [4.5703125, 44.59046718130883], 13 | [7.03125, 49.15296965617042], 14 | [-3.515625, 49.83798245308484], 15 | [-2.109375, 47.040182144806664] 16 | ] 17 | ] 18 | } 19 | }, 20 | { 21 | "type": "Feature", 22 | "properties": {}, 23 | "geometry": { 24 | "type": "Polygon", 25 | "coordinates": [ 26 | [ 27 | [9.64599609375, 47.70976154266637], 28 | [9.4482421875, 47.73932336136857], 29 | [8.8330078125, 47.47266286861342], 30 | [10.21728515625, 46.604167162931844], 31 | [11.755371093749998, 46.81509864599243], 32 | [11.865234375, 47.90161354142077], 33 | [9.64599609375, 47.70976154266637] 34 | ] 35 | ] 36 | } 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /test-data/area-geom-multipolgon.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "MultiPolygon", 3 | "coordinates": [ 4 | [ 5 | [ 6 | [102, 2], 7 | [103, 2], 8 | [103, 3], 9 | [102, 3], 10 | [102, 2] 11 | ] 12 | ], 13 | [ 14 | [ 15 | [100, 0], 16 | [101, 0], 17 | [101, 1], 18 | [100, 1], 19 | [100, 0] 20 | ] 21 | ] 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /test-data/area-geom-polygon.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Polygon", 3 | "coordinates": [ 4 | [ 5 | [-2.275543, 53.464547], 6 | [-2.275543, 53.489271], 7 | [-2.215118, 53.489271], 8 | [-2.215118, 53.464547], 9 | [-2.275543, 53.464547] 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test-data/area-multipolygon.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": { 4 | "stroke": "#F00", 5 | "stroke-width": 6 6 | }, 7 | "geometry": { 8 | "type": "MultiPolygon", 9 | "coordinates": [ 10 | [ 11 | [ 12 | [102, 2], 13 | [103, 2], 14 | [103, 3], 15 | [102, 3], 16 | [102, 2] 17 | ] 18 | ], 19 | [ 20 | [ 21 | [100, 0], 22 | [101, 0], 23 | [101, 1], 24 | [100, 1], 25 | [100, 0] 26 | ] 27 | ] 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test-data/area-polygon.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "Polygon", 6 | "coordinates": [ 7 | [ 8 | [125, -15], 9 | [113, -22], 10 | [117, -37], 11 | [130, -33], 12 | [148, -39], 13 | [154, -27], 14 | [144, -15], 15 | [125, -15] 16 | ] 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test-data/bbox-featurecollection.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "geometry": { 7 | "type": "Point", 8 | "coordinates": [102, 0.5] 9 | }, 10 | "properties": {} 11 | }, 12 | { 13 | "type": "Feature", 14 | "geometry": { 15 | "type": "LineString", 16 | "coordinates": [ 17 | [102, -10], 18 | [103, 1], 19 | [104, 0], 20 | [130, 4] 21 | ] 22 | }, 23 | "properties": {} 24 | }, 25 | { 26 | "type": "Feature", 27 | "geometry": { 28 | "type": "Polygon", 29 | "coordinates": [ 30 | [ 31 | [20, 0], 32 | [101, 0], 33 | [101, 1], 34 | [100, 1], 35 | [100, 0] 36 | ] 37 | ] 38 | }, 39 | "properties": {} 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /test-data/bbox-geometry-multipolygon.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "MultiPolygon", 3 | "coordinates": [ 4 | [ 5 | [ 6 | [102, 2], 7 | [103, 2], 8 | [103, 3], 9 | [102, 3], 10 | [102, 2] 11 | ] 12 | ], 13 | [ 14 | [ 15 | [100, 0], 16 | [101, 0], 17 | [101, 1], 18 | [100, 1], 19 | [100, 0] 20 | ], 21 | [ 22 | [100.2, 0.2], 23 | [100.8, 0.2], 24 | [100.8, 0.8], 25 | [100.2, 0.8], 26 | [100.2, 0.2] 27 | ] 28 | ] 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /test-data/bbox-linestring.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "LineString", 6 | "coordinates": [ 7 | [102, -10], 8 | [103, 1], 9 | [104, 0], 10 | [130, 4] 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test-data/bbox-multilinestring.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "MultiLineString", 6 | "coordinates": [ 7 | [ 8 | [100, 0], 9 | [101, 1] 10 | ], 11 | [ 12 | [102, 2], 13 | [103, 3] 14 | ] 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test-data/bbox-multipoint.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "MultiPoint", 6 | "coordinates": [ 7 | [102, -10], 8 | [103, 1], 9 | [104, 0], 10 | [130, 4] 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test-data/bbox-multipolygon.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "MultiPolygon", 6 | "coordinates": [ 7 | [ 8 | [ 9 | [102, 2], 10 | [103, 2], 11 | [103, 3], 12 | [102, 3], 13 | [102, 2] 14 | ] 15 | ], 16 | [ 17 | [ 18 | [100, 0], 19 | [101, 0], 20 | [101, 1], 21 | [100, 1], 22 | [100, 0] 23 | ], 24 | [ 25 | [100.2, 0.2], 26 | [100.8, 0.2], 27 | [100.8, 0.8], 28 | [100.2, 0.8], 29 | [100.2, 0.2] 30 | ] 31 | ] 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test-data/bbox-point.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "Point", 6 | "coordinates": [102, 0.5] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test-data/bbox-polygon-linestring.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "LineString", 3 | "coordinates": [ 4 | [102, -10], 5 | [103, 1], 6 | [104, 0], 7 | [130, 4] 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /test-data/bbox-polygon.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "Polygon", 6 | "coordinates": [ 7 | [ 8 | [101, 0], 9 | [101, 1], 10 | [100, 1], 11 | [100, 0], 12 | [101, 0] 13 | ] 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test-data/imbalanced-polygon.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "Polygon", 6 | "coordinates": [ 7 | [ 8 | [4.854240417480469, 45.77258200374433], 9 | [4.8445844650268555, 45.777431068484894], 10 | [4.845442771911621, 45.778658234059755], 11 | [4.845914840698242, 45.779376562352425], 12 | [4.846644401550292, 45.78021460033108], 13 | [4.847245216369629, 45.78078326178593], 14 | [4.848060607910156, 45.78138184652523], 15 | [4.8487043380737305, 45.78186070968964], 16 | [4.849562644958495, 45.78248921135124], 17 | [4.850893020629883, 45.78302792142197], 18 | [4.852008819580077, 45.78374619341895], 19 | [4.852995872497559, 45.784075398324866], 20 | [4.853854179382324, 45.78443452873236], 21 | [4.8549699783325195, 45.78470387501975], 22 | [4.85569953918457, 45.784793656826345], 23 | [4.857330322265624, 45.784853511283764], 24 | [4.858231544494629, 45.78494329284938], 25 | [4.859304428100585, 45.784883438488365], 26 | [4.858360290527344, 45.77294120818474], 27 | [4.854240417480469, 45.77258200374433] 28 | ] 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test-data/mercator.featurecollection.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "properties": {}, 7 | "geometry": { 8 | "type": "Polygon", 9 | "coordinates": [ 10 | [ 11 | [-12913060.93202, 7967317.535016], 12 | [-10018754.171395, 7967317.535016], 13 | [-10018754.171395, 9876845.895795], 14 | [-12913060.93202, 9876845.895795], 15 | [-12913060.93202, 7967317.535016] 16 | ] 17 | ] 18 | } 19 | }, 20 | { 21 | "type": "Feature", 22 | "properties": {}, 23 | "geometry": { 24 | "type": "LineString", 25 | "coordinates": [ 26 | [-2269873.991957, 3991847.410438], 27 | [-391357.58482, 6026906.856034], 28 | [1565430.33928, 1917652.163291] 29 | ] 30 | } 31 | }, 32 | { 33 | "type": "Feature", 34 | "properties": {}, 35 | "geometry": { 36 | "type": "Point", 37 | "coordinates": [-8492459.534936, 430493.386177] 38 | } 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /test-data/mercator.geometry.linestring.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "LineString", 3 | "coordinates": [ 4 | [-14245416.087452, -156543.052253], 5 | [-10136161.391181, 3952711.561921], 6 | [-7983694.73033, 7866287.482754], 7 | [-3287403.712489, 9783939.750186] 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /test-data/mercator.geometry.multilinestring.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "MultiLineString", 3 | "coordinates": [ 4 | [ 5 | [-14245416.087452, -156543.052253], 6 | [-10136161.391181, 3952711.561921], 7 | [-7983694.73033, 7866287.482754], 8 | [-3287403.712489, 9783939.750186] 9 | ], 10 | [ 11 | [-8101101.950116, 3443946.802336], 12 | [-9196903.187613, 1487158.765218], 13 | [-5518141.890304, 1095801.284623], 14 | [-9392582.035682, -1252344.285755], 15 | [-3287403.712489, -7474929.934621] 16 | ] 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /test-data/mercator.geometry.multipoint.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "MultiPoint", 3 | "coordinates": [ 4 | [-2269873.991957, 3991847.410438], 5 | [-391357.58482, 6026906.856034], 6 | [1565430.33928, 1917652.163291] 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /test-data/mercator.geometry.multipolygon.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "MultiPolygon", 3 | "coordinates": [ 4 | [ 5 | [ 6 | [-12913060.93202, -5621521.486192], 7 | [-10018754.171395, -5621521.486192], 8 | [-10018754.171395, -7558415.656082], 9 | [-12913060.93202, -7558415.656082], 10 | [-12913060.93202, -5621521.486192] 11 | ] 12 | ], 13 | [ 14 | [ 15 | [-10057889.874217, 1017529.749988], 16 | [-8649002.568865, -391357.57973], 17 | [-7240115.263512, 1369751.525059], 18 | [-7318386.780476, 2465552.744045], 19 | [-9353446.221541, 2817774.632412], 20 | [-10057889.874217, 1017529.749988] 21 | ] 22 | ] 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /test-data/mercator.geometry.point.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Point", 3 | "coordinates": [-7903683.846322424, 5012341.663847514] 4 | } 5 | -------------------------------------------------------------------------------- /test-data/mercator.geometry.polygon.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Polygon", 3 | "coordinates": [ 4 | [ 5 | [-12913060.93202, 7967317.535016], 6 | [-10018754.171395, 7967317.535016], 7 | [-10018754.171395, 9876845.895795], 8 | [-12913060.93202, 9876845.895795], 9 | [-12913060.93202, 7967317.535016] 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test-data/mercator.geometrycollection.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "TYPE": "GeometryCollection", 3 | "geometries": [ 4 | { 5 | "TYPE": "Point", 6 | "coordinates": [-7903683.846322424, 5012341.663847514] 7 | }, 8 | { 9 | "TYPE": "LineString", 10 | "coordinates": [ 11 | [-2269873.991957, 3991847.410438], 12 | [-391357.58482, 6026906.856034], 13 | [1565430.33928, 1917652.163291] 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /test-data/mercator.linestring.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "LineString", 6 | "coordinates": [ 7 | [-2269873.991957, 3991847.410438], 8 | [-391357.58482, 6026906.856034], 9 | [1565430.33928, 1917652.163291] 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test-data/mercator.multilinestring.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "MultiLineString", 6 | "coordinates": [ 7 | [ 8 | [-14245416.087452, -156543.052253], 9 | [-10136161.391181, 3952711.561921], 10 | [-7983694.73033, 7866287.482754], 11 | [-3287403.712489, 9783939.750186] 12 | ], 13 | [ 14 | [-8101101.950116, 3443946.802336], 15 | [-9196903.187613, 1487158.765218], 16 | [-5518141.890304, 1095801.284623], 17 | [-9392582.035682, -1252344.285755], 18 | [-3287403.712489, -7474929.934621] 19 | ] 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test-data/mercator.multipoint.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "MultiPoint", 6 | "coordinates": [ 7 | [-2269873.991957, 3991847.410438], 8 | [-391357.58482, 6026906.856034], 9 | [1565430.33928, 1917652.163291] 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test-data/mercator.multipolygon.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "MultiPolygon", 6 | "coordinates": [ 7 | [ 8 | [ 9 | [-12913060.93202, -5621521.486192], 10 | [-10018754.171395, -5621521.486192], 11 | [-10018754.171395, -7558415.656082], 12 | [-12913060.93202, -7558415.656082], 13 | [-12913060.93202, -5621521.486192] 14 | ] 15 | ], 16 | [ 17 | [ 18 | [-10057889.874217, 1017529.749988], 19 | [-8649002.568865, -391357.57973], 20 | [-7240115.263512, 1369751.525059], 21 | [-7318386.780476, 2465552.744045], 22 | [-9353446.221541, 2817774.632412], 23 | [-10057889.874217, 1017529.749988] 24 | ] 25 | ] 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test-data/mercator.passed180thmeridian.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "Polygon", 6 | "coordinates": [ 7 | [ 8 | [12601714.231207, -2700367.319659], 9 | [12993071.816027, -4138606.416476], 10 | [14685693.356459, -3639625.540684], 11 | [15742358.891133, -4637587.344467], 12 | [16534857.930819, -4627803.413134], 13 | [17141462.21512, -3169996.399371], 14 | [15810846.426732, -1203424.537238], 15 | [15615167.689982, -1986139.800958], 16 | [15096618.792691, -1692621.582407], 17 | [15214026.123796, -1340399.683217], 18 | [14617205.820861, -1271912.182186], 19 | [13609459.970374, -1917652.163291], 20 | [13521404.583364, -2201386.426958], 21 | [12601714.231207, -2700367.319659] 22 | ] 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test-data/mercator.passed180thmeridian2.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "Polygon", 6 | "coordinates": [ 7 | [ 8 | [-8179373.46708, 1271912.182186], 9 | [-9099063.819238, -410925.448093], 10 | [-8394620.166561, -1682837.629118], 11 | [-7827151.696402, -2172034.572631], 12 | [-8492459.534936, -6535671.749414], 13 | [-7651040.811063, -7494497.668144], 14 | [-7279251.077654, -7298818.963454], 15 | [-7670608.662474, -6535671.749414], 16 | [-6985732.861209, -4794130.456523], 17 | [-6457400.093872, -4637587.344467], 18 | [-5439870.37334, -3306971.589232], 19 | [-5420302.521929, -2954749.719326], 20 | [-4637587.352288, -2524256.44615], 21 | [-3874440.034059, -763147.332293], 22 | [-5635549.221409, 117407.281873], 23 | [-5792092.255338, 508764.91044], 24 | [-7122708.043726, 1213208.514796], 25 | [-8179373.46708, 1271912.182186] 26 | ] 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test-data/mercator.point.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "Point", 6 | "coordinates": [-7903683.846322424, 5012341.663847514] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test-data/mercator.polygon.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "Polygon", 6 | "coordinates": [ 7 | [ 8 | [-12913060.93202, 7967317.535016], 9 | [-10018754.171395, 7967317.535016], 10 | [-10018754.171395, 9876845.895795], 11 | [-12913060.93202, 9876845.895795], 12 | [-12913060.93202, 7967317.535016] 13 | ] 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test-data/multiLineString.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "MultiLineString", 6 | "coordinates": [ 7 | [ 8 | [-77.031669, 38.878605], 9 | [-77.029609, 38.881946], 10 | [-77.020339, 38.884084], 11 | [-77.025661, 38.885821], 12 | [-77.021884, 38.889563], 13 | [-77.019824, 38.892368] 14 | ], 15 | [ 16 | [-77.041669, 38.885821], 17 | [-77.039609, 38.881946], 18 | [-77.030339, 38.884084], 19 | [-77.035661, 38.878605] 20 | ] 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test-data/multipoly-with-hole.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": { 4 | "name": "Poly with Hole", 5 | "value": 3, 6 | "filename": "poly-with-hole.json" 7 | }, 8 | "bbox": [ 9 | -86.77362442016602, 10 | 36.170862616662134, 11 | -86.67303085327148, 12 | 36.23084281427824 13 | ], 14 | "geometry": { 15 | "type": "MultiPolygon", 16 | "coordinates": [ 17 | [ 18 | [ 19 | [-86.76624298095703, 36.171278341935434], 20 | [-86.77362442016602, 36.2014818084173], 21 | [-86.74100875854492, 36.19607929145354], 22 | [-86.74238204956055, 36.170862616662134], 23 | [-86.76624298095703, 36.171278341935434] 24 | ] 25 | ], 26 | [ 27 | [ 28 | [-86.70478820800781, 36.23084281427824], 29 | [-86.73980712890625, 36.21062368007896], 30 | [-86.71371459960938, 36.173495506147], 31 | [-86.67526245117186, 36.17709826419592], 32 | [-86.67303085327148, 36.20910010895552], 33 | [-86.68041229248047, 36.230427405208005], 34 | [-86.70478820800781, 36.23084281427824] 35 | ], 36 | [ 37 | [-86.6934585571289, 36.217271643303604], 38 | [-86.71268463134766, 36.20771501855801], 39 | [-86.70238494873047, 36.19067640168397], 40 | [-86.68487548828125, 36.19691047217554], 41 | [-86.68264389038086, 36.20993115142727], 42 | [-86.6934585571289, 36.217271643303604] 43 | ] 44 | ] 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test-data/poly-with-hole.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": { 4 | "name": "Poly with Hole", 5 | "value": 3, 6 | "filename": "poly-with-hole.json" 7 | }, 8 | "bbox": [ 9 | -86.73980712890625, 10 | 36.173495506147, 11 | -86.67303085327148, 12 | 36.23084281427824 13 | ], 14 | "geometry": { 15 | "type": "Polygon", 16 | "coordinates": [ 17 | [ 18 | [-86.70478820800781, 36.23084281427824], 19 | [-86.73980712890625, 36.21062368007896], 20 | [-86.71371459960938, 36.173495506147], 21 | [-86.67526245117186, 36.17709826419592], 22 | [-86.67303085327148, 36.20910010895552], 23 | [-86.68041229248047, 36.230427405208005], 24 | [-86.70478820800781, 36.23084281427824] 25 | ], 26 | [ 27 | [-86.6934585571289, 36.217271643303604], 28 | [-86.71268463134766, 36.20771501855801], 29 | [-86.70238494873047, 36.19067640168397], 30 | [-86.68487548828125, 36.19691047217554], 31 | [-86.68264389038086, 36.20993115142727], 32 | [-86.6934585571289, 36.217271643303604] 33 | ] 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test-data/polygon.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "Polygon", 6 | "coordinates": [ 7 | [ 8 | [-77.031669, 38.878605], 9 | [-77.029609, 38.881946], 10 | [-77.020339, 38.884084], 11 | [-77.025661, 38.885821], 12 | [-77.021884, 38.889563], 13 | [-77.019824, 38.892368], 14 | [-77.04, 38.88], 15 | [-77.031669, 38.878605] 16 | ] 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test-data/wgs84.featurecollection.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "properties": {}, 7 | "geometry": { 8 | "type": "Polygon", 9 | "coordinates": [ 10 | [ 11 | [-116, 58], 12 | [-90, 58], 13 | [-90, 66], 14 | [-116, 66], 15 | [-116, 58] 16 | ] 17 | ] 18 | } 19 | }, 20 | { 21 | "type": "Feature", 22 | "properties": {}, 23 | "geometry": { 24 | "type": "LineString", 25 | "coordinates": [ 26 | [-20.390625, 33.72434], 27 | [-3.515625, 47.517201], 28 | [14.0625, 16.972741] 29 | ] 30 | } 31 | }, 32 | { 33 | "type": "Feature", 34 | "properties": {}, 35 | "geometry": { 36 | "type": "Point", 37 | "coordinates": [-76.289062, 3.864255] 38 | } 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /test-data/wgs84.geometry.linestring.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "LineString", 3 | "coordinates": [ 4 | [-20.39062500000365, 33.72434000000235], 5 | [-3.5156249999990803, 47.51720099999992], 6 | [14.062499999996321, 16.97274100000141] 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /test-data/wgs84.geometry.multilinestring.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "MultiLineString", 3 | "coordinates": [ 4 | [ 5 | [-127.96875000000244, -1.4061090000014986], 6 | [-91.05468700000083, 33.43144100000098], 7 | [-71.71874999999919, 57.51582299999881], 8 | [-29.531250000001258, 65.658275] 9 | ], 10 | [ 11 | [-72.77343699999663, 29.535230000001896], 12 | [-82.61718700000304, 13.23994499999653], 13 | [-49.57031200000271, 9.79567800000412], 14 | [-84.3749999999959, -11.178402000004093], 15 | [-29.531250000001258, -55.57834500000183] 16 | ] 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /test-data/wgs84.geometry.multipoint.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "MultiPoint", 3 | "coordinates": [ 4 | [-20.39062500000365, 33.72434000000235], 5 | [-3.5156249999990803, 47.51720099999992], 6 | [14.062499999996321, 16.97274100000141] 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /test-data/wgs84.geometry.multipolygon.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "MultiPolygon", 3 | "coordinates": [ 4 | [ 5 | [ 6 | [-116, -45], 7 | [-90, -45], 8 | [-90, -56], 9 | [-116, -56], 10 | [-116, -45] 11 | ] 12 | ], 13 | [ 14 | [ 15 | [-90.351563, 9.102097], 16 | [-77.695312, -3.513421], 17 | [-65.039063, 12.21118], 18 | [-65.742188, 21.616579], 19 | [-84.023437, 24.527135], 20 | [-90.351563, 9.102097] 21 | ] 22 | ] 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /test-data/wgs84.geometry.point.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Point", 3 | "coordinates": [-71, 41] 4 | } 5 | -------------------------------------------------------------------------------- /test-data/wgs84.geometry.polygon.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Polygon", 3 | "coordinates": [ 4 | [ 5 | [-116, 58], 6 | [-90, 58], 7 | [-90, 66], 8 | [-116, 66], 9 | [-116, 58] 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test-data/wgs84.geometrycollection.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "TYPE": "GeometryCollection", 3 | "geometries": [ 4 | { 5 | "TYPE": "Point", 6 | "coordinates": [-71.0, 40.99999999999998] 7 | }, 8 | { 9 | "TYPE": "LineString", 10 | "coordinates": [ 11 | [-20.39062500000365, 33.72434000000235], 12 | [-3.5156249999990803, 47.51720099999992], 13 | [14.062499999996321, 16.97274100000141] 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /test-data/wgs84.linestring.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "LineString", 6 | "coordinates": [ 7 | [-20.39062500000365, 33.72434000000235], 8 | [-3.5156249999990803, 47.51720099999992], 9 | [14.062499999996321, 16.97274100000141] 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test-data/wgs84.multilinestring.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "MultiLineString", 6 | "coordinates": [ 7 | [ 8 | [-127.96875000000244, -1.4061090000014986], 9 | [-91.05468700000083, 33.43144100000098], 10 | [-71.71874999999919, 57.51582299999881], 11 | [-29.531250000001258, 65.658275] 12 | ], 13 | [ 14 | [-72.77343699999663, 29.535230000001896], 15 | [-82.61718700000304, 13.23994499999653], 16 | [-49.57031200000271, 9.79567800000412], 17 | [-84.3749999999959, -11.178402000004093], 18 | [-29.531250000001258, -55.57834500000183] 19 | ] 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test-data/wgs84.multipoint.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "MultiPoint", 6 | "coordinates": [ 7 | [-20.39062500000365, 33.72434000000235], 8 | [-3.5156249999990803, 47.51720099999992], 9 | [14.062499999996321, 16.97274100000141] 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test-data/wgs84.multipolygon.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "MultiPolygon", 6 | "coordinates": [ 7 | [ 8 | [ 9 | [-116, -45], 10 | [-90, -45], 11 | [-90, -56], 12 | [-116, -56], 13 | [-116, -45] 14 | ] 15 | ], 16 | [ 17 | [ 18 | [-90.351563, 9.102097], 19 | [-77.695312, -3.513421], 20 | [-65.039063, 12.21118], 21 | [-65.742188, 21.616579], 22 | [-84.023437, 24.527135], 23 | [-90.351563, 9.102097] 24 | ] 25 | ] 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test-data/wgs84.passed180thmeridian.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "Polygon", 6 | "coordinates": [ 7 | [ 8 | [113.20312499999733, -23.56398700000191], 9 | [116.7187499999964, -34.8138030000016], 10 | [131.92382799999635, -31.05293399999659], 11 | [141.41601599999615, -38.41055799999684], 12 | [148.5351559999959, -38.34165599999897], 13 | [153.98437499999565, -27.371766999999128], 14 | [142.03124999999878, -10.746968999999027], 15 | [140.27343800000153, -17.560247000003802], 16 | [135.61523400000323, -15.029686000000622], 17 | [136.66992199999626, -11.953348999996189], 18 | [131.30859400000273, -11.350797000000282], 19 | [122.25585899999774, -16.9727410000014], 20 | [121.46484399999632, -19.394068000000363], 21 | [113.20312499999733, -23.56398700000191] 22 | ] 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test-data/wgs84.passed180thmeridian2.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "Polygon", 6 | "coordinates": [ 7 | [ 8 | [-73.476562, 11.350797], 9 | [-81.738281, -3.688855], 10 | [-75.410156, -14.944785], 11 | [-70.3125, -19.145168], 12 | [-76.289062, -50.513427], 13 | [-68.730469, -55.677584], 14 | [-65.390625, -54.673831], 15 | [-68.90625, -50.513427], 16 | [-62.753906, -39.504041], 17 | [-58.007813, -38.410558], 18 | [-48.867187, -28.459033], 19 | [-48.691406, -25.641526], 20 | [-41.660156, -22.105999], 21 | [-34.804687, -6.83917], 22 | [-50.625, 1.054628], 23 | [-52.03125, 4.565474], 24 | [-63.984375, 10.833306], 25 | [-73.476562, 11.350797] 26 | ] 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test-data/wgs84.point.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "Point", 6 | "coordinates": [-71, 41] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test-data/wgs84.polygon.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": {}, 4 | "geometry": { 5 | "type": "Polygon", 6 | "coordinates": [ 7 | [ 8 | [-116, 58], 9 | [-90, 58], 10 | [-90, 66], 11 | [-116, 66], 12 | [-116, 58] 13 | ] 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/assert/assert.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | // Equal checks if values are equal 10 | func Equal(t *testing.T, a, b interface{}) { 11 | if a == nil || b == nil { 12 | if a == b { 13 | return 14 | } 15 | } 16 | exp, ok := a.([]byte) 17 | if !ok { 18 | if reflect.DeepEqual(a, b) { 19 | return 20 | } 21 | } 22 | act, ok := b.([]byte) 23 | if !ok { 24 | t.Errorf("Received %v (type %v), expected %v (type %v)", a, reflect.TypeOf(a), b, reflect.TypeOf(b)) 25 | } 26 | if exp == nil || act == nil { 27 | if exp == nil && act == nil { 28 | return 29 | } 30 | } 31 | if bytes.Equal(exp, act) { 32 | return 33 | } 34 | t.Errorf("Received %v (type %v), expected %v (type %v)", a, reflect.TypeOf(a), b, reflect.TypeOf(b)) 35 | } 36 | 37 | // True asserts that the specified value is true 38 | func True(t *testing.T, value bool, msgAndArgs ...interface{}) bool { 39 | if !value { 40 | t.Errorf("Received %v, expected %v", value, true) 41 | } 42 | return value 43 | } 44 | 45 | func containsKind(kinds []reflect.Kind, kind reflect.Kind) bool { 46 | for i := 0; i < len(kinds); i++ { 47 | if kind == kinds[i] { 48 | return true 49 | } 50 | } 51 | 52 | return false 53 | } 54 | 55 | func isNil(object interface{}) bool { 56 | if object == nil { 57 | return true 58 | } 59 | value := reflect.ValueOf(object) 60 | kind := value.Kind() 61 | isNilableKind := containsKind( 62 | []reflect.Kind{ 63 | reflect.Chan, reflect.Func, 64 | reflect.Interface, reflect.Map, 65 | reflect.Ptr, reflect.Slice}, 66 | kind) 67 | if isNilableKind && value.IsNil() { 68 | return true 69 | } 70 | return false 71 | } 72 | 73 | // Nil asserts that the specified object is nil. 74 | // 75 | // assert.Nil(t, err) 76 | func Nil(t *testing.T, object interface{}) bool { 77 | return isNil(object) 78 | } 79 | 80 | // NotNil asserts that the specified object is not nil. 81 | // 82 | // assert.NotNil(t, err) 83 | func NotNil(t *testing.T, object interface{}) bool { 84 | return !isNil(object) 85 | } 86 | -------------------------------------------------------------------------------- /utils/common.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // Float64Ptr returns the pointer to a float64. 4 | func Float64Ptr(v float64) *float64 { 5 | return &v 6 | } 7 | 8 | // Int64Ptr returns the pointer to an int64. 9 | func Int64Ptr(v int64) *int64 { 10 | return &v 11 | } 12 | 13 | // Int32Ptr returns the pointer to an int32. 14 | func Int32Ptr(v int32) *int32 { 15 | return &v 16 | } 17 | 18 | // IntPtr returns the pointer to an int 19 | func IntPtr(v int) *int { 20 | return &v 21 | } 22 | 23 | // StringPtr returns the pointer to a string. 24 | func StringPtr(v string) *string { 25 | return &v 26 | } 27 | 28 | // BoolPtr returns the pointer to a string. 29 | func BoolPtr(v bool) *bool { 30 | return &v 31 | } 32 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | ) 7 | 8 | // LoadJSONFixture loads a testing file 9 | func LoadJSONFixture(filename string) (string, error) { 10 | filepath := filename 11 | 12 | b, err := os.ReadFile(filepath) 13 | if err != nil { 14 | return "", err 15 | } 16 | 17 | return string(b), nil 18 | } 19 | 20 | // IsArray returns true of the interface is an array. 21 | func IsArray(v interface{}) bool { 22 | rv := reflect.ValueOf(v) 23 | return rv.Kind() == reflect.Array || rv.Kind() == reflect.Slice 24 | } 25 | --------------------------------------------------------------------------------