├── .github └── workflows │ ├── golangci-lint.yml │ └── main.yml ├── .gitignore ├── .golangci.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── annotate ├── change.go ├── change_test.go ├── datasource.go ├── edgecases_test.go ├── errors.go ├── errors_test.go ├── geo.go ├── geo_test.go ├── internal │ └── core │ │ ├── compute.go │ │ ├── compute_test.go │ │ ├── datasourcer_test.go │ │ ├── errors.go │ │ ├── types.go │ │ └── types_test.go ├── options.go ├── order.go ├── order_test.go ├── relation.go ├── relation_test.go ├── shared │ └── child.go ├── testdata │ ├── relation_2714790.osm │ ├── relation_2714790_expected.osm │ ├── relation_4017808.osm │ ├── relation_4017808_expected.osm │ ├── way_230391153.osm │ ├── way_230391153_expected.osm │ ├── way_6394949.osm │ └── way_6394949_expected.osm ├── way.go └── way_test.go ├── bounds.go ├── bounds_test.go ├── change.go ├── change_test.go ├── changeset.go ├── changeset_test.go ├── codecov.yml ├── datasource.go ├── datasource_test.go ├── diff.go ├── diff_test.go ├── element.go ├── element_test.go ├── feature.go ├── feature_test.go ├── go.mod ├── go.sum ├── internal └── mputil │ ├── join.go │ ├── join_test.go │ ├── mputil.go │ └── mputil_test.go ├── json.go ├── node.go ├── node_test.go ├── note.go ├── note_test.go ├── object.go ├── object_test.go ├── osm.go ├── osm_test.go ├── osmapi ├── README.md ├── changeset.go ├── changeset_test.go ├── datasource.go ├── datasource_test.go ├── live_test.go ├── map.go ├── map_test.go ├── node.go ├── node_test.go ├── note.go ├── note_test.go ├── options.go ├── relation.go ├── relation_test.go ├── user.go ├── user_test.go ├── way.go └── way_test.go ├── osmgeojson ├── README.md ├── benchmarks_test.go ├── build_polygon.go ├── build_polygon_test.go ├── convert.go ├── convert_test.go ├── options.go ├── options_test.go └── testdata │ └── benchmark.osm ├── osmpbf ├── README.md ├── decode.go ├── decode_data.go ├── decode_test.go ├── example_stats_test.go ├── internal │ └── osmpbf │ │ ├── README.md │ │ ├── fileformat.pb.go │ │ ├── fileformat.proto │ │ ├── generate.go │ │ ├── osmformat.pb.go │ │ └── osmformat.proto ├── scanner.go ├── scanner_test.go ├── zlib_cgo.go └── zlib_go.go ├── osmtest ├── scanner.go └── scanner_test.go ├── osmxml ├── example_test.go ├── scanner.go └── scanner_test.go ├── polygon.go ├── polygon_test.go ├── relation.go ├── relation_test.go ├── replication ├── README.md ├── changesets.go ├── changesets_test.go ├── datasource.go ├── interval.go ├── interval_test.go ├── live_test.go ├── search.go └── search_test.go ├── tag.go ├── tag_test.go ├── testdata ├── andorra-latest.osm.bz2 ├── annotate_diff.xml ├── annotated_diff.xml ├── changeset_38162206.osc ├── changeset_38162210.osc ├── compare_scanners.go ├── delaware-latest.osm.pbf ├── minute_871.osc ├── relation-updates.osm └── way-updates.osm ├── update.go ├── update_test.go ├── user.go ├── user_test.go ├── way.go └── way_test.go /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | golangci: 13 | name: lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-go@v3 18 | with: 19 | go-version: '1.21' 20 | 21 | - name: golangci-lint 22 | uses: golangci/golangci-lint-action@v3 23 | with: 24 | version: v1.55 25 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | 8 | jobs: 9 | build-and-test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - name: Setup Go 15 | uses: actions/setup-go@v3 16 | with: 17 | go-version: '1.21' 18 | 19 | - name: Run build 20 | run: go build . 21 | 22 | - name: Run vet 23 | run: | 24 | go vet . 25 | 26 | - name: Run tests 27 | run: go test -v -coverprofile=profile.cov ./... 28 | 29 | - name: codecov 30 | uses: codecov/codecov-action@v1 31 | with: 32 | file: ./profile.cov 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.osm.bz2 2 | *.pbf 3 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | issues: 2 | exclude-rules: 3 | - path: '(.+)_test\.go' 4 | linters: 5 | - errcheck 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [v0.8.0](https://github.com/paulmach/osm/compare/v0.7.1...v0.8.0) - 2024-01-08 6 | 7 | ### Changed 8 | 9 | - go 1.16 is required, updated usages of `ioutil` for similar functions in `io` and `os` 10 | 11 | ### Fixed 12 | 13 | - correctly JSON unmarshal elements with a type tag by [@paulmach](https://github.com/paulmach) in https://github.com/paulmach/osm/pull/53 14 | 15 | ## [v0.7.1](https://github.com/paulmach/osm/compare/v0.7.0...v0.7.1) - 2022-11-29 16 | 17 | ### Added 18 | 19 | - osm: add Tags.FindTag and Tags.HasTag methods by [@paulmach](https://github.com/paulmach) in https://github.com/paulmach/osm/pull/45 20 | 21 | ### Fixed 22 | 23 | - osm: support version as json number or string by [@paulmach](https://github.com/paulmach) in https://github.com/paulmach/osm/pull/46 24 | 25 | ## [v0.7.0](https://github.com/paulmach/osm/compare/v0.6.0...v0.7.0) - 2022-08-17 26 | 27 | ### Changed 28 | 29 | - remove node/ways/relations marshaling into this packages custom binary format by [@paulmach](https://github.com/paulmach) in https://github.com/paulmach/osm/pull/40 30 | 31 | ## [v0.6.0](https://github.com/paulmach/osm/compare/v0.5.0...v0.6.0) - 2022-08-16 32 | 33 | ### Added 34 | 35 | - json: ability to unmarshal osmjson by [@paulmach](https://github.com/paulmach) in https://github.com/paulmach/osm/pull/39 36 | - json: add support for external json implementations by [@paulmach](https://github.com/paulmach) in https://github.com/paulmach/osm/pull/39 37 | 38 | ## [v0.5.0](https://github.com/paulmach/osm/compare/v0.4.0...v0.5.0) - 2022-06-07 39 | 40 | ### Added 41 | 42 | - replication: ability to get changeset state by [@paulmach](https://github.com/paulmach) in https://github.com/paulmach/osm/pull/37 43 | - replication: search for state/sequence number by timestamp by [@paulmach](https://github.com/paulmach) in https://github.com/paulmach/osm/pull/38 44 | 45 | ## [v0.4.0](https://github.com/paulmach/osm/compare/v0.3.0...v0.4.0) - 2022-05-26 46 | 47 | ### Changed 48 | 49 | - protobuf: port to google protobuf by [@OlafFlebbeBosch](https://github.com/OlafFlebbeBoch) in https://github.com/paulmach/osm/pull/36 50 | 51 | ## [v0.3.0](https://github.com/paulmach/osm/compare/v0.2.2...v0.3.0) - 2022-04-21 52 | 53 | ### Added 54 | 55 | - osmpbf: preallocation node tags array by [@paulmach](https://github.com/paulmach) in https://github.com/paulmach/osm/pull/33 56 | - osmpbf: support "sparse" dense nodes by [@paulmach](https://github.com/paulmach) in https://github.com/paulmach/osm/pull/32 57 | - osmpbf: add filter functions by [@paulmach](https://github.com/paulmach) in https://github.com/paulmach/osm/pull/30 58 | 59 | ## [v0.2.2](https://github.com/paulmach/osm/compare/v0.2.1...v0.2.2) - 2021-04-27 60 | 61 | ### Fixed 62 | 63 | - osmpbf: fixed memory allocation issues by [@paulmach](https://github.com/paulmach) in https://github.com/paulmach/osm/pull/26 64 | 65 | ## [v0.2.1](https://github.com/paulmach/osm/compare/v0.2.0...v0.2.1) - 2021-02-04 66 | 67 | ### Changed 68 | 69 | - osmpbf: reduces memory usage when decoding by [@oflebbe](https://github.com/oflebbe) in https://github.com/paulmach/osm/pull/22 70 | - Fix some more typos by [@meyermarcel](https://github.com/meyermarcel) in https://github.com/paulmach/osm/pull/23 71 | 72 | ## [v0.2.0](https://github.com/paulmach/osm/compare/v0.1.1...v0.2.0) - 2021-01-09 73 | 74 | ### Changed 75 | 76 | - osmpbf: ability to efficiently skip types when decoding by [@paulmach](https://github.com/paulmach) in https://github.com/paulmach/osm/pull/18 77 | - osmpbf: use [protoscan](https://github.com/paulmach/protoscan) for a 10%ish performance improvement 78 | - osmpbf: use cgo/czlib to decode protobufs (if cgo enabled), 20% faster on benchmarks [@paulmach](https://github.com/paulmach) in https://github.com/paulmach/osm/pull/19 79 | - deprecated node/ways/relations marshaling into this packages custom binary format [`8fcda5`](https://github.com/paulmach/osm/commit/8fcda5dc49b4767df63eccb5a25f3e63d5b17f4d) 80 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Paul Mach 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. 21 | -------------------------------------------------------------------------------- /annotate/datasource.go: -------------------------------------------------------------------------------- 1 | package annotate 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/paulmach/osm" 7 | "github.com/paulmach/osm/annotate/internal/core" 8 | "github.com/paulmach/osm/annotate/shared" 9 | 10 | "github.com/paulmach/orb" 11 | "github.com/paulmach/orb/planar" 12 | ) 13 | 14 | type wayDatasource struct { 15 | NodeHistoryDatasourcer 16 | } 17 | 18 | type wayChildDatasource struct { 19 | NodeHistoryAsChildrenDatasourcer 20 | } 21 | 22 | func newWayDatasourcer(ds NodeHistoryDatasourcer) core.Datasourcer { 23 | if d, ok := ds.(NodeHistoryAsChildrenDatasourcer); ok { 24 | return &wayChildDatasource{d} 25 | } 26 | 27 | return &wayDatasource{ds} 28 | } 29 | 30 | func (wds *wayDatasource) Get(ctx context.Context, id osm.FeatureID) (core.ChildList, error) { 31 | if id.Type() != osm.TypeNode { 32 | panic("only node types supported") 33 | } 34 | 35 | nodes, err := wds.NodeHistory(ctx, id.NodeID()) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | return nodesToChildList(nodes), nil 41 | } 42 | 43 | func (wds *wayChildDatasource) Get(ctx context.Context, id osm.FeatureID) (core.ChildList, error) { 44 | if id.Type() != osm.TypeNode { 45 | panic("only node types supported") 46 | } 47 | 48 | return wds.NodeHistoryAsChildren(ctx, id.NodeID()) 49 | } 50 | 51 | type relationDatasource struct { 52 | osm.HistoryDatasourcer 53 | } 54 | 55 | type relationChildDatasource struct { 56 | HistoryAsChildrenDatasourcer 57 | } 58 | 59 | func newRelationDatasourcer(ds osm.HistoryDatasourcer) core.Datasourcer { 60 | if d, ok := ds.(HistoryAsChildrenDatasourcer); ok { 61 | return &relationChildDatasource{d} 62 | } 63 | 64 | return &relationDatasource{ds} 65 | } 66 | 67 | func (rds *relationDatasource) Get(ctx context.Context, id osm.FeatureID) (core.ChildList, error) { 68 | 69 | switch id.Type() { 70 | case osm.TypeNode: 71 | nodes, err := rds.NodeHistory(ctx, id.NodeID()) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | return nodesToChildList(nodes), nil 77 | case osm.TypeWay: 78 | ways, err := rds.WayHistory(ctx, id.WayID()) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | return waysToChildList(ways), nil 84 | case osm.TypeRelation: 85 | relations, err := rds.RelationHistory(ctx, id.RelationID()) 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | return relationsToChildList(relations), nil 91 | } 92 | 93 | return nil, &UnsupportedMemberTypeError{ 94 | MemberType: id.Type(), 95 | } 96 | } 97 | 98 | func (rds *relationChildDatasource) Get(ctx context.Context, id osm.FeatureID) (core.ChildList, error) { 99 | 100 | switch id.Type() { 101 | case osm.TypeNode: 102 | return rds.NodeHistoryAsChildren(ctx, id.NodeID()) 103 | case osm.TypeWay: 104 | return rds.WayHistoryAsChildren(ctx, id.WayID()) 105 | case osm.TypeRelation: 106 | return rds.RelationHistoryAsChildren(ctx, id.RelationID()) 107 | } 108 | 109 | return nil, &UnsupportedMemberTypeError{ 110 | MemberType: id.Type(), 111 | } 112 | } 113 | 114 | func nodesToChildList(nodes osm.Nodes) core.ChildList { 115 | if len(nodes) == 0 { 116 | return nil 117 | } 118 | 119 | list := make(core.ChildList, len(nodes)) 120 | nodes.SortByIDVersion() 121 | for i, n := range nodes { 122 | c := shared.FromNode(n) 123 | c.VersionIndex = i 124 | list[i] = c 125 | } 126 | 127 | return list 128 | } 129 | 130 | func waysToChildList(ways osm.Ways) core.ChildList { 131 | if len(ways) == 0 { 132 | return nil 133 | } 134 | 135 | list := make(core.ChildList, len(ways)) 136 | ways.SortByIDVersion() 137 | for i, w := range ways { 138 | c := shared.FromWay(w) 139 | c.VersionIndex = i 140 | 141 | if i != 0 { 142 | c.ReverseOfPrevious = IsReverse(w, ways[i-1]) 143 | } 144 | 145 | list[i] = c 146 | } 147 | 148 | return list 149 | } 150 | 151 | // IsReverse checks to see if this way update was a "reversal". It is very tricky 152 | // to generally answer this question but easier for a relation minor update. 153 | // Since the relation wasn't updated we assume things are still connected and 154 | // can just check the endpoints. 155 | func IsReverse(w1, w2 *osm.Way) bool { 156 | if len(w1.Nodes) < 2 || len(w2.Nodes) < 2 { 157 | return false 158 | } 159 | 160 | // check if either is a ring 161 | if w1.Nodes[0].ID == w1.Nodes[len(w1.Nodes)-1].ID || 162 | w2.Nodes[0].ID == w2.Nodes[len(w2.Nodes)-1].ID { 163 | 164 | r1 := orb.Ring(w1.LineString()) 165 | r2 := orb.Ring(w2.LineString()) 166 | return planar.Area(r1)*planar.Area(r2) < 0 167 | } 168 | 169 | // not a ring so see if endpoint were flipped 170 | return w1.Nodes[0].ID == w2.Nodes[len(w2.Nodes)-1].ID && 171 | w2.Nodes[0].ID == w1.Nodes[len(w1.Nodes)-1].ID 172 | } 173 | 174 | func relationsToChildList(relations osm.Relations) core.ChildList { 175 | if len(relations) == 0 { 176 | return nil 177 | } 178 | 179 | list := make(core.ChildList, len(relations)) 180 | relations.SortByIDVersion() 181 | for i, r := range relations { 182 | c := shared.FromRelation(r) 183 | c.VersionIndex = i 184 | list[i] = c 185 | } 186 | 187 | return list 188 | } 189 | -------------------------------------------------------------------------------- /annotate/errors.go: -------------------------------------------------------------------------------- 1 | package annotate 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/paulmach/osm" 8 | "github.com/paulmach/osm/annotate/internal/core" 9 | ) 10 | 11 | // NoHistoryError is returned if there is no entry in the history 12 | // map for a specific child. 13 | type NoHistoryError struct { 14 | ID osm.FeatureID 15 | } 16 | 17 | // Error returns a pretty string of the error. 18 | func (e *NoHistoryError) Error() string { 19 | return fmt.Sprintf("element history not found for %v", e.ID) 20 | } 21 | 22 | // NoVisibleChildError is returned if there are no visible children 23 | // for a parent at a given time. 24 | type NoVisibleChildError struct { 25 | ID osm.FeatureID 26 | Timestamp time.Time 27 | } 28 | 29 | // Error returns a pretty string of the error. 30 | func (e *NoVisibleChildError) Error() string { 31 | return fmt.Sprintf("no visible child for %v at %v", e.ID, e.Timestamp) 32 | } 33 | 34 | // UnsupportedMemberTypeError is returned if a relation member is not a 35 | // node, way or relation. 36 | type UnsupportedMemberTypeError struct { 37 | RelationID osm.RelationID 38 | MemberType osm.Type 39 | Index int 40 | } 41 | 42 | // Error returns a pretty string of the error. 43 | func (e *UnsupportedMemberTypeError) Error() string { 44 | return fmt.Sprintf("unsupported member type %v for relation %d at %d", e.MemberType, e.RelationID, e.Index) 45 | } 46 | 47 | func mapErrors(err error) error { 48 | switch t := err.(type) { 49 | case *core.NoHistoryError: 50 | return &NoHistoryError{ 51 | ID: t.ChildID, 52 | } 53 | case *core.NoVisibleChildError: 54 | return &NoVisibleChildError{ 55 | ID: t.ChildID, 56 | Timestamp: t.Timestamp, 57 | } 58 | } 59 | 60 | return err 61 | } 62 | -------------------------------------------------------------------------------- /annotate/errors_test.go: -------------------------------------------------------------------------------- 1 | package annotate 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/paulmach/osm/annotate/internal/core" 8 | ) 9 | 10 | func TestMapErrors(t *testing.T) { 11 | e := mapErrors(&core.NoHistoryError{}) 12 | if _, ok := e.(*NoHistoryError); !ok { 13 | t.Errorf("should map NoHistoryError: %+v", e) 14 | } 15 | 16 | e = mapErrors(&core.NoVisibleChildError{}) 17 | if _, ok := e.(*NoVisibleChildError); !ok { 18 | t.Errorf("should map NoVisibleChildError: %+v", e) 19 | } 20 | 21 | err := errors.New("some error") 22 | if e := mapErrors(err); e != err { 23 | t.Errorf("should pass through other errors: %v", e) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /annotate/geo.go: -------------------------------------------------------------------------------- 1 | package annotate 2 | 3 | import ( 4 | "math" 5 | "time" 6 | 7 | "github.com/paulmach/orb" 8 | "github.com/paulmach/orb/geo" 9 | "github.com/paulmach/osm" 10 | "github.com/paulmach/osm/internal/mputil" 11 | ) 12 | 13 | func wayPointOnSurface(w *osm.Way) orb.Point { 14 | centroid := wayCentroid(w) 15 | 16 | // find closest node to centroid. 17 | // This is how ST_PointOnSurface is implemented. 18 | min := math.MaxFloat64 19 | index := 0 20 | for i, n := range w.Nodes { 21 | d := geo.Distance(centroid, n.Point()) 22 | if d < min { 23 | index = i 24 | min = d 25 | } 26 | } 27 | 28 | return w.Nodes[index].Point() 29 | } 30 | 31 | func wayCentroid(w *osm.Way) orb.Point { 32 | dist := 0.0 33 | point := orb.Point{} 34 | 35 | seg := [2]orb.Point{} 36 | 37 | for i := 0; i < len(w.Nodes)-1; i++ { 38 | seg[0] = w.Nodes[i].Point() 39 | seg[1] = w.Nodes[i+1].Point() 40 | 41 | d := geo.Distance(seg[0], seg[1]) 42 | 43 | point[0] += (seg[0][0] + seg[1][0]) / 2.0 * d 44 | point[1] += (seg[0][1] + seg[1][1]) / 2.0 * d 45 | 46 | dist += d 47 | } 48 | 49 | point[0] /= dist 50 | point[1] /= dist 51 | 52 | return point 53 | } 54 | 55 | // orientation will annotate the orientation of multipolygon relation members. 56 | // This makes it possible to reconstruct relations with partial data in the right direction. 57 | // Return value indicates if the result is 'tainted', e.g. not all way members were present. 58 | func orientation(members osm.Members, ways map[osm.WayID]*osm.Way, at time.Time) bool { 59 | outer, inner, tainted := mputil.Group(members, ways, at) 60 | 61 | outers := mputil.Join(outer) 62 | inners := mputil.Join(inner) 63 | 64 | for _, outer := range outers { 65 | annotateOrientation(members, outer, orb.CCW) 66 | } 67 | 68 | for _, inner := range inners { 69 | annotateOrientation(members, inner, orb.CW) 70 | } 71 | 72 | return tainted 73 | } 74 | 75 | func annotateOrientation(members osm.Members, ms mputil.MultiSegment, o orb.Orientation) { 76 | factor := orb.Orientation(1) 77 | if ms.Orientation() != o { 78 | factor = -1 79 | } 80 | 81 | for _, segment := range ms { 82 | if segment.Reversed { 83 | members[segment.Index].Orientation = -1 * factor * o 84 | } else { 85 | members[segment.Index].Orientation = factor * o 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /annotate/geo_test.go: -------------------------------------------------------------------------------- 1 | package annotate 2 | 3 | import ( 4 | "encoding/xml" 5 | "testing" 6 | 7 | "github.com/paulmach/orb" 8 | "github.com/paulmach/osm" 9 | ) 10 | 11 | func TestWayPointOnSurface(t *testing.T) { 12 | data := ` 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ` 21 | 22 | var w *osm.Way 23 | if err := xml.Unmarshal([]byte(data), &w); err != nil { 24 | t.Fatalf("failed to unmarshal: %v", err) 25 | } 26 | 27 | sp := wayPointOnSurface(w) 28 | 29 | expected := orb.Point{w.Nodes[3].Lon, w.Nodes[3].Lat} 30 | if !sp.Equal(expected) { 31 | t.Errorf("incorrect centroid: %v", sp) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /annotate/internal/core/datasourcer_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/paulmach/osm" 8 | ) 9 | 10 | var _ Datasourcer = &TestDS{} 11 | 12 | var ErrNotFound = errors.New("not found") 13 | 14 | // TestDS implements a datasource for testing. 15 | type TestDS struct { 16 | data map[osm.FeatureID]ChildList 17 | } 18 | 19 | // Get returns the history in ChildList form. 20 | func (tds *TestDS) Get(ctx context.Context, id osm.FeatureID) (ChildList, error) { 21 | if tds.data == nil { 22 | return nil, ErrNotFound 23 | } 24 | 25 | v := tds.data[id] 26 | if v == nil { 27 | return nil, ErrNotFound 28 | } 29 | 30 | return v, nil 31 | } 32 | 33 | // MustGet is used by tests only to simplify some code. 34 | func (tds *TestDS) MustGet(id osm.FeatureID) ChildList { 35 | v, err := tds.Get(context.TODO(), id) 36 | if err != nil { 37 | panic(err) 38 | } 39 | 40 | return v 41 | } 42 | 43 | func (tds *TestDS) NotFound(err error) bool { 44 | return err == ErrNotFound 45 | } 46 | 47 | // Set sets the element history into the map. 48 | // The element is deleted if list is nil. 49 | func (tds *TestDS) Set(id osm.FeatureID, list ChildList) { 50 | if tds.data == nil { 51 | tds.data = make(map[osm.FeatureID]ChildList) 52 | } 53 | 54 | if list == nil { 55 | delete(tds.data, id) 56 | } 57 | 58 | tds.data[id] = list 59 | } 60 | -------------------------------------------------------------------------------- /annotate/internal/core/errors.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/paulmach/osm" 8 | ) 9 | 10 | // NoHistoryError is returned if there is no entry in the history 11 | // map for a specific child. 12 | type NoHistoryError struct { 13 | ChildID osm.FeatureID 14 | } 15 | 16 | // Error returns a pretty string of the error. 17 | func (e *NoHistoryError) Error() string { 18 | return fmt.Sprintf("element history not found for %v", e.ChildID) 19 | } 20 | 21 | // NoVisibleChildError is returned if there are no visible children 22 | // for a parent at a given time. 23 | type NoVisibleChildError struct { 24 | ChildID osm.FeatureID 25 | Timestamp time.Time 26 | } 27 | 28 | // Error returns a pretty string of the error. 29 | func (e *NoVisibleChildError) Error() string { 30 | return fmt.Sprintf("no visible child for %v at %v", e.ChildID, e.Timestamp) 31 | } 32 | -------------------------------------------------------------------------------- /annotate/internal/core/types.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/paulmach/osm" 7 | "github.com/paulmach/osm/annotate/shared" 8 | ) 9 | 10 | // A Parent is something that holds children. ie. ways have nodes as children 11 | // and relations can have nodes, ways and relations as children. 12 | type Parent interface { 13 | ID() osm.FeatureID // used for logging 14 | ChangesetID() osm.ChangesetID 15 | 16 | Version() int 17 | Visible() bool 18 | Timestamp() time.Time 19 | Committed() time.Time 20 | 21 | // Refs returns normalized information about the children. 22 | // Currently this is the feature ids and if it is already annotated. 23 | // Note: we auto-annotate all unannotated children if they would have 24 | // been filtered out. 25 | Refs() (osm.FeatureIDs, []bool) 26 | SetChild(idx int, c *shared.Child) 27 | } 28 | 29 | // A ChildList is a set 30 | type ChildList []*shared.Child 31 | 32 | // FindVisible locates the child visible at the given time. 33 | // If 'at' is on or after osm.CommitInfoStart the committed 34 | // time is used to determine visiblity. If 'at' is before, a range +-eps 35 | // around the give time. Will return the closes visible node within that 36 | // range, or the previous node if visible. Children after 'at' but within 37 | // the eps must have the same changeset id as provided (the parent's). 38 | // If the previous node is not visible, or does not exits will return nil. 39 | func (cl ChildList) FindVisible(cid osm.ChangesetID, at time.Time, eps time.Duration) *shared.Child { 40 | var ( 41 | diff time.Duration = -1 42 | nearest *shared.Child 43 | ) 44 | 45 | start := at.Add(-eps) 46 | for _, c := range cl { 47 | 48 | if c.Committed.Before(osm.CommitInfoStart) { 49 | // more complicated logic for early data. 50 | offset := c.Timestamp.Sub(start) 51 | visible := c.Visible 52 | 53 | // if this node is after the end then it's over 54 | if offset > 2*eps { 55 | break 56 | } 57 | 58 | // if we're before the start set with the latest node 59 | if offset < 0 { 60 | if visible { 61 | nearest = c 62 | } else { 63 | nearest = nil 64 | } 65 | 66 | continue 67 | } 68 | 69 | // we're in the range!!! 70 | d := absDuration(offset - eps) 71 | if diff < 0 || (d <= diff) { 72 | // first within range, set if not visible 73 | if diff == -1 && !visible && offset == 0 { 74 | nearest = nil 75 | } 76 | 77 | // only update nearest if visible since we want 78 | // the closest visible within the range. 79 | if visible { 80 | if offset <= eps { 81 | // if we're before at, pick it 82 | nearest = c 83 | } else if c.ChangesetID == cid { 84 | // if we're after at, changeset must be same 85 | nearest = c 86 | } else { 87 | // after at, not same changeset, ignore. 88 | continue 89 | } 90 | } 91 | 92 | diff = d 93 | } 94 | } else { 95 | // simpler logic, if committed is on or before 'at' 96 | // consider that element. 97 | if c.Committed.After(at) { 98 | break 99 | } 100 | 101 | if c.Visible { 102 | nearest = c 103 | } else { 104 | nearest = nil 105 | } 106 | } 107 | } 108 | 109 | return nearest 110 | } 111 | 112 | // VersionBefore finds the last child before a given time. 113 | func (cl ChildList) VersionBefore(end time.Time) *shared.Child { 114 | var latest *shared.Child 115 | 116 | for _, c := range cl { 117 | if !timeThreshold(c, 0).Before(end) { 118 | break 119 | } 120 | 121 | latest = c 122 | } 123 | 124 | return latest 125 | } 126 | 127 | func timeThreshold(c *shared.Child, esp time.Duration) time.Time { 128 | if c.Committed.Before(osm.CommitInfoStart) { 129 | return c.Timestamp.Add(esp) 130 | } 131 | 132 | return c.Committed 133 | } 134 | 135 | func timeThresholdParent(p Parent, esp time.Duration) time.Time { 136 | if p.Committed().Before(osm.CommitInfoStart) { 137 | return p.Timestamp().Add(esp) 138 | } 139 | 140 | return p.Committed() 141 | } 142 | 143 | func absDuration(d time.Duration) time.Duration { 144 | if d < 0 { 145 | return -d 146 | } 147 | 148 | return d 149 | } 150 | -------------------------------------------------------------------------------- /annotate/options.go: -------------------------------------------------------------------------------- 1 | package annotate 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/paulmach/osm" 7 | "github.com/paulmach/osm/annotate/internal/core" 8 | ) 9 | 10 | // Option is a parameter that can be used for annotating. 11 | type Option func(*core.Options) error 12 | 13 | const defaultThreshold = 30 * time.Minute 14 | 15 | // Threshold is used if the "committed at" time is unknown and deals with 16 | // the flexibility of commit orders, e.g. nodes in the same commit as 17 | // the way can have a timestamp after the way. Threshold defines the time 18 | // range to "forward group" these changes. 19 | // Default 30 minutes. 20 | func Threshold(t time.Duration) Option { 21 | return func(o *core.Options) error { 22 | o.Threshold = t 23 | return nil 24 | } 25 | } 26 | 27 | // IgnoreInconsistency will try to match children even if they are missing. 28 | // This should be used when you want to gracefully handle the weird data in OSM. 29 | // 30 | // Nodes with unclear/inconsistent data will not be annotated. Causes include: 31 | // - redacted data: In 2012 due to the license change data had to be removed. 32 | // This could be some nodes of a way. There exist ways for which some nodes have 33 | // just a single delete version, e.g. way 159081205, node 376130526 34 | // - data pre element versioning: pre-2012(?) data versions were not kept, so 35 | // for old ways there many be no information about some nodes. For example, 36 | // a node may be updated after a way and there is no way to get the original 37 | // version of the node and way. 38 | // - bad editors: sometimes a node is edited 7 times in a single changeset 39 | // and version 5 is a delete. See node 321452894, part of way 28831147. 40 | func IgnoreInconsistency(yes bool) Option { 41 | return func(o *core.Options) error { 42 | o.IgnoreInconsistency = yes 43 | return nil 44 | } 45 | } 46 | 47 | // IgnoreMissingChildren will ignore children for which the datasource returns 48 | // datasource.ErrNotFound. This can be useful for partial history extracts where 49 | // there may be relations for which the way was not included, e.g. a relation has 50 | // a way inside the extract bounds and other ways outside the bounds. 51 | func IgnoreMissingChildren(yes bool) Option { 52 | return func(o *core.Options) error { 53 | o.IgnoreMissingChildren = yes 54 | return nil 55 | } 56 | } 57 | 58 | // ChildFilter allows for only a subset of children to be annotated on the parent. 59 | // This can greatly improve update speed by only worrying about the children 60 | // updated in the same batch. All unannotated children will be annotated regardless 61 | // of the results of the filter function. 62 | func ChildFilter(filter func(osm.FeatureID) bool) Option { 63 | return func(o *core.Options) error { 64 | o.ChildFilter = filter 65 | return nil 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /annotate/order.go: -------------------------------------------------------------------------------- 1 | package annotate 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/paulmach/osm" 8 | ) 9 | 10 | // RelationHistoryDatasourcer is an more strict interface for when we only need the relation history. 11 | type RelationHistoryDatasourcer interface { 12 | RelationHistory(context.Context, osm.RelationID) (osm.Relations, error) 13 | NotFound(error) bool 14 | } 15 | 16 | var _ RelationHistoryDatasourcer = &osm.HistoryDatasource{} 17 | 18 | // A ChildFirstOrdering is a struct that allows for a set of relations to be 19 | // processed in a dept first order. Since relations can reference other 20 | // relations we need to make sure children are added before parents. 21 | type ChildFirstOrdering struct { 22 | // CompletedIndex is the number of relation ids in the provided 23 | // array that have been finished. This can be used as a good restart position. 24 | CompletedIndex int 25 | 26 | ctx context.Context 27 | done context.CancelFunc 28 | ds RelationHistoryDatasourcer 29 | visited map[osm.RelationID]struct{} 30 | out chan osm.RelationID 31 | wg sync.WaitGroup 32 | 33 | id osm.RelationID 34 | err error 35 | } 36 | 37 | // NewChildFirstOrdering creates a new ordering object. It is used to provided 38 | // a child before parent ordering for relations. This order must be used when 39 | // inserting+annotating relations into the datastore. 40 | func NewChildFirstOrdering( 41 | ctx context.Context, 42 | ids []osm.RelationID, 43 | ds RelationHistoryDatasourcer, 44 | ) *ChildFirstOrdering { 45 | ctx, done := context.WithCancel(ctx) 46 | o := &ChildFirstOrdering{ 47 | ctx: ctx, 48 | done: done, 49 | ds: ds, 50 | visited: make(map[osm.RelationID]struct{}, len(ids)), 51 | out: make(chan osm.RelationID), 52 | } 53 | 54 | o.wg.Add(1) 55 | go func() { 56 | defer o.wg.Done() 57 | defer close(o.out) 58 | 59 | path := make([]osm.RelationID, 0, 100) 60 | for i, id := range ids { 61 | err := o.walk(id, path) 62 | if err != nil { 63 | o.err = err 64 | return 65 | } 66 | 67 | o.CompletedIndex = i 68 | } 69 | }() 70 | 71 | return o 72 | } 73 | 74 | // Err returns a non-nil error if something went wrong with search, 75 | // like a cycle, or a datasource error. 76 | func (o *ChildFirstOrdering) Err() error { 77 | if o.err != nil { 78 | return o.err 79 | } 80 | 81 | return o.ctx.Err() 82 | } 83 | 84 | // Next locates the next relation id that can be used. 85 | // Returns false if the context is closed, something went wrong 86 | // or the full tree has been walked. 87 | func (o *ChildFirstOrdering) Next() bool { 88 | if o.err != nil || o.ctx.Err() != nil { 89 | return false 90 | } 91 | 92 | select { 93 | case id := <-o.out: 94 | if id == 0 { 95 | return false 96 | } 97 | o.id = id 98 | return true 99 | case <-o.ctx.Done(): 100 | return false 101 | } 102 | } 103 | 104 | // RelationID is the id found by the previous scan. 105 | func (o *ChildFirstOrdering) RelationID() osm.RelationID { 106 | return o.id 107 | } 108 | 109 | // Close can be used to terminate the scanning process before 110 | // all ids have been walked. 111 | func (o *ChildFirstOrdering) Close() { 112 | o.done() 113 | o.wg.Wait() 114 | } 115 | 116 | func (o *ChildFirstOrdering) walk(id osm.RelationID, path []osm.RelationID) error { 117 | if _, ok := o.visited[id]; ok { 118 | return nil 119 | } 120 | 121 | relations, err := o.ds.RelationHistory(o.ctx, id) 122 | if o.ds.NotFound(err) { 123 | return nil 124 | } 125 | 126 | if err != nil { 127 | return err 128 | } 129 | 130 | for _, r := range relations { 131 | for _, m := range r.Members { 132 | if m.Type != osm.TypeRelation { 133 | continue 134 | } 135 | 136 | mid := osm.RelationID(m.Ref) 137 | for _, pid := range path { 138 | if pid == mid { 139 | // circular relations are allowed, 140 | // source: https://github.com/openstreetmap/openstreetmap-website/issues/1465#issuecomment-282323187 141 | 142 | // since this relation is already being worked through higher 143 | // up the stack, we can just return here. 144 | return nil 145 | } 146 | } 147 | 148 | err := o.walk(mid, append(path, mid)) 149 | if err != nil { 150 | return err 151 | } 152 | } 153 | } 154 | 155 | if o.ctx.Err() != nil { 156 | return o.ctx.Err() 157 | } 158 | 159 | o.visited[id] = struct{}{} 160 | select { 161 | case o.out <- id: 162 | case <-o.ctx.Done(): 163 | return o.ctx.Err() 164 | } 165 | 166 | return nil 167 | } 168 | -------------------------------------------------------------------------------- /annotate/relation.go: -------------------------------------------------------------------------------- 1 | package annotate 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/paulmach/osm" 8 | "github.com/paulmach/osm/annotate/internal/core" 9 | "github.com/paulmach/osm/annotate/shared" 10 | ) 11 | 12 | // HistoryAsChildrenDatasourcer is an advanced data source that 13 | // returns the needed elements as children directly. 14 | type HistoryAsChildrenDatasourcer interface { 15 | osm.HistoryDatasourcer 16 | NodeHistoryAsChildren(context.Context, osm.NodeID) ([]*shared.Child, error) 17 | WayHistoryAsChildren(context.Context, osm.WayID) ([]*shared.Child, error) 18 | RelationHistoryAsChildren(context.Context, osm.RelationID) ([]*shared.Child, error) 19 | } 20 | 21 | // Relations computes the updates for the given relations 22 | // and annotate members with stuff like changeset and lon/lat data. 23 | // The input relations are modified to include this information. 24 | func Relations( 25 | ctx context.Context, 26 | relations osm.Relations, 27 | datasource osm.HistoryDatasourcer, 28 | opts ...Option, 29 | ) error { 30 | computeOpts := &core.Options{ 31 | Threshold: defaultThreshold, 32 | } 33 | for _, o := range opts { 34 | err := o(computeOpts) 35 | if err != nil { 36 | return err 37 | } 38 | } 39 | 40 | parents := make([]core.Parent, len(relations)) 41 | for i, r := range relations { 42 | parents[i] = &parentRelation{Relation: r} 43 | } 44 | 45 | rds := newRelationDatasourcer(datasource) 46 | updatesForParents, err := core.Compute(ctx, parents, rds, computeOpts) 47 | if err != nil { 48 | return mapErrors(err) 49 | } 50 | 51 | for _, p := range parents { 52 | r := p.(*parentRelation) 53 | if r.Relation.Polygon() { 54 | orientation(r.Relation.Members, r.ways, r.Relation.CommittedAt()) 55 | } 56 | } 57 | 58 | for i, updates := range updatesForParents { 59 | relations[i].Updates = updates 60 | } 61 | 62 | return nil 63 | } 64 | 65 | // A parentRelation wraps a osm.Relation into the core.Parent interface 66 | // so that updates can be computed. 67 | type parentRelation struct { 68 | Relation *osm.Relation 69 | ways map[osm.WayID]*osm.Way 70 | } 71 | 72 | func (r *parentRelation) ID() osm.FeatureID { 73 | return r.Relation.FeatureID() 74 | } 75 | 76 | func (r *parentRelation) ChangesetID() osm.ChangesetID { 77 | return r.Relation.ChangesetID 78 | } 79 | 80 | func (r *parentRelation) Version() int { 81 | return r.Relation.Version 82 | } 83 | 84 | func (r *parentRelation) Visible() bool { 85 | return r.Relation.Visible 86 | } 87 | 88 | func (r *parentRelation) Timestamp() time.Time { 89 | return r.Relation.Timestamp 90 | } 91 | 92 | func (r *parentRelation) Committed() time.Time { 93 | if r.Relation.Committed == nil { 94 | return time.Time{} 95 | } 96 | 97 | return *r.Relation.Committed 98 | } 99 | 100 | func (r *parentRelation) Refs() (osm.FeatureIDs, []bool) { 101 | ids := make(osm.FeatureIDs, len(r.Relation.Members)) 102 | annotated := make([]bool, len(r.Relation.Members)) 103 | 104 | for i := range r.Relation.Members { 105 | ids[i] = r.Relation.Members[i].FeatureID() 106 | annotated[i] = r.Relation.Members[i].Version != 0 107 | } 108 | 109 | return ids, annotated 110 | } 111 | 112 | func (r *parentRelation) SetChild(idx int, child *shared.Child) { 113 | if r.Relation.Polygon() && r.ways == nil { 114 | r.ways = make(map[osm.WayID]*osm.Way, len(r.Relation.Members)) 115 | } 116 | 117 | if child == nil { 118 | return 119 | } 120 | 121 | r.Relation.Members[idx].Version = child.Version 122 | r.Relation.Members[idx].ChangesetID = child.ChangesetID 123 | r.Relation.Members[idx].Lat = child.Lat 124 | r.Relation.Members[idx].Lon = child.Lon 125 | 126 | if r.ways != nil && child.Way != nil { 127 | r.ways[child.Way.ID] = child.Way 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /annotate/shared/child.go: -------------------------------------------------------------------------------- 1 | // Package shared is used by annotate and the internal core. 2 | // External usage of this package is for advanced use only. 3 | package shared 4 | 5 | import ( 6 | "time" 7 | 8 | "github.com/paulmach/osm" 9 | ) 10 | 11 | // A Child represents a node, way or relation that is a dependent for 12 | // annotating ways or relations. 13 | type Child struct { 14 | ID osm.FeatureID 15 | Version int 16 | ChangesetID osm.ChangesetID 17 | 18 | // VersionIndex is the index of the version if sorted from lowest to highest. 19 | // This is necessary since version don't have to start at 1 or be sequential. 20 | VersionIndex int 21 | Timestamp time.Time 22 | Committed time.Time 23 | 24 | // for nodes 25 | Lon, Lat float64 26 | 27 | // for ways 28 | Way *osm.Way 29 | ReverseOfPrevious bool 30 | 31 | // moving the visible bool here decreases the struct size from 32 | // size 120 (size class 128) to 112 (size class 112). 33 | Visible bool 34 | } 35 | 36 | // Update generates an update from this child. 37 | func (c *Child) Update() osm.Update { 38 | return osm.Update{ 39 | Version: c.Version, 40 | Timestamp: updateTimestamp(c.Timestamp, c.Committed), 41 | ChangesetID: c.ChangesetID, 42 | 43 | Lat: c.Lat, 44 | Lon: c.Lon, 45 | 46 | Reverse: c.ReverseOfPrevious, 47 | } 48 | } 49 | 50 | // FromNode converts a node to a child. 51 | func FromNode(n *osm.Node) *Child { 52 | c := &Child{ 53 | ID: n.FeatureID(), 54 | Version: n.Version, 55 | ChangesetID: n.ChangesetID, 56 | Visible: n.Visible, 57 | Timestamp: n.Timestamp, 58 | 59 | Lon: n.Lon, 60 | Lat: n.Lat, 61 | } 62 | 63 | if n.Committed != nil { 64 | c.Committed = *n.Committed 65 | } 66 | 67 | return c 68 | } 69 | 70 | // FromWay converts a way to a child. 71 | func FromWay(w *osm.Way) *Child { 72 | c := &Child{ 73 | ID: w.FeatureID(), 74 | Version: w.Version, 75 | ChangesetID: w.ChangesetID, 76 | Visible: w.Visible, 77 | Timestamp: w.Timestamp, 78 | Way: w, 79 | } 80 | 81 | if w.Committed != nil { 82 | c.Committed = *w.Committed 83 | } 84 | 85 | return c 86 | } 87 | 88 | // FromRelation converts a way to a child. 89 | func FromRelation(r *osm.Relation) *Child { 90 | c := &Child{ 91 | ID: r.FeatureID(), 92 | Version: r.Version, 93 | ChangesetID: r.ChangesetID, 94 | Visible: r.Visible, 95 | Timestamp: r.Timestamp, 96 | } 97 | 98 | if r.Committed != nil { 99 | c.Committed = *r.Committed 100 | } 101 | 102 | return c 103 | } 104 | 105 | func updateTimestamp(timestamp, committed time.Time) time.Time { 106 | if timestamp.Before(osm.CommitInfoStart) || committed.IsZero() { 107 | return timestamp 108 | } 109 | 110 | return committed 111 | } 112 | -------------------------------------------------------------------------------- /annotate/testdata/way_230391153.osm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /annotate/testdata/way_230391153_expected.osm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /annotate/way.go: -------------------------------------------------------------------------------- 1 | package annotate 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/paulmach/osm" 8 | "github.com/paulmach/osm/annotate/internal/core" 9 | "github.com/paulmach/osm/annotate/shared" 10 | ) 11 | 12 | // NodeHistoryDatasourcer is an more strict interface for when we only need node history. 13 | type NodeHistoryDatasourcer interface { 14 | NodeHistory(context.Context, osm.NodeID) (osm.Nodes, error) 15 | NotFound(error) bool 16 | } 17 | 18 | // NodeHistoryAsChildrenDatasourcer is an advanced data source that 19 | // returns the needed nodes as children directly. 20 | type NodeHistoryAsChildrenDatasourcer interface { 21 | NodeHistoryDatasourcer 22 | NodeHistoryAsChildren(context.Context, osm.NodeID) ([]*shared.Child, error) 23 | } 24 | 25 | var _ NodeHistoryDatasourcer = &osm.HistoryDatasource{} 26 | 27 | // Ways computes the updates for the given ways 28 | // and annotate the way nodes with changeset and lon/lat data. 29 | // The input ways are modified to include this information. 30 | func Ways( 31 | ctx context.Context, 32 | ways osm.Ways, 33 | datasource NodeHistoryDatasourcer, 34 | opts ...Option, 35 | ) error { 36 | computeOpts := &core.Options{ 37 | Threshold: defaultThreshold, 38 | } 39 | for _, o := range opts { 40 | err := o(computeOpts) 41 | if err != nil { 42 | return err 43 | } 44 | } 45 | 46 | parents := make([]core.Parent, len(ways)) 47 | for i, w := range ways { 48 | parents[i] = &parentWay{Way: w} 49 | } 50 | 51 | wds := newWayDatasourcer(datasource) 52 | updatesForParents, err := core.Compute(ctx, parents, wds, computeOpts) 53 | if err != nil { 54 | return mapErrors(err) 55 | } 56 | 57 | // fill in updates 58 | for i, updates := range updatesForParents { 59 | ways[i].Updates = updates 60 | } 61 | 62 | return nil 63 | } 64 | 65 | // A parentWay wraps a osm.Way into the core.Parent interface 66 | // so that updates can be computed. 67 | type parentWay struct { 68 | Way *osm.Way 69 | } 70 | 71 | func (w *parentWay) ID() osm.FeatureID { 72 | return w.Way.FeatureID() 73 | } 74 | 75 | func (w *parentWay) ChangesetID() osm.ChangesetID { 76 | return w.Way.ChangesetID 77 | } 78 | 79 | func (w *parentWay) Version() int { 80 | return w.Way.Version 81 | } 82 | 83 | func (w *parentWay) Visible() bool { 84 | return w.Way.Visible 85 | } 86 | 87 | func (w *parentWay) Timestamp() time.Time { 88 | return w.Way.Timestamp 89 | } 90 | 91 | func (w *parentWay) Committed() time.Time { 92 | if w.Way.Committed == nil { 93 | return time.Time{} 94 | } 95 | 96 | return *w.Way.Committed 97 | } 98 | 99 | func (w *parentWay) Refs() (osm.FeatureIDs, []bool) { 100 | ids := make(osm.FeatureIDs, len(w.Way.Nodes)) 101 | annotated := make([]bool, len(w.Way.Nodes)) 102 | 103 | for i := range w.Way.Nodes { 104 | ids[i] = w.Way.Nodes[i].FeatureID() 105 | annotated[i] = w.Way.Nodes[i].Version != 0 106 | } 107 | 108 | return ids, annotated 109 | } 110 | 111 | func (w *parentWay) SetChild(idx int, child *shared.Child) { 112 | if child == nil { 113 | return 114 | } 115 | 116 | w.Way.Nodes[idx].Version = child.Version 117 | w.Way.Nodes[idx].ChangesetID = child.ChangesetID 118 | w.Way.Nodes[idx].Lat = child.Lat 119 | w.Way.Nodes[idx].Lon = child.Lon 120 | } 121 | -------------------------------------------------------------------------------- /annotate/way_test.go: -------------------------------------------------------------------------------- 1 | package annotate 2 | 3 | import ( 4 | "context" 5 | "encoding/xml" 6 | "fmt" 7 | "os" 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/paulmach/osm" 12 | ) 13 | 14 | func TestWays(t *testing.T) { 15 | ids := []osm.WayID{ 16 | 6394949, 17 | 230391153, 18 | } 19 | 20 | for _, id := range ids { 21 | o := loadTestdata(t, fmt.Sprintf("testdata/way_%d.osm", id)) 22 | 23 | ds := (&osm.OSM{Nodes: o.Nodes}).HistoryDatasource() 24 | err := Ways(context.Background(), o.Ways, ds) 25 | if err != nil { 26 | t.Fatalf("compute error: %v", err) 27 | } 28 | 29 | filename := fmt.Sprintf("testdata/way_%d_expected.osm", id) 30 | expected := loadTestdata(t, filename) 31 | 32 | if !reflect.DeepEqual(o.Ways, expected.Ways) { 33 | t.Errorf("expected way for id %d not equal", id) 34 | 35 | filename := fmt.Sprintf("testdata/way_%d_got.osm", id) 36 | t.Errorf("expected way not equal, file saved to %s", filename) 37 | 38 | data, _ := xml.MarshalIndent(&osm.OSM{Ways: o.Ways}, "", " ") 39 | err := os.WriteFile(filename, data, 0644) 40 | if err != nil { 41 | t.Fatalf("write error: %v", err) 42 | } 43 | } 44 | } 45 | } 46 | 47 | func TestWays_childFilter(t *testing.T) { 48 | nodes := osm.Nodes{ 49 | {ID: 1, Version: 1, Lat: 1, Lon: 1, Visible: true}, 50 | {ID: 1, Version: 2, Lat: 2, Lon: 2, Visible: true}, 51 | {ID: 1, Version: 3, Lat: 3, Lon: 3, Visible: true}, 52 | {ID: 2, Version: 1, Lat: 1, Lon: 1, Visible: true}, 53 | {ID: 2, Version: 2, Lat: 2, Lon: 2, Visible: true}, 54 | {ID: 2, Version: 3, Lat: 3, Lon: 3, Visible: true}, 55 | {ID: 3, Version: 1, Lat: 1, Lon: 1, Visible: true}, 56 | } 57 | 58 | ways := osm.Ways{ 59 | { 60 | ID: 1, 61 | Version: 1, 62 | Visible: true, 63 | Nodes: osm.WayNodes{ 64 | {ID: 1, Version: 1}, 65 | {ID: 2, Version: 1}, // filter says no annotate 66 | {ID: 3}, // annotate because not 67 | }, 68 | }, 69 | } 70 | 71 | ds := (&osm.OSM{Nodes: nodes}).HistoryDatasource() 72 | err := Ways( 73 | context.Background(), 74 | ways, 75 | ds, 76 | Threshold(0), 77 | ChildFilter(func(fid osm.FeatureID) bool { 78 | return fid == osm.NodeID(1).FeatureID() 79 | }), 80 | ) 81 | if err != nil { 82 | t.Fatalf("compute error: %v", err) 83 | } 84 | 85 | if ways[0].Nodes[0].Lat == 0 { 86 | t.Errorf("should annotate first node") 87 | } 88 | 89 | if ways[0].Nodes[1].Lat != 0 { 90 | t.Errorf("should not annotate second node") 91 | } 92 | 93 | if ways[0].Nodes[2].Lat == 0 { 94 | t.Errorf("should annotate third node") 95 | } 96 | } 97 | 98 | func BenchmarkWay(b *testing.B) { 99 | o := loadTestdata(b, "testdata/way_6394949.osm") 100 | ds := (&osm.OSM{Nodes: o.Nodes}).HistoryDatasource() 101 | 102 | b.ReportAllocs() 103 | b.ResetTimer() 104 | for n := 0; n < b.N; n++ { 105 | err := Ways(context.Background(), o.Ways, ds) 106 | if err != nil { 107 | b.Fatalf("compute error: %v", err) 108 | } 109 | } 110 | } 111 | 112 | func BenchmarkWays(b *testing.B) { 113 | o := loadTestdata(b, "testdata/relation_2714790.osm") 114 | ds := o.HistoryDatasource() 115 | 116 | b.ReportAllocs() 117 | b.ResetTimer() 118 | for n := 0; n < b.N; { 119 | for id, ways := range ds.Ways { 120 | err := Ways(context.Background(), ways, ds) 121 | if err != nil { 122 | b.Fatalf("compute error for way %d: %v", id, err) 123 | } 124 | 125 | n++ 126 | if n >= b.N { 127 | break 128 | } 129 | } 130 | } 131 | } 132 | 133 | func loadTestdata(tb testing.TB, filename string) *osm.OSM { 134 | data, err := os.ReadFile(filename) 135 | if err != nil { 136 | tb.Fatalf("unable to open file: %v", err) 137 | } 138 | 139 | o := &osm.OSM{} 140 | err = xml.Unmarshal(data, o) 141 | if err != nil { 142 | tb.Fatalf("unable to unmarshal data: %v", err) 143 | } 144 | 145 | return o 146 | } 147 | -------------------------------------------------------------------------------- /bounds.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/paulmach/orb/maptile" 7 | ) 8 | 9 | // Bounds are the bounds of osm data as defined in the xml file. 10 | type Bounds struct { 11 | MinLat float64 `xml:"minlat,attr"` 12 | MaxLat float64 `xml:"maxlat,attr"` 13 | MinLon float64 `xml:"minlon,attr"` 14 | MaxLon float64 `xml:"maxlon,attr"` 15 | } 16 | 17 | // NewBoundsFromTile creates a bound given an online map tile index. 18 | func NewBoundsFromTile(t maptile.Tile) (*Bounds, error) { 19 | maxIndex := uint32(1 << t.Z) 20 | if t.X >= maxIndex { 21 | return nil, errors.New("osm: x index out of range for this zoom") 22 | } 23 | if t.Y >= maxIndex { 24 | return nil, errors.New("osm: y index out of range for this zoom") 25 | } 26 | 27 | b := t.Bound() 28 | return &Bounds{ 29 | MinLat: b.Min.Lat(), 30 | MaxLat: b.Max.Lat(), 31 | MinLon: b.Min.Lon(), 32 | MaxLon: b.Max.Lon(), 33 | }, nil 34 | } 35 | 36 | // ContainsNode returns true if the node is within the bound. 37 | // Uses inclusive intervals, ie. returns true if on the boundary. 38 | func (b *Bounds) ContainsNode(n *Node) bool { 39 | if n.Lat < b.MinLat || n.Lat > b.MaxLat { 40 | return false 41 | } 42 | 43 | if n.Lon < b.MinLon || n.Lon > b.MaxLon { 44 | return false 45 | } 46 | 47 | return true 48 | } 49 | 50 | // ObjectID returns the bounds type but with 0 id. Since id doesn't make sense. 51 | // This is here to implement the Object interface since it technically is an 52 | // osm object type. It also allows bounds to be returned via the osmxml.Scanner. 53 | func (b *Bounds) ObjectID() ObjectID { 54 | return ObjectID(boundsMask) 55 | } 56 | -------------------------------------------------------------------------------- /bounds_test.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/paulmach/orb/maptile" 8 | ) 9 | 10 | func TestNewBoundFromTile(t *testing.T) { 11 | if _, err := NewBoundsFromTile(maptile.New(1000, 1, 3)); err == nil { 12 | t.Errorf("should return error for x out of bound") 13 | } 14 | 15 | if _, err := NewBoundsFromTile(maptile.New(1, 1000, 3)); err == nil { 16 | t.Errorf("should return error for y out of bound") 17 | } 18 | 19 | bounds, _ := NewBoundsFromTile(maptile.New(7, 8, 9)) 20 | 21 | // check 9 tiles around bounds 22 | for i := -1; i <= 1; i++ { 23 | for j := -1; j <= 1; j++ { 24 | t.Run(fmt.Sprintf("i %d j %d", i, j), func(t *testing.T) { 25 | n := centroid(mustBounds(t, uint32(7+i), uint32(8+j), 9)) 26 | if i == 0 && j == 0 { 27 | if !bounds.ContainsNode(n) { 28 | t.Errorf("should contain point") 29 | } 30 | } else { 31 | if bounds.ContainsNode(n) { 32 | t.Errorf("should not contain point") 33 | } 34 | } 35 | }) 36 | } 37 | } 38 | } 39 | 40 | func TestBounds_ContainsNode(t *testing.T) { 41 | b := &Bounds{} 42 | 43 | if v := b.ContainsNode(&Node{}); !v { 44 | t.Errorf("should contain node on boundary") 45 | } 46 | 47 | if v := b.ContainsNode(&Node{Lat: -1}); v { 48 | t.Errorf("should not contain node outside bounds") 49 | } 50 | if v := b.ContainsNode(&Node{Lat: 1}); v { 51 | t.Errorf("should not contain node outside bounds") 52 | } 53 | if v := b.ContainsNode(&Node{Lon: -1}); v { 54 | t.Errorf("should not contain node outside bounds") 55 | } 56 | 57 | if v := b.ContainsNode(&Node{Lon: 1}); v { 58 | t.Errorf("should not contain node outside bounds") 59 | } 60 | } 61 | 62 | func mustBounds(t *testing.T, x, y uint32, z maptile.Zoom) *Bounds { 63 | bounds, err := NewBoundsFromTile(maptile.New(x, y, z)) 64 | if err != nil { 65 | t.Fatalf("invalid bounds: %v", err) 66 | } 67 | 68 | return bounds 69 | } 70 | 71 | func centroid(b *Bounds) *Node { 72 | return &Node{ 73 | Lon: (b.MinLon + b.MaxLon) / 2, 74 | Lat: (b.MinLat + b.MaxLat) / 2, 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /change.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import ( 4 | "encoding/xml" 5 | ) 6 | 7 | // Change is the structure of a changeset to be 8 | // uploaded or downloaded from the osm api server. 9 | // See: http://wiki.openstreetmap.org/wiki/OsmChange 10 | type Change struct { 11 | Version string `xml:"version,attr,omitempty" json:"version,omitempty"` 12 | Generator string `xml:"generator,attr,omitempty" json:"generator,omitempty"` 13 | 14 | // to indicate the origin of the data 15 | Copyright string `xml:"copyright,attr,omitempty" json:"copyright,omitempty"` 16 | Attribution string `xml:"attribution,attr,omitempty" json:"attribution,omitempty"` 17 | License string `xml:"license,attr,omitempty" json:"license,omitempty"` 18 | 19 | Create *OSM `xml:"create" json:"create,omitempty"` 20 | Modify *OSM `xml:"modify" json:"modify,omitempty"` 21 | Delete *OSM `xml:"delete" json:"delete,omitempty"` 22 | } 23 | 24 | // AppendCreate will append the object to the Create OSM object. 25 | func (c *Change) AppendCreate(o Object) { 26 | if c.Create == nil { 27 | c.Create = &OSM{} 28 | } 29 | 30 | c.Create.Append(o) 31 | } 32 | 33 | // AppendModify will append the object to the Modify OSM object. 34 | func (c *Change) AppendModify(o Object) { 35 | if c.Modify == nil { 36 | c.Modify = &OSM{} 37 | } 38 | 39 | c.Modify.Append(o) 40 | } 41 | 42 | // AppendDelete will append the object to the Delete OSM object. 43 | func (c *Change) AppendDelete(o Object) { 44 | if c.Delete == nil { 45 | c.Delete = &OSM{} 46 | } 47 | 48 | c.Delete.Append(o) 49 | } 50 | 51 | // HistoryDatasource converts the change object to a datasource accessible 52 | // by feature id. All the creates, modifies and deletes will be added 53 | // in that order. 54 | func (c *Change) HistoryDatasource() *HistoryDatasource { 55 | ds := &HistoryDatasource{} 56 | 57 | ds.add(c.Create, true) 58 | ds.add(c.Modify, true) 59 | ds.add(c.Delete, false) 60 | 61 | return ds 62 | } 63 | 64 | // MarshalXML implements the xml.Marshaller method to allow for the 65 | // correct wrapper/start element case and attr data. 66 | func (c Change) MarshalXML(e *xml.Encoder, start xml.StartElement) error { 67 | start.Name.Local = "osmChange" 68 | start.Attr = []xml.Attr{} 69 | 70 | if c.Version != "" { 71 | start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "version"}, Value: c.Version}) 72 | } 73 | 74 | if c.Generator != "" { 75 | start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "generator"}, Value: c.Generator}) 76 | } 77 | 78 | if c.Copyright != "" { 79 | start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "copyright"}, Value: c.Copyright}) 80 | } 81 | 82 | if c.Attribution != "" { 83 | start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "attribution"}, Value: c.Attribution}) 84 | } 85 | 86 | if c.License != "" { 87 | start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "license"}, Value: c.License}) 88 | } 89 | 90 | if err := e.EncodeToken(start); err != nil { 91 | return err 92 | } 93 | 94 | if err := marshalInnerChange(e, "create", c.Create); err != nil { 95 | return err 96 | } 97 | 98 | if err := marshalInnerChange(e, "modify", c.Modify); err != nil { 99 | return err 100 | } 101 | 102 | if err := marshalInnerChange(e, "delete", c.Delete); err != nil { 103 | return err 104 | } 105 | 106 | return e.EncodeToken(start.End()) 107 | } 108 | 109 | func marshalInnerChange(e *xml.Encoder, name string, o *OSM) error { 110 | if o == nil { 111 | return nil 112 | } 113 | 114 | t := xml.StartElement{Name: xml.Name{Local: name}} 115 | if err := e.EncodeToken(t); err != nil { 116 | return err 117 | } 118 | 119 | if err := o.marshalInnerXML(e); err != nil { 120 | return err 121 | } 122 | 123 | return e.EncodeToken(t.End()) 124 | } 125 | -------------------------------------------------------------------------------- /changeset.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import ( 4 | "encoding/xml" 5 | "time" 6 | ) 7 | 8 | // ChangesetID is the primary key for an osm changeset. 9 | type ChangesetID int64 10 | 11 | // ObjectID is a helper returning the object id for this changeset id. 12 | func (id ChangesetID) ObjectID() ObjectID { 13 | return ObjectID(changesetMask | (id << versionBits)) 14 | } 15 | 16 | // Changesets is a collection with some helper functions attached. 17 | type Changesets []*Changeset 18 | 19 | // A Changeset is a set of metadata around a set of osm changes. 20 | type Changeset struct { 21 | XMLName xmlNameJSONTypeCS `xml:"changeset" json:"type"` 22 | ID ChangesetID `xml:"id,attr" json:"id"` 23 | User string `xml:"user,attr" json:"user,omitempty"` 24 | UserID UserID `xml:"uid,attr" json:"uid,omitempty"` 25 | CreatedAt time.Time `xml:"created_at,attr" json:"created_at"` 26 | ClosedAt time.Time `xml:"closed_at,attr" json:"closed_at"` 27 | Open bool `xml:"open,attr" json:"open"` 28 | ChangesCount int `xml:"num_changes,attr,omitempty" json:"num_changes,omitempty"` 29 | MinLat float64 `xml:"min_lat,attr" json:"min_lat,omitempty"` 30 | MaxLat float64 `xml:"max_lat,attr" json:"max_lat,omitempty"` 31 | MinLon float64 `xml:"min_lon,attr" json:"min_lon,omitempty"` 32 | MaxLon float64 `xml:"max_lon,attr" json:"max_lon,omitempty"` 33 | CommentsCount int `xml:"comments_count,attr,omitempty" json:"comments_count,omitempty"` 34 | Tags Tags `xml:"tag" json:"tags,omitempty"` 35 | Discussion *ChangesetDiscussion `xml:"discussion,omitempty" json:"discussion,omitempty"` 36 | 37 | Change *Change `xml:"-" json:"change,omitempty"` 38 | } 39 | 40 | // ObjectID returns the object id of the changeset. 41 | func (c *Changeset) ObjectID() ObjectID { 42 | return c.ID.ObjectID() 43 | } 44 | 45 | // Bounds returns the bounds of the changeset as a bounds object. 46 | func (c *Changeset) Bounds() *Bounds { 47 | return &Bounds{ 48 | MinLat: c.MinLat, 49 | MaxLat: c.MaxLat, 50 | MinLon: c.MinLon, 51 | MaxLon: c.MaxLon, 52 | } 53 | } 54 | 55 | // Comment is a helper and returns the changeset comment from the tag. 56 | func (c *Changeset) Comment() string { 57 | return c.Tags.Find("comment") 58 | } 59 | 60 | // CreatedBy is a helper and returns the changeset created by from the tag. 61 | func (c *Changeset) CreatedBy() string { 62 | return c.Tags.Find("created_by") 63 | } 64 | 65 | // Locale is a helper and returns the changeset locale from the tag. 66 | func (c *Changeset) Locale() string { 67 | return c.Tags.Find("locale") 68 | } 69 | 70 | // Host is a helper and returns the changeset host from the tag. 71 | func (c *Changeset) Host() string { 72 | return c.Tags.Find("host") 73 | } 74 | 75 | // ImageryUsed is a helper and returns imagery used for the changeset from the tag. 76 | func (c *Changeset) ImageryUsed() string { 77 | return c.Tags.Find("imagery_used") 78 | } 79 | 80 | // Source is a helper and returns source for the changeset from the tag. 81 | func (c *Changeset) Source() string { 82 | return c.Tags.Find("source") 83 | } 84 | 85 | // Bot is a helper and returns true if the bot tag is a yes. 86 | func (c *Changeset) Bot() bool { 87 | // As of July 5, 2015: 300k yes, 123 no, 8 other 88 | return c.Tags.Find("bot") == "yes" 89 | } 90 | 91 | // IDs returns the ids of the changesets in the slice. 92 | func (cs Changesets) IDs() []ChangesetID { 93 | if len(cs) == 0 { 94 | return nil 95 | } 96 | 97 | r := make([]ChangesetID, 0, len(cs)) 98 | for _, c := range cs { 99 | r = append(r, c.ID) 100 | } 101 | 102 | return r 103 | } 104 | 105 | // ChangesetDiscussion is a conversation about a changeset. 106 | type ChangesetDiscussion struct { 107 | Comments []*ChangesetComment `xml:"comment" json:"comments"` 108 | } 109 | 110 | // ChangesetComment is a specific comment in a changeset discussion. 111 | type ChangesetComment struct { 112 | User string `xml:"user,attr" json:"user"` 113 | UserID UserID `xml:"uid,attr" json:"uid"` 114 | Timestamp time.Time `xml:"date,attr" json:"date"` 115 | Text string `xml:"text" json:"text"` 116 | } 117 | 118 | // MarshalXML implements the xml.Marshaller method to exclude this 119 | // whole element if the comments are empty. 120 | func (csd ChangesetDiscussion) MarshalXML(e *xml.Encoder, start xml.StartElement) error { 121 | if len(csd.Comments) == 0 { 122 | return nil 123 | } 124 | 125 | if err := e.EncodeToken(start); err != nil { 126 | return err 127 | } 128 | 129 | t := xml.StartElement{Name: xml.Name{Local: "comment"}} 130 | if err := e.EncodeElement(csd.Comments, t); err != nil { 131 | return err 132 | } 133 | 134 | return e.EncodeToken(start.End()) 135 | } 136 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: off 4 | patch: 5 | default: 6 | threshold: 50% 7 | 8 | precision: 2 9 | round: down 10 | range: "70...90" 11 | 12 | comment: false 13 | -------------------------------------------------------------------------------- /datasource.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | ) 7 | 8 | // A HistoryDatasourcer defines an interface to osm history data. 9 | type HistoryDatasourcer interface { 10 | NodeHistory(context.Context, NodeID) (Nodes, error) 11 | WayHistory(context.Context, WayID) (Ways, error) 12 | RelationHistory(context.Context, RelationID) (Relations, error) 13 | NotFound(error) bool 14 | } 15 | 16 | var errNotFound = errors.New("osm: feature not found") 17 | 18 | // A HistoryDatasource wraps maps to implement the HistoryDataSource interface. 19 | type HistoryDatasource struct { 20 | Nodes map[NodeID]Nodes 21 | Ways map[WayID]Ways 22 | Relations map[RelationID]Relations 23 | } 24 | 25 | var _ HistoryDatasourcer = &HistoryDatasource{} 26 | 27 | func (ds *HistoryDatasource) add(o *OSM, visible ...bool) { 28 | if o == nil { 29 | return 30 | } 31 | 32 | if len(o.Nodes) > 0 { 33 | if ds.Nodes == nil { 34 | ds.Nodes = make(map[NodeID]Nodes) 35 | } 36 | 37 | for _, n := range o.Nodes { 38 | if len(visible) == 1 { 39 | n.Visible = visible[0] 40 | } 41 | ds.Nodes[n.ID] = append(ds.Nodes[n.ID], n) 42 | } 43 | } 44 | 45 | if len(o.Ways) > 0 { 46 | if ds.Ways == nil { 47 | ds.Ways = make(map[WayID]Ways) 48 | } 49 | 50 | for _, w := range o.Ways { 51 | if len(visible) == 1 { 52 | w.Visible = visible[0] 53 | } 54 | ds.Ways[w.ID] = append(ds.Ways[w.ID], w) 55 | } 56 | } 57 | 58 | if len(o.Relations) > 0 { 59 | if ds.Relations == nil { 60 | ds.Relations = make(map[RelationID]Relations) 61 | } 62 | 63 | for _, r := range o.Relations { 64 | if len(visible) == 1 { 65 | r.Visible = visible[0] 66 | } 67 | ds.Relations[r.ID] = append(ds.Relations[r.ID], r) 68 | } 69 | } 70 | } 71 | 72 | // NodeHistory returns the history for the given id from the map. 73 | func (ds *HistoryDatasource) NodeHistory(ctx context.Context, id NodeID) (Nodes, error) { 74 | if ds.Nodes == nil { 75 | return nil, errNotFound 76 | } 77 | 78 | v := ds.Nodes[id] 79 | if v == nil { 80 | return nil, errNotFound 81 | } 82 | 83 | return v, nil 84 | } 85 | 86 | // WayHistory returns the history for the given id from the map. 87 | func (ds *HistoryDatasource) WayHistory(ctx context.Context, id WayID) (Ways, error) { 88 | if ds.Ways == nil { 89 | return nil, errNotFound 90 | } 91 | 92 | v := ds.Ways[id] 93 | if v == nil { 94 | return nil, errNotFound 95 | } 96 | 97 | return v, nil 98 | } 99 | 100 | // RelationHistory returns the history for the given id from the map. 101 | func (ds *HistoryDatasource) RelationHistory(ctx context.Context, id RelationID) (Relations, error) { 102 | if ds.Relations == nil { 103 | return nil, errNotFound 104 | } 105 | 106 | v := ds.Relations[id] 107 | if v == nil { 108 | return nil, errNotFound 109 | } 110 | 111 | return v, nil 112 | } 113 | 114 | // NotFound returns true if the error returned is a not found error. 115 | func (ds *HistoryDatasource) NotFound(err error) bool { 116 | return err == errNotFound 117 | } 118 | -------------------------------------------------------------------------------- /datasource_test.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | func TestHistoryDatasource(t *testing.T) { 9 | ctx := context.Background() 10 | 11 | t.Run("empty datasource", func(t *testing.T) { 12 | ds := &HistoryDatasource{} 13 | 14 | if _, err := ds.NodeHistory(ctx, 1); !ds.NotFound(err) { 15 | t.Errorf("should be not found error: %v", err) 16 | } 17 | 18 | if _, err := ds.WayHistory(ctx, 1); !ds.NotFound(err) { 19 | t.Errorf("should be not found error: %v", err) 20 | } 21 | 22 | if _, err := ds.RelationHistory(ctx, 1); !ds.NotFound(err) { 23 | t.Errorf("should be not found error: %v", err) 24 | } 25 | }) 26 | 27 | o := &OSM{ 28 | Nodes: Nodes{ 29 | {ID: 1, Version: 1}, 30 | {ID: 1, Version: 2}, 31 | }, 32 | Ways: Ways{ 33 | {ID: 1, Version: 1}, 34 | {ID: 1, Version: 2}, 35 | {ID: 1, Version: 3}, 36 | }, 37 | Relations: Relations{ 38 | {ID: 1, Version: 1}, 39 | {ID: 1, Version: 2}, 40 | {ID: 1, Version: 3}, 41 | {ID: 1, Version: 4}, 42 | }, 43 | } 44 | 45 | t.Run("non-empty datasource", func(t *testing.T) { 46 | ds := o.HistoryDatasource() 47 | 48 | ns, err := ds.NodeHistory(ctx, 1) 49 | if err != nil { 50 | t.Errorf("should not return error: %v", err) 51 | } 52 | 53 | if len(ns) != 2 { 54 | t.Errorf("incorrect nodes: %v", ns) 55 | } 56 | 57 | ws, err := ds.WayHistory(ctx, 1) 58 | if err != nil { 59 | t.Errorf("should not return error: %v", err) 60 | } 61 | 62 | if len(ws) != 3 { 63 | t.Errorf("incorrect ways: %v", ns) 64 | } 65 | 66 | rs, err := ds.RelationHistory(ctx, 1) 67 | if err != nil { 68 | t.Errorf("should not return error: %v", err) 69 | } 70 | 71 | if len(rs) != 4 { 72 | t.Errorf("incorrect relations: %v", ns) 73 | } 74 | }) 75 | 76 | t.Run("not found non-empty datasource", func(t *testing.T) { 77 | ds := o.HistoryDatasource() 78 | 79 | if _, err := ds.NodeHistory(ctx, 2); !ds.NotFound(err) { 80 | t.Errorf("should be not found error: %v", err) 81 | } 82 | 83 | if _, err := ds.WayHistory(ctx, 2); !ds.NotFound(err) { 84 | t.Errorf("should be not found error: %v", err) 85 | } 86 | 87 | if _, err := ds.RelationHistory(ctx, 2); !ds.NotFound(err) { 88 | t.Errorf("should be not found error: %v", err) 89 | } 90 | }) 91 | 92 | } 93 | -------------------------------------------------------------------------------- /diff.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import "encoding/xml" 4 | 5 | // Diff represents a difference of osm data with old and new data. 6 | type Diff struct { 7 | XMLName xml.Name `xml:"osm"` 8 | Actions Actions `xml:"action"` 9 | Changesets Changesets `xml:"changeset"` 10 | } 11 | 12 | // Actions is a set of diff actions. 13 | type Actions []Action 14 | 15 | // Action is an explicit create, modify or delete action with 16 | // old and new data if applicable. Different properties of this 17 | // struct will be populated depending on the action. 18 | // Create: da.OSM will contain the new element 19 | // Modify: da.Old and da.New will contain the old and new elements. 20 | // Delete: da.Old and da.New will contain the old and new elements. 21 | type Action struct { 22 | Type ActionType `xml:"type,attr"` 23 | *OSM `xml:",omitempty"` 24 | Old *OSM `xml:"old,omitempty"` 25 | New *OSM `xml:"new,omitempty"` 26 | } 27 | 28 | // UnmarshalXML converts xml into a diff action. 29 | func (a *Action) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { 30 | for _, attr := range start.Attr { 31 | if attr.Name.Local == "type" { 32 | a.Type = ActionType(attr.Value) 33 | break 34 | } 35 | } 36 | 37 | for { 38 | token, err := d.Token() 39 | if err != nil { 40 | break 41 | } 42 | 43 | start, ok := token.(xml.StartElement) 44 | if !ok { 45 | continue 46 | } 47 | 48 | switch start.Name.Local { 49 | case "old": 50 | a.Old = &OSM{} 51 | if err := d.DecodeElement(a.Old, &start); err != nil { 52 | return err 53 | } 54 | case "new": 55 | a.New = &OSM{} 56 | if err := d.DecodeElement(a.New, &start); err != nil { 57 | return err 58 | } 59 | case "node": 60 | n := &Node{} 61 | if err := d.DecodeElement(&n, &start); err != nil { 62 | return err 63 | } 64 | a.OSM = &OSM{Nodes: Nodes{n}} 65 | case "way": 66 | w := &Way{} 67 | if err := d.DecodeElement(&w, &start); err != nil { 68 | return err 69 | } 70 | a.OSM = &OSM{Ways: Ways{w}} 71 | case "relation": 72 | r := &Relation{} 73 | if err := d.DecodeElement(&r, &start); err != nil { 74 | return err 75 | } 76 | a.OSM = &OSM{Relations: Relations{r}} 77 | } 78 | } 79 | 80 | return nil 81 | } 82 | 83 | // MarshalXML converts a diff action to xml creating the proper structures. 84 | func (a Action) MarshalXML(e *xml.Encoder, start xml.StartElement) error { 85 | start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "type"}, Value: string(a.Type)}) 86 | if err := e.EncodeToken(start); err != nil { 87 | return err 88 | } 89 | 90 | if a.OSM != nil { 91 | if err := a.OSM.marshalInnerElementsXML(e); err != nil { 92 | return err 93 | } 94 | } 95 | 96 | if a.Old != nil { 97 | if err := marshalInnerChange(e, "old", a.Old); err != nil { 98 | return err 99 | } 100 | } 101 | 102 | if a.New != nil { 103 | if err := marshalInnerChange(e, "new", a.New); err != nil { 104 | return err 105 | } 106 | } 107 | 108 | return e.EncodeToken(start.End()) 109 | } 110 | 111 | // ActionType is a strong type for the different diff actions. 112 | type ActionType string 113 | 114 | // The different types of diff actions. 115 | const ( 116 | ActionCreate ActionType = "create" 117 | ActionModify ActionType = "modify" 118 | ActionDelete ActionType = "delete" 119 | ) 120 | -------------------------------------------------------------------------------- /diff_test.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import ( 4 | "encoding/xml" 5 | "os" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestDiff_MarshalXML(t *testing.T) { 11 | data := []byte(` 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | `) 24 | 25 | diff := &Diff{} 26 | err := xml.Unmarshal(data, &diff) 27 | if err != nil { 28 | t.Errorf("unmarshal error: %v", err) 29 | } 30 | 31 | if l := len(diff.Actions); l != 2 { 32 | t.Errorf("incorrect num of actions: %v", l) 33 | } 34 | 35 | marshalled, err := xml.MarshalIndent(diff, "", " ") 36 | if err != nil { 37 | t.Errorf("marshal error: %v", err) 38 | } 39 | 40 | if !reflect.DeepEqual(marshalled, data) { 41 | t.Errorf("incorrect marshal") 42 | t.Logf("%v", string(marshalled)) 43 | t.Logf("%v", string(data)) 44 | } 45 | 46 | // specifics 47 | diff = &Diff{} 48 | _, err = xml.Marshal(diff) 49 | if err != nil { 50 | t.Errorf("unable to marshal: %v", err) 51 | } 52 | 53 | // create 54 | diff.Actions = append(diff.Actions, Action{ 55 | Type: ActionCreate, 56 | OSM: &OSM{Nodes: Nodes{{ID: 1}}}, 57 | }) 58 | _, err = xml.Marshal(diff) 59 | if err != nil { 60 | t.Errorf("unable to marshal: %v", err) 61 | } 62 | 63 | // modify 64 | diff.Actions = append(diff.Actions, Action{ 65 | Type: ActionModify, 66 | Old: &OSM{Nodes: Nodes{{ID: 1}}}, 67 | New: &OSM{Nodes: Nodes{{ID: 1}}}, 68 | }) 69 | _, err = xml.Marshal(diff) 70 | if err != nil { 71 | t.Errorf("unable to marshal: %v", err) 72 | } 73 | } 74 | 75 | func TestDiff(t *testing.T) { 76 | data, err := os.ReadFile("testdata/annotated_diff.xml") 77 | if err != nil { 78 | t.Fatalf("unable to read file: %v", err) 79 | } 80 | 81 | diff := &Diff{} 82 | err = xml.Unmarshal(data, &diff) 83 | if err != nil { 84 | t.Errorf("unable to unmarshal: %v", err) 85 | } 86 | 87 | if l := len(diff.Actions); l != 1094 { 88 | t.Fatalf("incorrect number of actions, got %d", l) 89 | } 90 | 91 | // create way 92 | if at := diff.Actions[1075].Type; at != ActionCreate { 93 | t.Errorf("not a create action, %v", at) 94 | } 95 | 96 | way := diff.Actions[1075].Ways[0] 97 | 98 | if id := way.ID; id != 180669361 { 99 | t.Errorf("incorrect way id, got %v", id) 100 | } 101 | 102 | // modify relation 103 | if at := diff.Actions[1088].Type; at != ActionModify { 104 | t.Errorf("not a modify action, %v", at) 105 | } 106 | 107 | oldRelation := diff.Actions[1088].Old.Relations[0] 108 | newRelation := diff.Actions[1088].New.Relations[0] 109 | 110 | if oldRelation.ID != newRelation.ID { 111 | t.Errorf("modify diff is not correct") 112 | t.Logf("old: %v", oldRelation) 113 | t.Logf("new: %v", newRelation) 114 | } 115 | 116 | // delete node 117 | if at := diff.Actions[44].Type; at != ActionDelete { 118 | t.Fatalf("not a delete action, %v", at) 119 | } 120 | 121 | oldNode := diff.Actions[44].Old.Nodes[0] 122 | newNode := diff.Actions[44].New.Nodes[0] 123 | 124 | if oldNode.ID != newNode.ID { 125 | t.Errorf("delete diff is not correct") 126 | t.Logf("old: %v", oldNode) 127 | t.Logf("new: %v", newNode) 128 | } 129 | 130 | if newNode.Visible { 131 | t.Errorf("new node must not be visible") 132 | t.Logf("old: %v", oldNode) 133 | t.Logf("new: %v", newNode) 134 | } 135 | 136 | // should marshal the unmarshalled data 137 | _, err = xml.Marshal(diff) 138 | if err != nil { 139 | t.Errorf("unable to marshal: %v", err) 140 | } 141 | } 142 | 143 | func BenchmarkDiff_Marshal(b *testing.B) { 144 | data, err := os.ReadFile("testdata/annotated_diff.xml") 145 | if err != nil { 146 | b.Fatalf("unable to read file: %v", err) 147 | } 148 | 149 | diff := &Diff{} 150 | err = xml.Unmarshal(data, &diff) 151 | if err != nil { 152 | b.Fatalf("unmarshal error: %v", err) 153 | } 154 | 155 | b.ReportAllocs() 156 | b.ResetTimer() 157 | for i := 0; i < b.N; i++ { 158 | _, err := xml.Marshal(diff) 159 | if err != nil { 160 | b.Fatalf("marshal error: %v", err) 161 | } 162 | } 163 | } 164 | 165 | func BenchmarkDiff_Unmarshal(b *testing.B) { 166 | data, err := os.ReadFile("testdata/annotated_diff.xml") 167 | if err != nil { 168 | b.Fatalf("unable to read file: %v", err) 169 | } 170 | 171 | b.ReportAllocs() 172 | b.ResetTimer() 173 | for i := 0; i < b.N; i++ { 174 | diff := &Diff{} 175 | err := xml.Unmarshal(data, &diff) 176 | if err != nil { 177 | b.Fatalf("unmarshal error: %v", err) 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/paulmach/osm 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/datadog/czlib v0.0.0-20160811164712-4bc9a24e37f2 7 | github.com/paulmach/orb v0.1.3 8 | github.com/paulmach/protoscan v0.2.1 9 | golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0 10 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 11 | google.golang.org/protobuf v1.27.1 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/datadog/czlib v0.0.0-20160811164712-4bc9a24e37f2 h1:ISaMhBq2dagaoptFGUyywT5SzpysCbHofX3sCNw1djo= 2 | github.com/datadog/czlib v0.0.0-20160811164712-4bc9a24e37f2/go.mod h1:2yDaWzisHKoQoxm+EU4YgKBaD7g1M0pxy7THWG44Lro= 3 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 4 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 5 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 6 | github.com/paulmach/orb v0.1.3 h1:Wa1nzU269Zv7V9paVEY1COWW8FCqv4PC/KJRbJSimpM= 7 | github.com/paulmach/orb v0.1.3/go.mod h1:VFlX/8C+IQ1p6FTRRKzKoOPJnvEtA5G0Veuqwbu//Vk= 8 | github.com/paulmach/protoscan v0.2.1 h1:rM0FpcTjUMvPUNk2BhPJrreDKetq43ChnL+x1sRg8O8= 9 | github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= 10 | golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0 h1:xQwXv67TxFo9nC1GJFyab5eq/5B590r6RlnL/G8Sz7w= 11 | golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 12 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 13 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 14 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 15 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 16 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 17 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 18 | -------------------------------------------------------------------------------- /internal/mputil/join.go: -------------------------------------------------------------------------------- 1 | package mputil 2 | 3 | // Join will join a set of segments into a set of connected MultiSegments. 4 | func Join(segments []Segment) []MultiSegment { 5 | lists := []MultiSegment{} 6 | segments = compact(segments) 7 | 8 | // matches are removed from `segments` and put into the current 9 | // group, so when `segments` is empty we're done. 10 | for len(segments) != 0 { 11 | current := MultiSegment{segments[len(segments)-1]} 12 | segments = segments[:len(segments)-1] 13 | 14 | // if the current group is a ring, we're done. 15 | // else add in all the lines. 16 | for len(segments) != 0 && !current.First().Equal(current.Last()) { 17 | first := current.First() 18 | last := current.Last() 19 | 20 | foundAt := -1 21 | for i, segment := range segments { 22 | if last.Equal(segment.First()) { 23 | // nice fit at the end of current 24 | 25 | segment.Line = segment.Line[1:] 26 | current = append(current, segment) 27 | foundAt = i 28 | break 29 | } else if last.Equal(segment.Last()) { 30 | // reverse it and it'll fit at the end 31 | segment.Reverse() 32 | 33 | segment.Line = segment.Line[1:] 34 | current = append(current, segment) 35 | foundAt = i 36 | break 37 | } else if first.Equal(segment.Last()) { 38 | // nice fit at the start of current 39 | segment.Line = segment.Line[:len(segment.Line)-1] 40 | current = append(MultiSegment{segment}, current...) 41 | 42 | foundAt = i 43 | break 44 | } else if first.Equal(segment.First()) { 45 | // reverse it and it'll fit at the start 46 | segment.Reverse() 47 | 48 | segment.Line = segment.Line[:len(segment.Line)-1] 49 | current = append(MultiSegment{segment}, current...) 50 | 51 | foundAt = i 52 | break 53 | } 54 | } 55 | 56 | if foundAt == -1 { 57 | break // Invalid geometry (dangling way, unclosed ring) 58 | } 59 | 60 | // remove the found/matched segment from the list. 61 | if foundAt < len(segments)/2 { 62 | // first half, shift up 63 | for i := foundAt; i > 0; i-- { 64 | segments[i] = segments[i-1] 65 | } 66 | segments = segments[1:] 67 | } else { 68 | // second half, shift down 69 | for i := foundAt + 1; i < len(segments); i++ { 70 | segments[i-1] = segments[i] 71 | } 72 | segments = segments[:len(segments)-1] 73 | } 74 | } 75 | 76 | lists = append(lists, current) 77 | } 78 | 79 | return lists 80 | } 81 | 82 | func compact(ms MultiSegment) MultiSegment { 83 | at := 0 84 | for _, s := range ms { 85 | if len(s.Line) <= 1 { 86 | continue 87 | } 88 | 89 | ms[at] = s 90 | at++ 91 | } 92 | 93 | return ms[:at] 94 | } 95 | -------------------------------------------------------------------------------- /internal/mputil/mputil.go: -------------------------------------------------------------------------------- 1 | package mputil 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/paulmach/orb" 7 | "github.com/paulmach/osm" 8 | ) 9 | 10 | // Segment is a section of a multipolygon with some extra information 11 | // on the member it came from. 12 | type Segment struct { 13 | Index uint32 14 | Orientation orb.Orientation 15 | Reversed bool 16 | Line orb.LineString 17 | } 18 | 19 | // Reverse will reverse the line string of the segment. 20 | func (s *Segment) Reverse() { 21 | s.Reversed = !s.Reversed 22 | s.Line.Reverse() 23 | } 24 | 25 | // First returns the first point in the segment linestring. 26 | func (s Segment) First() orb.Point { 27 | return s.Line[0] 28 | } 29 | 30 | // Last returns the last point in the segment linestring. 31 | func (s Segment) Last() orb.Point { 32 | return s.Line[len(s.Line)-1] 33 | } 34 | 35 | // MultiSegment is an ordered set of segments that form a continuous 36 | // section of a multipolygon. 37 | type MultiSegment []Segment 38 | 39 | // First returns the first point in the list of linestrings. 40 | func (ms MultiSegment) First() orb.Point { 41 | return ms[0].Line[0] 42 | } 43 | 44 | // Last returns the last point in the list of linestrings. 45 | func (ms MultiSegment) Last() orb.Point { 46 | line := ms[len(ms)-1].Line 47 | return line[len(line)-1] 48 | } 49 | 50 | // LineString converts a multisegment into a geo linestring object. 51 | func (ms MultiSegment) LineString() orb.LineString { 52 | length := 0 53 | for _, s := range ms { 54 | length += len(s.Line) 55 | } 56 | 57 | line := make(orb.LineString, 0, length) 58 | for _, s := range ms { 59 | line = append(line, s.Line...) 60 | } 61 | 62 | return line 63 | } 64 | 65 | // Ring converts the multisegment to a ring of the given orientation. 66 | // It uses the orientation on the members if possible. 67 | func (ms MultiSegment) Ring(o orb.Orientation) orb.Ring { 68 | length := 0 69 | for _, s := range ms { 70 | length += len(s.Line) 71 | } 72 | 73 | ring := make(orb.Ring, 0, length) 74 | 75 | haveOrient := false 76 | reversed := false 77 | for _, s := range ms { 78 | if s.Orientation != 0 { 79 | haveOrient = true 80 | 81 | // if s.Orientation == o && s.Reversed { 82 | // reversed = true 83 | // } 84 | // if s.Orientation != 0 && !s.Reversed { 85 | // reversed = true 86 | // } 87 | 88 | if (s.Orientation == o) == s.Reversed { 89 | reversed = true 90 | } 91 | } 92 | 93 | ring = append(ring, s.Line...) 94 | } 95 | 96 | if (haveOrient && reversed) || (!haveOrient && ring.Orientation() != o) { 97 | ring.Reverse() 98 | } 99 | 100 | return ring 101 | } 102 | 103 | // Orientation computes the orientation of a multisegment like if it was ring. 104 | func (ms MultiSegment) Orientation() orb.Orientation { 105 | area := 0.0 106 | prev := ms.First() 107 | 108 | // implicitly move everything to near the origin to help with roundoff 109 | offset := prev 110 | for _, segment := range ms { 111 | for _, point := range segment.Line { 112 | area += (prev[0]-offset[0])*(point[1]-offset[1]) - 113 | (point[0]-offset[0])*(prev[1]-offset[1]) 114 | 115 | prev = point 116 | } 117 | } 118 | 119 | if area > 0 { 120 | return orb.CCW 121 | } 122 | 123 | return orb.CW 124 | } 125 | 126 | // Group will take the members and group them by inner our outer parts 127 | // of the relation. Will also build the way geometry. 128 | func Group( 129 | members osm.Members, 130 | ways map[osm.WayID]*osm.Way, 131 | at time.Time, 132 | ) (outer, inner []Segment, tainted bool) { 133 | for i, m := range members { 134 | if m.Type != osm.TypeWay { 135 | continue 136 | } 137 | 138 | w := ways[osm.WayID(m.Ref)] 139 | if w == nil { 140 | tainted = true 141 | continue // could be not found error, or something else. 142 | } 143 | 144 | line := w.LineStringAt(at) 145 | if len(line) != len(w.Nodes) { 146 | tainted = true 147 | } 148 | 149 | // zero length ways exist and don't make any sense when 150 | // building the multipolygon rings. 151 | if len(line) == 0 { 152 | continue 153 | } 154 | 155 | l := Segment{ 156 | Index: uint32(i), 157 | Orientation: m.Orientation, 158 | Reversed: false, 159 | Line: line, 160 | } 161 | 162 | if m.Role == "outer" { 163 | if l.Orientation == orb.CW { 164 | l.Reverse() 165 | } 166 | outer = append(outer, l) 167 | } else if m.Role == "inner" { 168 | if l.Orientation == orb.CCW { 169 | l.Reverse() 170 | } 171 | inner = append(inner, l) 172 | } 173 | } 174 | 175 | return outer, inner, tainted 176 | } 177 | -------------------------------------------------------------------------------- /json.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import ( 4 | "encoding/json" 5 | "encoding/xml" 6 | ) 7 | 8 | // CustomJSONMarshaler can be set to have the code use a different 9 | // json marshaler than the default in the standard library. 10 | // One use case in enabling `github.com/json-iterator/go` 11 | // with something like this: 12 | // 13 | // import ( 14 | // jsoniter "github.com/json-iterator/go" 15 | // "github.com/paulmach/osm" 16 | // ) 17 | // 18 | // var c = jsoniter.Config{ 19 | // EscapeHTML: true, 20 | // SortMapKeys: false, 21 | // MarshalFloatWith6Digits: true, 22 | // }.Froze() 23 | // 24 | // osm.CustomJSONMarshaler = c 25 | // osm.CustomJSONUnmarshaler = c 26 | // 27 | // Note that any errors encountered during marshaling will be different. 28 | var CustomJSONMarshaler interface { 29 | Marshal(v interface{}) ([]byte, error) 30 | } 31 | 32 | // CustomJSONUnmarshaler can be set to have the code use a different 33 | // json unmarshaler than the default in the standard library. 34 | // One use case in enabling `github.com/json-iterator/go` 35 | // with something like this: 36 | // 37 | // import ( 38 | // jsoniter "github.com/json-iterator/go" 39 | // "github.com/paulmach/osm" 40 | // ) 41 | // 42 | // var c = jsoniter.Config{ 43 | // EscapeHTML: true, 44 | // SortMapKeys: false, 45 | // MarshalFloatWith6Digits: true, 46 | // }.Froze() 47 | // 48 | // osm.CustomJSONMarshaler = c 49 | // osm.CustomJSONUnmarshaler = c 50 | // 51 | // Note that any errors encountered during unmarshaling will be different. 52 | var CustomJSONUnmarshaler interface { 53 | Unmarshal(data []byte, v interface{}) error 54 | } 55 | 56 | func marshalJSON(v interface{}) ([]byte, error) { 57 | if CustomJSONMarshaler == nil { 58 | return json.Marshal(v) 59 | } 60 | 61 | return CustomJSONMarshaler.Marshal(v) 62 | } 63 | 64 | func unmarshalJSON(data []byte, v interface{}) error { 65 | if CustomJSONUnmarshaler == nil { 66 | return json.Unmarshal(data, v) 67 | } 68 | 69 | return CustomJSONUnmarshaler.Unmarshal(data, v) 70 | } 71 | 72 | type nocopyRawMessage []byte 73 | 74 | func (m *nocopyRawMessage) UnmarshalJSON(data []byte) error { 75 | *m = data 76 | return nil 77 | } 78 | 79 | // xmlNameJSONTypeNode is kind of a hack to encode the proper json 80 | // object type attribute for this struct type. 81 | type xmlNameJSONTypeNode xml.Name 82 | 83 | func (x xmlNameJSONTypeNode) MarshalJSON() ([]byte, error) { 84 | return []byte(`"node"`), nil 85 | } 86 | 87 | func (x xmlNameJSONTypeNode) UnmarshalJSON(data []byte) error { 88 | return nil 89 | } 90 | 91 | // xmlNameJSONTypeWay is kind of a hack to encode the proper json 92 | // object type attribute for this struct type. 93 | type xmlNameJSONTypeWay xml.Name 94 | 95 | func (x xmlNameJSONTypeWay) MarshalJSON() ([]byte, error) { 96 | return []byte(`"way"`), nil 97 | } 98 | 99 | func (x xmlNameJSONTypeWay) UnmarshalJSON(data []byte) error { 100 | return nil 101 | } 102 | 103 | // xmlNameJSONTypeRel is kind of a hack to encode the proper json 104 | // object type attribute for this struct type. 105 | type xmlNameJSONTypeRel xml.Name 106 | 107 | func (x xmlNameJSONTypeRel) MarshalJSON() ([]byte, error) { 108 | return []byte(`"relation"`), nil 109 | } 110 | 111 | func (x xmlNameJSONTypeRel) UnmarshalJSON(data []byte) error { 112 | return nil 113 | } 114 | 115 | // xmlNameJSONTypeCS is kind of a hack to encode the proper json 116 | // object type attribute for this struct type. 117 | type xmlNameJSONTypeCS xml.Name 118 | 119 | func (x xmlNameJSONTypeCS) MarshalJSON() ([]byte, error) { 120 | return []byte(`"changeset"`), nil 121 | } 122 | 123 | func (x xmlNameJSONTypeCS) UnmarshalJSON(data []byte) error { 124 | return nil 125 | } 126 | 127 | // xmlNameJSONTypeUser is kind of a hack to encode the proper json 128 | // object type attribute for this struct type. 129 | type xmlNameJSONTypeUser xml.Name 130 | 131 | func (x xmlNameJSONTypeUser) MarshalJSON() ([]byte, error) { 132 | return []byte(`"user"`), nil 133 | } 134 | 135 | func (x xmlNameJSONTypeUser) UnmarshalJSON(data []byte) error { 136 | return nil 137 | } 138 | 139 | // xmlNameJSONTypeNote is kind of a hack to encode the proper json 140 | // object type attribute for this struct type. 141 | type xmlNameJSONTypeNote xml.Name 142 | 143 | func (x xmlNameJSONTypeNote) MarshalJSON() ([]byte, error) { 144 | return []byte(`"note"`), nil 145 | } 146 | 147 | func (x xmlNameJSONTypeNote) UnmarshalJSON(data []byte) error { 148 | return nil 149 | } 150 | -------------------------------------------------------------------------------- /node.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import ( 4 | "sort" 5 | "time" 6 | 7 | "github.com/paulmach/orb" 8 | ) 9 | 10 | // NodeID corresponds the primary key of a node. 11 | // The node id + version uniquely identify a node. 12 | type NodeID int64 13 | 14 | // ObjectID is a helper returning the object id for this node id. 15 | func (id NodeID) ObjectID(v int) ObjectID { 16 | return ObjectID(id.ElementID(v)) 17 | } 18 | 19 | // FeatureID is a helper returning the feature id for this node id. 20 | func (id NodeID) FeatureID() FeatureID { 21 | return FeatureID(nodeMask | (id << versionBits)) 22 | } 23 | 24 | // ElementID is a helper to convert the id to an element id. 25 | func (id NodeID) ElementID(v int) ElementID { 26 | return id.FeatureID().ElementID(v) 27 | } 28 | 29 | // Node is an osm point and allows for marshalling to/from osm xml. 30 | type Node struct { 31 | XMLName xmlNameJSONTypeNode `xml:"node" json:"type"` 32 | ID NodeID `xml:"id,attr" json:"id"` 33 | Lat float64 `xml:"lat,attr" json:"lat"` 34 | Lon float64 `xml:"lon,attr" json:"lon"` 35 | User string `xml:"user,attr" json:"user,omitempty"` 36 | UserID UserID `xml:"uid,attr" json:"uid,omitempty"` 37 | Visible bool `xml:"visible,attr" json:"visible"` 38 | Version int `xml:"version,attr" json:"version,omitempty"` 39 | ChangesetID ChangesetID `xml:"changeset,attr" json:"changeset,omitempty"` 40 | Timestamp time.Time `xml:"timestamp,attr" json:"timestamp"` 41 | Tags Tags `xml:"tag" json:"tags,omitempty"` 42 | 43 | // Committed, is the estimated time this object was committed 44 | // and made visible in the central OSM database. 45 | Committed *time.Time `xml:"committed,attr,omitempty" json:"committed,omitempty"` 46 | } 47 | 48 | // ObjectID returns the object id of the node. 49 | func (n *Node) ObjectID() ObjectID { 50 | return n.ID.ObjectID(n.Version) 51 | } 52 | 53 | // FeatureID returns the feature id of the node. 54 | func (n *Node) FeatureID() FeatureID { 55 | return n.ID.FeatureID() 56 | } 57 | 58 | // ElementID returns the element id of the node. 59 | func (n *Node) ElementID() ElementID { 60 | return n.ID.ElementID(n.Version) 61 | } 62 | 63 | // CommittedAt returns the best estimate on when this element 64 | // became was written/committed into the database. 65 | func (n *Node) CommittedAt() time.Time { 66 | if n.Committed != nil { 67 | return *n.Committed 68 | } 69 | 70 | return n.Timestamp 71 | } 72 | 73 | // TagMap returns the element tags as a key/value map. 74 | func (n *Node) TagMap() map[string]string { 75 | return n.Tags.Map() 76 | } 77 | 78 | // Point returns the orb.Point location for the node. 79 | // Will be (0, 0) for "deleted" nodes. 80 | func (n *Node) Point() orb.Point { 81 | return orb.Point{n.Lon, n.Lat} 82 | } 83 | 84 | // Nodes is a list of nodes with helper functions on top. 85 | type Nodes []*Node 86 | 87 | // IDs returns the ids for all the ways. 88 | func (ns Nodes) IDs() []NodeID { 89 | result := make([]NodeID, len(ns)) 90 | for i, n := range ns { 91 | result[i] = n.ID 92 | } 93 | 94 | return result 95 | } 96 | 97 | // FeatureIDs returns the feature ids for all the nodes. 98 | func (ns Nodes) FeatureIDs() FeatureIDs { 99 | r := make(FeatureIDs, len(ns)) 100 | for i, n := range ns { 101 | r[i] = n.FeatureID() 102 | } 103 | 104 | return r 105 | } 106 | 107 | // ElementIDs returns the element ids for all the nodes. 108 | func (ns Nodes) ElementIDs() ElementIDs { 109 | r := make(ElementIDs, len(ns)) 110 | for i, n := range ns { 111 | r[i] = n.ElementID() 112 | } 113 | 114 | return r 115 | } 116 | 117 | type nodesSort Nodes 118 | 119 | // SortByIDVersion will sort the set of nodes first by id and then version 120 | // in ascending order. 121 | func (ns Nodes) SortByIDVersion() { 122 | sort.Sort(nodesSort(ns)) 123 | } 124 | 125 | func (ns nodesSort) Len() int { return len(ns) } 126 | func (ns nodesSort) Swap(i, j int) { ns[i], ns[j] = ns[j], ns[i] } 127 | func (ns nodesSort) Less(i, j int) bool { 128 | if ns[i].ID == ns[j].ID { 129 | return ns[i].Version < ns[j].Version 130 | } 131 | 132 | return ns[i].ID < ns[j].ID 133 | } 134 | -------------------------------------------------------------------------------- /node_test.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "encoding/xml" 7 | "reflect" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestNode(t *testing.T) { 13 | data := []byte(``) 14 | 15 | n := Node{} 16 | err := xml.Unmarshal(data, &n) 17 | if err != nil { 18 | t.Fatalf("unmarshal error: %v", err) 19 | } 20 | 21 | if v := n.ID; v != 123 { 22 | t.Errorf("incorrect id, got %v", v) 23 | } 24 | 25 | if v := n.ChangesetID; v != 456 { 26 | t.Errorf("incorrect changeset, got %v", v) 27 | } 28 | 29 | if v := n.Timestamp; v != time.Date(2014, 4, 10, 0, 43, 05, 0, time.UTC) { 30 | t.Errorf("incorrect timestamp, got %v", v) 31 | } 32 | 33 | if v := n.Version; v != 1 { 34 | t.Errorf("incorrect version, got %v", v) 35 | } 36 | 37 | if v := n.Visible; !v { 38 | t.Errorf("incorrect visible, got %v", v) 39 | } 40 | 41 | if v := n.User; v != "user" { 42 | t.Errorf("incorrect user, got %v", v) 43 | } 44 | 45 | if v := n.UserID; v != 1357 { 46 | t.Errorf("incorrect user id, got %v", v) 47 | } 48 | 49 | if v := n.Lat; v != 50.7107023 { 50 | t.Errorf("incorrect lat, got %v", v) 51 | } 52 | 53 | if v := n.Lon; v != 6.0043943 { 54 | t.Errorf("incorrect lon, got %v", v) 55 | } 56 | } 57 | 58 | func TestNode_MarshalJSON(t *testing.T) { 59 | n := Node{ 60 | ID: 123, 61 | } 62 | 63 | data, err := json.Marshal(n) 64 | if err != nil { 65 | t.Fatalf("marshal error: %v", err) 66 | } 67 | 68 | if !bytes.Equal(data, []byte(`{"type":"node","id":123,"lat":0,"lon":0,"visible":false,"timestamp":"0001-01-01T00:00:00Z"}`)) { 69 | t.Errorf("incorrect json: %v", string(data)) 70 | } 71 | } 72 | 73 | func TestNode_MarshalXML(t *testing.T) { 74 | n := Node{ 75 | ID: 123, 76 | } 77 | 78 | data, err := xml.Marshal(n) 79 | if err != nil { 80 | t.Fatalf("xml marshal error: %v", err) 81 | } 82 | 83 | expected := `` 84 | if !bytes.Equal(data, []byte(expected)) { 85 | t.Errorf("incorrect marshal, got: %s", string(data)) 86 | } 87 | } 88 | 89 | func TestNodes_ids(t *testing.T) { 90 | ns := Nodes{ 91 | {ID: 1, Version: 3}, 92 | {ID: 2, Version: 4}, 93 | } 94 | 95 | eids := ElementIDs{NodeID(1).ElementID(3), NodeID(2).ElementID(4)} 96 | if ids := ns.ElementIDs(); !reflect.DeepEqual(ids, eids) { 97 | t.Errorf("incorrect element ids: %v", ids) 98 | } 99 | 100 | fids := FeatureIDs{NodeID(1).FeatureID(), NodeID(2).FeatureID()} 101 | if ids := ns.FeatureIDs(); !reflect.DeepEqual(ids, fids) { 102 | t.Errorf("incorrect feature ids: %v", ids) 103 | } 104 | 105 | nids := []NodeID{1, 2} 106 | if ids := ns.IDs(); !reflect.DeepEqual(ids, nids) { 107 | t.Errorf("incorrect node ids: %v", nids) 108 | } 109 | } 110 | 111 | func TestNodes_SortByIDVersion(t *testing.T) { 112 | ns := Nodes{ 113 | {ID: 7, Version: 3}, 114 | {ID: 2, Version: 4}, 115 | {ID: 5, Version: 2}, 116 | {ID: 5, Version: 3}, 117 | {ID: 5, Version: 4}, 118 | {ID: 3, Version: 4}, 119 | {ID: 4, Version: 4}, 120 | {ID: 9, Version: 4}, 121 | } 122 | 123 | ns.SortByIDVersion() 124 | 125 | eids := ElementIDs{ 126 | NodeID(2).ElementID(4), 127 | NodeID(3).ElementID(4), 128 | NodeID(4).ElementID(4), 129 | NodeID(5).ElementID(2), 130 | NodeID(5).ElementID(3), 131 | NodeID(5).ElementID(4), 132 | NodeID(7).ElementID(3), 133 | NodeID(9).ElementID(4), 134 | } 135 | 136 | if ids := ns.ElementIDs(); !reflect.DeepEqual(ids, eids) { 137 | t.Errorf("incorrect sort: %v", eids) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /note.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import ( 4 | "encoding/xml" 5 | "time" 6 | ) 7 | 8 | // NoteID is the unique identifier for an osm note. 9 | type NoteID int64 10 | 11 | // ObjectID is a helper returning the object id for this note id. 12 | func (id NoteID) ObjectID() ObjectID { 13 | return ObjectID(noteMask | (id << versionBits)) 14 | } 15 | 16 | const dateLayout = "2006-01-02 15:04:05 MST" 17 | 18 | // Date is an object to decode the date format used in the osm notes xml api. 19 | // The format is '2006-01-02 15:04:05 MST'. 20 | type Date struct { 21 | time.Time 22 | } 23 | 24 | // UnmarshalXML is meant to decode the osm note date formation of 25 | // '2006-01-02 15:04:05 MST' into a time.Time object. 26 | func (d *Date) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) error { 27 | var s string 28 | err := dec.DecodeElement(&s, &start) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | d.Time, err = time.Parse(dateLayout, s) 34 | return err 35 | } 36 | 37 | // MarshalXML is meant to encode the time.Time into the osm note date formation 38 | // of '2006-01-02 15:04:05 MST'. 39 | func (d Date) MarshalXML(e *xml.Encoder, start xml.StartElement) error { 40 | return e.EncodeElement(d.Format(dateLayout), start) 41 | } 42 | 43 | // MarshalJSON will return null if the date is empty. 44 | func (d Date) MarshalJSON() ([]byte, error) { 45 | if d.IsZero() { 46 | return []byte(`null`), nil 47 | } 48 | return marshalJSON(d.Time) 49 | } 50 | 51 | // Notes is a collection of notes with some helpers attached. 52 | type Notes []*Note 53 | 54 | // Note is information for other mappers dropped at a map location. 55 | type Note struct { 56 | XMLName xmlNameJSONTypeNote `xml:"note" json:"type"` 57 | ID NoteID `xml:"id" json:"id"` 58 | Lat float64 `xml:"lat,attr" json:"lat"` 59 | Lon float64 `xml:"lon,attr" json:"lon"` 60 | URL string `xml:"url" json:"url,omitempty"` 61 | CommentURL string `xml:"comment_url" json:"comment_url,omitempty"` 62 | CloseURL string `xml:"close_url" json:"close_url,omitempty"` 63 | ReopenURL string `xml:"reopen_url" json:"reopen_url,omitempty"` 64 | DateCreated Date `xml:"date_created" json:"date_created"` 65 | DateClosed Date `xml:"date_closed" json:"date_closed,omitempty"` 66 | Status NoteStatus `xml:"status" json:"status,omitempty"` 67 | Comments []*NoteComment `xml:"comments>comment" json:"comments"` 68 | } 69 | 70 | // NoteComment is a comment on a note. 71 | type NoteComment struct { 72 | XMLName xml.Name `xml:"comment" json:"-"` 73 | Date Date `xml:"date" json:"date"` 74 | UserID UserID `xml:"uid" json:"uid,omitempty"` 75 | User string `xml:"user" json:"user,omitempty"` 76 | UserURL string `xml:"user_url" json:"user_url,omitempty"` 77 | Action NoteCommentAction `xml:"action" json:"action"` 78 | Text string `xml:"text" json:"text"` 79 | HTML string `xml:"html" json:"html"` 80 | } 81 | 82 | // ObjectID returns the object id of the note. 83 | func (n *Note) ObjectID() ObjectID { 84 | return n.ID.ObjectID() 85 | } 86 | 87 | // NoteCommentAction are actions that a note comment took. 88 | type NoteCommentAction string 89 | 90 | // The set of comment actions. 91 | var ( 92 | NoteCommentOpened NoteCommentAction = "opened" 93 | NoteCommentComment NoteCommentAction = "commented" 94 | NoteCommentClosed NoteCommentAction = "closed" 95 | ) 96 | 97 | // NoteStatus is the status of the note. 98 | type NoteStatus string 99 | 100 | // A note can be open or closed. 101 | var ( 102 | NoteOpen NoteStatus = "open" 103 | NoteClosed NoteStatus = "closed" 104 | ) 105 | -------------------------------------------------------------------------------- /note_test.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "encoding/xml" 7 | "reflect" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestNote_UnmarshalXML(t *testing.T) { 13 | rawXML := []byte(` 14 | 15 | 1302953 16 | note url 17 | comment url 18 | close url 19 | reopen url 20 | 2018-02-17 17:34:48 UTC 21 | closed 22 | 2018-02-17 22:16:03 UTC 23 | 24 | 25 | 2018-02-17 17:34:48 UTC 26 | 251221 27 | spiregrain 28 | user url 29 | opened 30 | comment text 31 | comment html 32 | 33 | 34 | 2018-02-17 22:16:03 UTC 35 | 251221 36 | spiregrain 37 | https://api.openstreetmap.org/user/spiregrain 38 | closed 39 | 40 |

41 |
42 |
43 |
`) 44 | n := &Note{} 45 | 46 | err := xml.Unmarshal(rawXML, &n) 47 | if err != nil { 48 | t.Fatalf("unmarshal error: %v", err) 49 | } 50 | 51 | if v := n.ID; v != 1302953 { 52 | t.Errorf("incorrect value: %v", v) 53 | } 54 | 55 | if v := n.Lat; v != 51.5438971 { 56 | t.Errorf("incorrect value: %v", v) 57 | } 58 | 59 | if v := n.Lon; v != 0.0088488 { 60 | t.Errorf("incorrect value: %v", v) 61 | } 62 | 63 | if v := n.URL; v != "note url" { 64 | t.Errorf("incorrect value: %v", v) 65 | } 66 | 67 | if v := n.CommentURL; v != "comment url" { 68 | t.Errorf("incorrect value: %v", v) 69 | } 70 | 71 | if v := n.CloseURL; v != "close url" { 72 | t.Errorf("incorrect value: %v", v) 73 | } 74 | 75 | if v := n.ReopenURL; v != "reopen url" { 76 | t.Errorf("incorrect value: %v", v) 77 | } 78 | 79 | if v := n.DateCreated; !v.Equal(time.Date(2018, 2, 17, 17, 34, 48, 0, time.UTC)) { 80 | t.Errorf("incorrect value: %v", v) 81 | } 82 | 83 | if v := n.DateClosed; !v.Equal(time.Date(2018, 2, 17, 22, 16, 3, 0, time.UTC)) { 84 | t.Errorf("incorrect value: %v", v) 85 | } 86 | 87 | if v := n.Status; v != NoteClosed { 88 | t.Errorf("incorrect value: %v", v) 89 | } 90 | 91 | // comments 92 | if v := len(n.Comments); v != 2 { 93 | t.Errorf("incorrect value: %v", v) 94 | } 95 | 96 | if v := n.Comments[0].Date; !v.Equal(time.Date(2018, 2, 17, 17, 34, 48, 0, time.UTC)) { 97 | t.Errorf("incorrect value: %v", v) 98 | } 99 | 100 | if v := n.Comments[0].UserID; v != 251221 { 101 | t.Errorf("incorrect value: %v", v) 102 | } 103 | 104 | if v := n.Comments[0].User; v != "spiregrain" { 105 | t.Errorf("incorrect value: %v", v) 106 | } 107 | 108 | if v := n.Comments[0].UserURL; v != "user url" { 109 | t.Errorf("incorrect value: %v", v) 110 | } 111 | 112 | if v := n.Comments[0].Action; v != NoteCommentOpened { 113 | t.Errorf("incorrect value: %v", v) 114 | } 115 | 116 | if v := n.Comments[0].Text; v != "comment text" { 117 | t.Errorf("incorrect value: %v", v) 118 | } 119 | 120 | if v := n.Comments[0].HTML; v != "comment html" { 121 | t.Errorf("incorrect value: %v", v) 122 | } 123 | 124 | // should marshal correctly. 125 | data, err := xml.Marshal(n) 126 | if err != nil { 127 | t.Fatalf("xml marshal error: %v", err) 128 | } 129 | 130 | nn := &Note{} 131 | err = xml.Unmarshal(data, &nn) 132 | if err != nil { 133 | t.Fatalf("xml unmarshal error: %v", err) 134 | } 135 | 136 | if !reflect.DeepEqual(nn, n) { 137 | t.Errorf("incorrect marshal") 138 | t.Log(nn) 139 | t.Log(n) 140 | } 141 | } 142 | 143 | func TestNote_MarshalJSON(t *testing.T) { 144 | n := Note{ 145 | ID: 123, 146 | Lat: 10, 147 | Lon: 20, 148 | DateCreated: Date{time.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC)}, 149 | } 150 | 151 | data, err := json.Marshal(n) 152 | if err != nil { 153 | t.Fatalf("marshal error: %v", err) 154 | } 155 | 156 | if !bytes.Equal(data, []byte(`{"type":"note","id":123,"lat":10,"lon":20,"date_created":"2018-01-01T00:00:00Z","date_closed":null,"comments":null}`)) { 157 | t.Errorf("incorrect json: %v", string(data)) 158 | } 159 | } 160 | 161 | func TestNote_ObjectID(t *testing.T) { 162 | n := Note{ID: 123} 163 | id := n.ObjectID() 164 | 165 | if v := id.Type(); v != TypeNote { 166 | t.Errorf("incorrect type: %v", v) 167 | } 168 | 169 | if v := id.Ref(); v != 123 { 170 | t.Errorf("incorrect ref: %v", 123) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /object.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // ObjectID encodes the type and ref of an osm object, 10 | // e.g. nodes, ways, relations, changesets, notes and users. 11 | type ObjectID int64 12 | 13 | // Type returns the Type of the object. 14 | func (id ObjectID) Type() Type { 15 | switch id & typeMask { 16 | case nodeMask: 17 | return TypeNode 18 | case wayMask: 19 | return TypeWay 20 | case relationMask: 21 | return TypeRelation 22 | case changesetMask: 23 | return TypeChangeset 24 | case noteMask: 25 | return TypeNote 26 | case userMask: 27 | return TypeUser 28 | case boundsMask: 29 | return TypeBounds 30 | } 31 | 32 | panic("unknown type") 33 | } 34 | 35 | // Ref returns the ID reference for the object. Not unique without the type. 36 | func (id ObjectID) Ref() int64 { 37 | return int64((id & refMask) >> versionBits) 38 | } 39 | 40 | // Version returns the version of the object. 41 | // Will return 0 if the object doesn't have versions like users, notes and changesets. 42 | func (id ObjectID) Version() int { 43 | return int(id & (versionMask)) 44 | } 45 | 46 | // String returns "type/ref:version" for the object. 47 | func (id ObjectID) String() string { 48 | if id.Version() == 0 { 49 | return fmt.Sprintf("%s/%d:-", id.Type(), id.Ref()) 50 | } 51 | 52 | return fmt.Sprintf("%s/%d:%d", id.Type(), id.Ref(), id.Version()) 53 | } 54 | 55 | // ParseObjectID takes a string and tries to determine the object id from it. 56 | // The string must be formatted as "type/id:version", the same as the result of the String method. 57 | func ParseObjectID(s string) (ObjectID, error) { 58 | parts := strings.Split(s, "/") 59 | if len(parts) != 2 { 60 | return 0, fmt.Errorf("invalid element id: %v", s) 61 | } 62 | 63 | parts2 := strings.Split(parts[1], ":") 64 | if l := len(parts2); l == 0 || l > 2 { 65 | return 0, fmt.Errorf("invalid element id: %v", s) 66 | } 67 | 68 | var version int 69 | ref, err := strconv.ParseInt(parts2[0], 10, 64) 70 | if err != nil { 71 | return 0, fmt.Errorf("invalid element id: %v: %v", s, err) 72 | } 73 | 74 | if len(parts2) == 2 && parts2[1] != "-" { 75 | v, e := strconv.ParseInt(parts2[1], 10, 64) 76 | if e != nil { 77 | return 0, fmt.Errorf("invalid element id: %v: %v", s, err) 78 | } 79 | version = int(v) 80 | } 81 | 82 | oid, err := Type(parts[0]).objectID(ref, version) 83 | if err != nil { 84 | return 0, fmt.Errorf("invalid element id: %v: %v", s, err) 85 | } 86 | 87 | return oid, nil 88 | } 89 | 90 | // An Object represents a Node, Way, Relation, Changeset, Note or User only. 91 | type Object interface { 92 | ObjectID() ObjectID 93 | 94 | // private is so that **ID types don't implement this interface. 95 | private() 96 | } 97 | 98 | func (n *Node) private() {} 99 | func (w *Way) private() {} 100 | func (r *Relation) private() {} 101 | func (c *Changeset) private() {} 102 | func (n *Note) private() {} 103 | func (u *User) private() {} 104 | func (b *Bounds) private() {} 105 | 106 | // Objects is a set of objects with some helpers 107 | type Objects []Object 108 | 109 | // ObjectIDs returns a slice of the object ids of the osm objects. 110 | func (os Objects) ObjectIDs() ObjectIDs { 111 | if len(os) == 0 { 112 | return nil 113 | } 114 | 115 | ids := make(ObjectIDs, 0, len(os)) 116 | for _, o := range os { 117 | ids = append(ids, o.ObjectID()) 118 | } 119 | 120 | return ids 121 | } 122 | 123 | // ObjectIDs is a slice of ObjectIDs with some helpers on top. 124 | type ObjectIDs []ObjectID 125 | 126 | // A Scanner reads osm data from planet dump files. 127 | // It is based on the bufio.Scanner, common usage. 128 | // Scanners are not safe for parallel use. One should feed the 129 | // objects into their own channel and have workers read from that. 130 | // 131 | // s := scanner.New(r) 132 | // defer s.Close() 133 | // 134 | // for s.Next() { 135 | // o := s.Object() 136 | // // do something 137 | // } 138 | // 139 | // if s.Err() != nil { 140 | // // scanner did not complete fully 141 | // } 142 | type Scanner interface { 143 | Scan() bool 144 | Object() Object 145 | Err() error 146 | Close() error 147 | } 148 | -------------------------------------------------------------------------------- /object_test.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestParseObjectID(t *testing.T) { 9 | cases := []struct { 10 | name string 11 | string string 12 | id ObjectID 13 | }{ 14 | { 15 | name: "node", 16 | id: NodeID(0).ObjectID(1), 17 | }, 18 | { 19 | name: "zero version node", 20 | id: NodeID(3).ObjectID(0), 21 | }, 22 | { 23 | name: "way", 24 | id: WayID(10).ObjectID(2), 25 | }, 26 | { 27 | name: "relation", 28 | id: RelationID(100).ObjectID(3), 29 | }, 30 | { 31 | name: "changeset", 32 | id: ChangesetID(1000).ObjectID(), 33 | }, 34 | { 35 | name: "note", 36 | id: NoteID(10000).ObjectID(), 37 | }, 38 | { 39 | name: "user", 40 | id: UserID(5000).ObjectID(), 41 | }, 42 | { 43 | name: "node feature", 44 | string: "node/100", 45 | id: NodeID(100).ObjectID(0), 46 | }, 47 | { 48 | name: "bounds", 49 | string: "bounds/0", 50 | id: (&Bounds{}).ObjectID(), 51 | }, 52 | } 53 | 54 | for _, tc := range cases { 55 | t.Run(tc.name, func(t *testing.T) { 56 | var ( 57 | id ObjectID 58 | err error 59 | ) 60 | 61 | if tc.string == "" { 62 | id, err = ParseObjectID(tc.id.String()) 63 | if err != nil { 64 | t.Errorf("parse error: %v", err) 65 | return 66 | } 67 | } else { 68 | id, err = ParseObjectID(tc.string) 69 | if err != nil { 70 | t.Errorf("parse error: %v", err) 71 | return 72 | } 73 | } 74 | 75 | if id != tc.id { 76 | t.Errorf("incorrect id: %v != %v", id, tc.id) 77 | } 78 | }) 79 | } 80 | 81 | // errors 82 | if _, err := ParseObjectID("123"); err == nil { 83 | t.Errorf("should return error if only one part") 84 | } 85 | 86 | if _, err := ParseObjectID("node/1:1:1"); err == nil { 87 | t.Errorf("should return error if multiple :") 88 | } 89 | 90 | if _, err := ParseObjectID("node/abc:1"); err == nil { 91 | t.Errorf("should return error if id not a number") 92 | } 93 | 94 | if _, err := ParseObjectID("node/1:abc"); err == nil { 95 | t.Errorf("should return error if version not a number") 96 | } 97 | 98 | if _, err := ParseObjectID("lake/1:1"); err == nil { 99 | t.Errorf("should return error if not a valid type") 100 | } 101 | } 102 | 103 | func TestObjects_ObjectIDs(t *testing.T) { 104 | es := Objects{ 105 | &Node{ID: 1, Version: 5}, 106 | &Way{ID: 2, Version: 6}, 107 | &Relation{ID: 3, Version: 7}, 108 | &Node{ID: 4, Version: 8}, 109 | &User{ID: 5}, 110 | &Note{ID: 6}, 111 | } 112 | 113 | expected := ObjectIDs{ 114 | NodeID(1).ObjectID(5), 115 | WayID(2).ObjectID(6), 116 | RelationID(3).ObjectID(7), 117 | NodeID(4).ObjectID(8), 118 | UserID(5).ObjectID(), 119 | NoteID(6).ObjectID(), 120 | } 121 | 122 | if ids := es.ObjectIDs(); !reflect.DeepEqual(ids, expected) { 123 | t.Errorf("incorrect ids: %v", ids) 124 | } 125 | } 126 | 127 | func TestObjectID_implementations(t *testing.T) { 128 | type oid interface { 129 | ObjectID() ObjectID 130 | } 131 | 132 | var _ oid = ElementID(0) 133 | 134 | var _ oid = &Node{} 135 | var _ oid = &Way{} 136 | var _ oid = &Relation{} 137 | var _ oid = &Changeset{} 138 | var _ oid = &Note{} 139 | var _ oid = &User{} 140 | 141 | var _ oid = ChangesetID(0) 142 | var _ oid = NoteID(0) 143 | var _ oid = UserID(0) 144 | 145 | type oidv interface { 146 | ObjectID(v int) ObjectID 147 | } 148 | 149 | var _ oidv = FeatureID(0) 150 | var _ oidv = NodeID(0) 151 | var _ oidv = WayID(0) 152 | var _ oidv = RelationID(0) 153 | 154 | // These should not implement the ObjectID methods 155 | noImplement := []interface{}{ 156 | WayNode{}, 157 | Member{}, 158 | } 159 | 160 | for _, ni := range noImplement { 161 | if _, ok := ni.(oid); ok { 162 | t.Errorf("%T should not have ObjectID() method", ni) 163 | } 164 | 165 | if _, ok := ni.(oidv); ok { 166 | t.Errorf("%T should not have ObjectID(v int) method", ni) 167 | } 168 | } 169 | } 170 | 171 | func TestObject_implementations(t *testing.T) { 172 | var _ Object = &Node{} 173 | var _ Object = &Way{} 174 | var _ Object = &Relation{} 175 | var _ Object = &Changeset{} 176 | var _ Object = &Note{} 177 | var _ Object = &User{} 178 | 179 | // These should not implement the Object interface 180 | noImplement := []interface{}{ 181 | ObjectID(0), 182 | FeatureID(0), 183 | ElementID(0), 184 | WayNode{}, 185 | Member{}, 186 | 187 | NodeID(0), 188 | WayID(0), 189 | RelationID(0), 190 | ChangesetID(0), 191 | NoteID(0), 192 | UserID(0), 193 | 194 | Nodes{}, 195 | Ways{}, 196 | Relations{}, 197 | Changesets{}, 198 | Notes{}, 199 | Users{}, 200 | } 201 | 202 | for _, ni := range noImplement { 203 | if _, ok := ni.(Object); ok { 204 | t.Errorf("%T should not be an object", ni) 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /osmapi/README.md: -------------------------------------------------------------------------------- 1 | osm/osmapi [![Godoc Reference](https://godoc.org/github.com/paulmach/osm/osmapi?status.svg)](https://godoc.org/github.com/paulmach/osm/osmapi) 2 | ========== 3 | 4 | Package osmapi provides an interface to the [OSM v0.6 API](https://wiki.openstreetmap.org/wiki/API_v0.6). 5 | 6 | Usage: 7 | 8 | ```go 9 | node, err := osmapi.Node(ctx, 1010) 10 | ``` 11 | 12 | This call issues a request to [api.openstreetmap.org/api/0.6/node/1010](https://api.openstreetmap.org/api/0.6/node/1010) 13 | and returns a parsed `osm.Node` object with all the methods attached. 14 | 15 | ## List of functions 16 | 17 | ```go 18 | func Map(context.Context, bounds *osm.Bounds) (*osm.OSM, error) 19 | 20 | func Node(context.Context, osm.NodeID) (*osm.Node, error) 21 | func Nodes(context.Context, []osm.NodeID) (osm.Nodes, error) 22 | func NodeVersion(context.Context, osm.NodeID, v int) (*osm.Node, error) 23 | func NodeHistory(context.Context, osm.NodeID) (osm.Nodes, error) 24 | 25 | func NodeWays(context.Context, osm.NodeID) (osm.Ways, error) 26 | func NodeRelations(context.Context, osm.NodeID) (osm.Relations, error) 27 | 28 | func Way(context.Context, osm.WayID) (*osm.Way, error) 29 | func Ways(context.Context, []osm.WayID) (osm.Ways, error) 30 | func WayFull(context.Context, osm.WayID) (*osm.OSM, error) 31 | func WayVersion(context.Context, osm.WayID, v int) (*osm.Way, error) 32 | func WayHistory(context.Context, osm.WayID) (osm.Ways, error) 33 | 34 | func WayRelations(context.Context, osm.WayID) (osm.Relations, error) 35 | 36 | func Relation(context.Context, osm.RelationID) (*osm.Relation, error) 37 | func Relations(context.Context, []osm.RelationID) (osm.Relations, error) 38 | func RelationFull(context.Context, osm.RelationID) (*osm.OSM, error) 39 | func RelationVersion(context.Context, osm.RelationID, v int) (*osm.Relation, error) 40 | func RelationHistory(context.Context, osm.RelationID) (osm.Relations, error) 41 | 42 | func RelationRelations(context.Context, osm.RelationID) (osm.Relations, error) 43 | 44 | func Changeset(context.Context, osm.ChangesetID) (*osm.Changeset, error) 45 | func ChangesetWithDiscussion(context.Context, osm.ChangesetID) (*osm.Changeset, error) 46 | func ChangesetDownload(context.Context, osm.ChangesetID) (*osm.Change, error) 47 | 48 | func Note(ctx context.Context, id osm.NoteID) (*osm.Note, error) { 49 | func Notes(ctx context.Context, bounds *osm.Bounds, opts ...NotesOption) (osm.Notes, error) 50 | func NotesSearch(ctx context.Context, query string, opts ...NotesOption) (osm.Notes, error) 51 | 52 | func User(ctx context.Context, id osm.UserID) (*osm.User, error) 53 | ``` 54 | 55 | See the [godoc reference](https://godoc.org/github.com/paulmach/osm/osmapi) 56 | for more details. 57 | 58 | ## Rate limiting 59 | 60 | This package can make sure of [`x/time/rate.Limiter`](https://godoc.org/golang.org/x/time/rate#Limiter) 61 | to throttle requests to the official api. Example usage: 62 | 63 | ```go 64 | // 10 qps 65 | osmapi.DefaultDatasource.Limiter = rate.NewLimiter(10, 1) 66 | ``` 67 | -------------------------------------------------------------------------------- /osmapi/changeset.go: -------------------------------------------------------------------------------- 1 | package osmapi 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/paulmach/osm" 8 | ) 9 | 10 | // Changeset returns a given changeset from the osm rest api. 11 | // Delegates to the DefaultDatasource and uses its http.Client to make the request. 12 | func Changeset(ctx context.Context, id osm.ChangesetID) (*osm.Changeset, error) { 13 | return DefaultDatasource.Changeset(ctx, id) 14 | } 15 | 16 | // Changeset returns a given changeset from the osm rest api. 17 | func (ds *Datasource) Changeset(ctx context.Context, id osm.ChangesetID) (*osm.Changeset, error) { 18 | url := fmt.Sprintf("%s/changeset/%d", ds.baseURL(), id) 19 | return ds.getChangeset(ctx, url) 20 | } 21 | 22 | // ChangesetWithDiscussion returns a changeset and its discussion from the osm rest api. 23 | // Delegates to the DefaultDatasource and uses its http.Client to make the request. 24 | func ChangesetWithDiscussion(ctx context.Context, id osm.ChangesetID) (*osm.Changeset, error) { 25 | return DefaultDatasource.ChangesetWithDiscussion(ctx, id) 26 | } 27 | 28 | // ChangesetWithDiscussion returns a changeset and its discussion from the osm rest api. 29 | func (ds *Datasource) ChangesetWithDiscussion(ctx context.Context, id osm.ChangesetID) (*osm.Changeset, error) { 30 | url := fmt.Sprintf("%s/changeset/%d?include_discussion=true", ds.baseURL(), id) 31 | return ds.getChangeset(ctx, url) 32 | } 33 | 34 | func (ds *Datasource) getChangeset(ctx context.Context, url string) (*osm.Changeset, error) { 35 | css := &osm.OSM{} 36 | if err := ds.getFromAPI(ctx, url, &css); err != nil { 37 | return nil, err 38 | } 39 | 40 | if l := len(css.Changesets); l != 1 { 41 | return nil, fmt.Errorf("wrong number of changesets, expected 1, got %v", l) 42 | } 43 | 44 | return css.Changesets[0], nil 45 | } 46 | 47 | // ChangesetDownload returns the full osmchange for the changeset using the osm rest api. 48 | // Delegates to the DefaultDatasource and uses its http.Client to make the request. 49 | func ChangesetDownload(ctx context.Context, id osm.ChangesetID) (*osm.Change, error) { 50 | return DefaultDatasource.ChangesetDownload(ctx, id) 51 | } 52 | 53 | // ChangesetDownload returns the full osmchange for the changeset using the osm rest api. 54 | func (ds *Datasource) ChangesetDownload(ctx context.Context, id osm.ChangesetID) (*osm.Change, error) { 55 | url := fmt.Sprintf("%s/changeset/%d/download", ds.baseURL(), id) 56 | 57 | change := &osm.Change{} 58 | if err := ds.getFromAPI(ctx, url, &change); err != nil { 59 | return nil, err 60 | } 61 | 62 | return change, nil 63 | } 64 | -------------------------------------------------------------------------------- /osmapi/changeset_test.go: -------------------------------------------------------------------------------- 1 | package osmapi 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestChangeset_urls(t *testing.T) { 12 | ctx := context.Background() 13 | 14 | url := "" 15 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 16 | url = r.URL.String() 17 | w.Write([]byte(``)) 18 | })) 19 | defer ts.Close() 20 | 21 | DefaultDatasource.BaseURL = ts.URL 22 | defer func() { 23 | DefaultDatasource.BaseURL = BaseURL 24 | }() 25 | 26 | t.Run("changeset", func(t *testing.T) { 27 | Changeset(ctx, 1) 28 | if !strings.Contains(url, "changeset/1") { 29 | t.Errorf("incorrect path: %v", url) 30 | } 31 | }) 32 | 33 | t.Run("changeset with discussion", func(t *testing.T) { 34 | ChangesetWithDiscussion(ctx, 1) 35 | if !strings.Contains(url, "changeset/1?include_discussion=true") { 36 | t.Errorf("incorrect path: %v", url) 37 | } 38 | }) 39 | 40 | t.Run("changeset download", func(t *testing.T) { 41 | ChangesetDownload(ctx, 1) 42 | if !strings.Contains(url, "changeset/1/download") { 43 | t.Errorf("incorrect path: %v", url) 44 | } 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /osmapi/datasource.go: -------------------------------------------------------------------------------- 1 | // Package osmapi provides an interface to the OSM v0.6 API. 2 | package osmapi 3 | 4 | import ( 5 | "context" 6 | "encoding/xml" 7 | "fmt" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/paulmach/osm" 12 | ) 13 | 14 | // BaseURL defines the api host. This can be change to hit 15 | // a dev server, for example, http://api06.dev.openstreetmap.org/api/0.6 16 | const BaseURL = "http://api.openstreetmap.org/api/0.6" 17 | 18 | // A RateLimiter is something that can wait until its next allowed request. 19 | // This interface is met by `golang.org/x/time/rate.Limiter` and is meant 20 | // to be used with it. For example: 21 | // // 10 qps 22 | // osmapi.DefaultDatasource.Limiter = rate.NewLimiter(10, 1) 23 | type RateLimiter interface { 24 | Wait(context.Context) error 25 | } 26 | 27 | // Datasource defines context about the http client to use to make requests. 28 | type Datasource struct { 29 | // If Limiter is non-nil. The datasource will wait/block until the request 30 | // is allowed by the rate limiter. To be a good citizen, it is recommended 31 | // to use this when making may concurrent requests against the prod osm api. 32 | // See the RateLimiter docs for more information. 33 | Limiter RateLimiter 34 | 35 | BaseURL string 36 | Client *http.Client 37 | } 38 | 39 | // DefaultDatasource is the Datasource used by package level convenience functions. 40 | var DefaultDatasource = &Datasource{ 41 | BaseURL: BaseURL, 42 | Client: &http.Client{ 43 | Timeout: 6 * time.Minute, // looks like the api server has a 5 min timeout. 44 | }, 45 | } 46 | 47 | var _ osm.HistoryDatasourcer = &Datasource{} 48 | 49 | // NewDatasource creates a Datasource using the given client. 50 | func NewDatasource(client *http.Client) *Datasource { 51 | return &Datasource{ 52 | Client: client, 53 | } 54 | } 55 | 56 | func (ds *Datasource) getFromAPI(ctx context.Context, url string, item interface{}) error { 57 | client := ds.Client 58 | if client == nil { 59 | client = DefaultDatasource.Client 60 | } 61 | 62 | if client == nil { 63 | client = http.DefaultClient 64 | } 65 | 66 | if ds.Limiter != nil { 67 | err := ds.Limiter.Wait(ctx) 68 | if err != nil { 69 | return err 70 | } 71 | } 72 | 73 | req, err := http.NewRequest("GET", url, nil) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | resp, err := client.Do(req.WithContext(ctx)) 79 | if err != nil { 80 | return err 81 | } 82 | defer resp.Body.Close() 83 | 84 | if resp.StatusCode == http.StatusNotFound { 85 | return &NotFoundError{URL: url} 86 | } 87 | 88 | if resp.StatusCode == http.StatusForbidden { 89 | return &ForbiddenError{URL: url} 90 | } 91 | 92 | if resp.StatusCode == http.StatusGone { 93 | return &GoneError{URL: url} 94 | } 95 | 96 | if resp.StatusCode == http.StatusRequestURITooLong { 97 | return &RequestURITooLongError{URL: url} 98 | } 99 | 100 | if resp.StatusCode != http.StatusOK { 101 | return &UnexpectedStatusCodeError{ 102 | Code: resp.StatusCode, 103 | URL: url, 104 | } 105 | } 106 | 107 | return xml.NewDecoder(resp.Body).Decode(item) 108 | } 109 | 110 | func (ds *Datasource) baseURL() string { 111 | if ds.BaseURL != "" { 112 | return ds.BaseURL 113 | } 114 | 115 | return BaseURL 116 | } 117 | 118 | // NotFound error will return true if the result is not found. 119 | func (ds *Datasource) NotFound(err error) bool { 120 | if err == nil { 121 | return false 122 | } 123 | 124 | _, ok := err.(*NotFoundError) 125 | return ok 126 | } 127 | 128 | // NotFoundError means 404 from the api. 129 | type NotFoundError struct { 130 | URL string 131 | } 132 | 133 | // Error returns an error message with the url causing the problem. 134 | func (e *NotFoundError) Error() string { 135 | return fmt.Sprintf("osmapi: not found at %s", e.URL) 136 | } 137 | 138 | // ForbiddenError means 403 from the api. 139 | // Returned whenever the version of the element is not available (due to redaction). 140 | type ForbiddenError struct { 141 | URL string 142 | } 143 | 144 | // Error returns an error message with the url causing the problem. 145 | func (e *ForbiddenError) Error() string { 146 | return fmt.Sprintf("osmapi: forbidden at %s", e.URL) 147 | } 148 | 149 | // GoneError is returned for deleted elements that get 410 from the api. 150 | type GoneError struct { 151 | URL string 152 | } 153 | 154 | // Error returns an error message with the url causing the problem. 155 | func (e *GoneError) Error() string { 156 | return fmt.Sprintf("osmapi: gone at %s", e.URL) 157 | } 158 | 159 | // RequestURITooLongError is returned when requesting too many ids in 160 | // a multi id request, ie. Nodes, Ways, Relations functions. 161 | type RequestURITooLongError struct { 162 | URL string 163 | } 164 | 165 | // Error returns an error message with the url causing the problem. 166 | func (e *RequestURITooLongError) Error() string { 167 | return fmt.Sprintf("osmapi: uri too long at %s", e.URL) 168 | } 169 | 170 | // UnexpectedStatusCodeError is return for a non 200 or 404 status code. 171 | type UnexpectedStatusCodeError struct { 172 | Code int 173 | URL string 174 | } 175 | 176 | // Error returns an error message with some information. 177 | func (e *UnexpectedStatusCodeError) Error() string { 178 | return fmt.Sprintf("osmapi: unexpected status code of %d for url %s", e.Code, e.URL) 179 | } 180 | -------------------------------------------------------------------------------- /osmapi/datasource_test.go: -------------------------------------------------------------------------------- 1 | package osmapi 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | func TestDatasourceNotFound(t *testing.T) { 9 | ds := NewDatasource(nil) 10 | 11 | if ds.NotFound(nil) { 12 | t.Errorf("should be false for nil") 13 | } 14 | 15 | if ds.NotFound(errors.New("foo")) { 16 | t.Errorf("should be false for random error") 17 | } 18 | 19 | if ds.NotFound(&GoneError{}) { 20 | t.Errorf("should be false for gone error") 21 | } 22 | 23 | if !ds.NotFound(&NotFoundError{}) { 24 | t.Errorf("should be true for not found error") 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /osmapi/map.go: -------------------------------------------------------------------------------- 1 | package osmapi 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/paulmach/osm" 8 | ) 9 | 10 | // Map returns the latest elements in the given bounding box. 11 | // Delegates to the DefaultDatasource and uses its http.Client to make the request. 12 | func Map(ctx context.Context, bounds *osm.Bounds, opts ...FeatureOption) (*osm.OSM, error) { 13 | return DefaultDatasource.Map(ctx, bounds, opts...) 14 | } 15 | 16 | // Map returns the latest elements in the given bounding box. 17 | func (ds *Datasource) Map(ctx context.Context, bounds *osm.Bounds, opts ...FeatureOption) (*osm.OSM, error) { 18 | params, err := featureOptions(opts) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | url := fmt.Sprintf("%s/map?bbox=%f,%f,%f,%f&%s", ds.baseURL(), 24 | bounds.MinLon, bounds.MinLat, 25 | bounds.MaxLon, bounds.MaxLat, 26 | params) 27 | o := &osm.OSM{} 28 | if err := ds.getFromAPI(ctx, url, &o); err != nil { 29 | return nil, err 30 | } 31 | 32 | return o, nil 33 | } 34 | -------------------------------------------------------------------------------- /osmapi/map_test.go: -------------------------------------------------------------------------------- 1 | package osmapi 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | "github.com/paulmach/osm" 12 | ) 13 | 14 | func TestMap_urls(t *testing.T) { 15 | ctx := context.Background() 16 | 17 | url := "" 18 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 19 | url = r.URL.String() 20 | w.Write([]byte(``)) 21 | })) 22 | defer ts.Close() 23 | 24 | DefaultDatasource.BaseURL = ts.URL 25 | defer func() { 26 | DefaultDatasource.BaseURL = BaseURL 27 | }() 28 | 29 | t.Run("map", func(t *testing.T) { 30 | bound := &osm.Bounds{ 31 | MinLon: 1, MinLat: 2, 32 | MaxLon: 3, MaxLat: 4, 33 | } 34 | 35 | Map(ctx, bound) 36 | if !strings.Contains(url, "map?bbox=1.000000,2.000000,3.000000,4.000000") { 37 | t.Errorf("incorrect path: %v", url) 38 | } 39 | 40 | Map(ctx, bound, At(time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC))) 41 | if !strings.Contains(url, "map?bbox=1.000000,2.000000,3.000000,4.000000&at=2016-01-01T00:00:00Z") { 42 | t.Errorf("incorrect path: %v", url) 43 | } 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /osmapi/node_test.go: -------------------------------------------------------------------------------- 1 | package osmapi 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | "github.com/paulmach/osm" 12 | ) 13 | 14 | func TestNode_urls(t *testing.T) { 15 | ctx := context.Background() 16 | 17 | url := "" 18 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 19 | url = r.URL.String() 20 | w.Write([]byte(``)) 21 | })) 22 | defer ts.Close() 23 | 24 | DefaultDatasource.BaseURL = ts.URL 25 | defer func() { 26 | DefaultDatasource.BaseURL = BaseURL 27 | }() 28 | 29 | t.Run("node", func(t *testing.T) { 30 | Node(ctx, 1) 31 | if !strings.Contains(url, "node/1") { 32 | t.Errorf("incorrect path: %v", url) 33 | } 34 | 35 | Node(ctx, 1, At(time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC))) 36 | if !strings.Contains(url, "node/1?at=2016-01-01T00:00:00Z") { 37 | t.Errorf("incorrect path: %v", url) 38 | } 39 | }) 40 | 41 | t.Run("nodes", func(t *testing.T) { 42 | Nodes(ctx, []osm.NodeID{1, 2}) 43 | if !strings.Contains(url, "nodes?nodes=1,2") { 44 | t.Errorf("incorrect path: %v", url) 45 | } 46 | 47 | Nodes(ctx, []osm.NodeID{1, 2}, At(time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC))) 48 | if !strings.Contains(url, "nodes?nodes=1,2&at=2016-01-01T00:00:00Z") { 49 | t.Errorf("incorrect path: %v", url) 50 | } 51 | }) 52 | 53 | t.Run("node version", func(t *testing.T) { 54 | NodeVersion(ctx, 1, 2) 55 | if !strings.Contains(url, "node/1/2") { 56 | t.Errorf("incorrect path: %v", url) 57 | } 58 | }) 59 | 60 | t.Run("node history", func(t *testing.T) { 61 | NodeHistory(ctx, 1) 62 | if !strings.Contains(url, "node/1/history") { 63 | t.Errorf("incorrect path: %v", url) 64 | } 65 | }) 66 | 67 | t.Run("node ways", func(t *testing.T) { 68 | NodeWays(ctx, 1) 69 | if !strings.Contains(url, "node/1/ways") { 70 | t.Errorf("incorrect path: %v", url) 71 | } 72 | 73 | NodeWays(ctx, 1, At(time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC))) 74 | if !strings.Contains(url, "node/1/ways?at=2016-01-01T00:00:00Z") { 75 | t.Errorf("incorrect path: %v", url) 76 | } 77 | }) 78 | 79 | t.Run("node relations", func(t *testing.T) { 80 | NodeRelations(ctx, 1) 81 | if !strings.Contains(url, "node/1/relations") { 82 | t.Errorf("incorrect path: %v", url) 83 | } 84 | 85 | NodeRelations(ctx, 1, At(time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC))) 86 | if !strings.Contains(url, "node/1/relations?at=2016-01-01T00:00:00Z") { 87 | t.Errorf("incorrect path: %v", url) 88 | } 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /osmapi/note.go: -------------------------------------------------------------------------------- 1 | package osmapi 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "strings" 8 | 9 | "github.com/paulmach/osm" 10 | ) 11 | 12 | // Note returns the note from the osm rest api. 13 | // Delegates to the DefaultDatasource and uses its http.Client to make the request. 14 | func Note(ctx context.Context, id osm.NoteID) (*osm.Note, error) { 15 | return DefaultDatasource.Note(ctx, id) 16 | } 17 | 18 | // Note returns the note from the osm rest api. 19 | func (ds *Datasource) Note(ctx context.Context, id osm.NoteID) (*osm.Note, error) { 20 | url := fmt.Sprintf("%s/notes/%d", ds.baseURL(), id) 21 | 22 | o := &osm.OSM{} 23 | if err := ds.getFromAPI(ctx, url, &o); err != nil { 24 | return nil, err 25 | } 26 | 27 | if l := len(o.Notes); l != 1 { 28 | return nil, fmt.Errorf("wrong number of notes, expected 1, got %v", l) 29 | } 30 | 31 | return o.Notes[0], nil 32 | } 33 | 34 | // Notes returns the notes in a bounding box. Can provide options to limit the results 35 | // or change what it means to be "closed". See the options or osm api v0.6 docs for details. 36 | // Delegates to the DefaultDatasource and uses its http.Client to make the request. 37 | func Notes(ctx context.Context, bounds *osm.Bounds, opts ...NotesOption) (osm.Notes, error) { 38 | return DefaultDatasource.Notes(ctx, bounds, opts...) 39 | } 40 | 41 | var _ NotesOption = Limit(1) 42 | var _ NotesOption = MaxDaysClosed(1) 43 | 44 | // Notes returns the notes in a bounding box. Can provide options to limit the results 45 | // or change what it means to be "closed". See the options or osm api v0.6 docs for details. 46 | func (ds *Datasource) Notes(ctx context.Context, bounds *osm.Bounds, opts ...NotesOption) (osm.Notes, error) { 47 | params := make([]string, 0, 1+len(opts)) 48 | params = append(params, fmt.Sprintf("bbox=%f,%f,%f,%f", 49 | bounds.MinLon, bounds.MinLat, 50 | bounds.MaxLon, bounds.MaxLat)) 51 | 52 | var err error 53 | for _, o := range opts { 54 | params, err = o.applyNotes(params) 55 | if err != nil { 56 | return nil, err 57 | } 58 | } 59 | 60 | url := fmt.Sprintf("%s/notes?%s", ds.baseURL(), strings.Join(params, "&")) 61 | 62 | o := &osm.OSM{} 63 | if err := ds.getFromAPI(ctx, url, &o); err != nil { 64 | return nil, err 65 | } 66 | 67 | return o.Notes, nil 68 | } 69 | 70 | // NotesSearch returns the notes in a bounding box whose text matches the query. 71 | // Can provide options to limit the results or change what it means to be "closed". 72 | // See the options or osm api v0.6 docs for details. 73 | // Delegates to the DefaultDatasource and uses its http.Client to make the request. 74 | func NotesSearch(ctx context.Context, query string, opts ...NotesOption) (osm.Notes, error) { 75 | return DefaultDatasource.NotesSearch(ctx, query, opts...) 76 | } 77 | 78 | // NotesSearch returns the notes whose text matches the query. 79 | // Can provide options to limit the results or change what it means to be "closed". 80 | // See the options or osm api v0.6 docs for details. 81 | func (ds *Datasource) NotesSearch(ctx context.Context, query string, opts ...NotesOption) (osm.Notes, error) { 82 | params := make([]string, 0, 1+len(opts)) 83 | params = append(params, fmt.Sprintf("q=%s", url.QueryEscape(query))) 84 | 85 | var err error 86 | for _, o := range opts { 87 | params, err = o.applyNotes(params) 88 | if err != nil { 89 | return nil, err 90 | } 91 | } 92 | 93 | url := fmt.Sprintf("%s/notes/search?%s", ds.baseURL(), strings.Join(params, "&")) 94 | 95 | o := &osm.OSM{} 96 | if err := ds.getFromAPI(ctx, url, &o); err != nil { 97 | return nil, err 98 | } 99 | 100 | return o.Notes, nil 101 | } 102 | -------------------------------------------------------------------------------- /osmapi/note_test.go: -------------------------------------------------------------------------------- 1 | package osmapi 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/paulmach/osm" 11 | ) 12 | 13 | func TestNote_urls(t *testing.T) { 14 | ctx := context.Background() 15 | 16 | url := "" 17 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 18 | url = r.URL.String() 19 | w.Write([]byte(``)) 20 | })) 21 | defer ts.Close() 22 | 23 | DefaultDatasource.BaseURL = ts.URL 24 | defer func() { 25 | DefaultDatasource.BaseURL = BaseURL 26 | }() 27 | 28 | t.Run("note", func(t *testing.T) { 29 | Note(ctx, 1) 30 | if !strings.Contains(url, "notes/1") { 31 | t.Errorf("incorrect path: %v", url) 32 | } 33 | }) 34 | 35 | t.Run("notes", func(t *testing.T) { 36 | bound := &osm.Bounds{ 37 | MinLon: 1, MinLat: 2, 38 | MaxLon: 3, MaxLat: 4, 39 | } 40 | 41 | Notes(ctx, bound) 42 | if !strings.Contains(url, "notes?bbox=1.000000,2.000000,3.000000,4.000000") { 43 | t.Errorf("incorrect path: %v", url) 44 | } 45 | 46 | Notes(ctx, bound, Limit(1), MaxDaysClosed(4)) 47 | if !strings.Contains(url, "notes?bbox=1.000000,2.000000,3.000000,4.000000&limit=1&closed=4") { 48 | t.Errorf("incorrect path: %v", url) 49 | } 50 | }) 51 | 52 | t.Run("nodes search", func(t *testing.T) { 53 | NotesSearch(ctx, "asdf") 54 | if !strings.Contains(url, "notes/search?q=asdf") { 55 | t.Errorf("incorrect path: %v", url) 56 | } 57 | 58 | NotesSearch(ctx, "asdf", Limit(1), MaxDaysClosed(4)) 59 | if !strings.Contains(url, "notes/search?q=asdf&limit=1&closed=4") { 60 | t.Errorf("incorrect path: %v", url) 61 | } 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /osmapi/options.go: -------------------------------------------------------------------------------- 1 | package osmapi 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | // FeatureOption can be used when fetching a feature or a set of different features. 11 | type FeatureOption interface { 12 | applyFeature([]string) ([]string, error) 13 | } 14 | 15 | // At adds an `at=2006-01-02T15:04:05Z` parameter to the request. 16 | // The osm.fyi supports requesting features and maps as they were at the given time. 17 | func At(t time.Time) FeatureOption { 18 | return &at{t} 19 | } 20 | 21 | type at struct{ t time.Time } 22 | 23 | func (o *at) applyFeature(p []string) ([]string, error) { 24 | return append(p, "at="+o.t.UTC().Format("2006-01-02T15:04:05Z")), nil 25 | } 26 | 27 | // NotesOption defines a valid option for the osmapi.Notes by bounding box api. 28 | type NotesOption interface { 29 | applyNotes([]string) ([]string, error) 30 | } 31 | 32 | // Limit indicates the number of results to return valid values [1,10000]. 33 | // Default is 100. 34 | func Limit(num int) NotesOption { 35 | return &limit{num} 36 | } 37 | 38 | // MaxDaysClosed specifies the number of days a note needs to be closed to 39 | // no longer be returned. 0 will return only open notes, -1 will return all notes. 40 | // Default is 7. 41 | func MaxDaysClosed(num int) NotesOption { 42 | return &maxDaysClosed{num} 43 | } 44 | 45 | type limit struct{ n int } 46 | 47 | func (o *limit) applyNotes(p []string) ([]string, error) { 48 | if o.n < 1 || 10000 < o.n { 49 | return nil, errors.New("osmapi: limit must be between 1 and 10000") 50 | } 51 | return append(p, fmt.Sprintf("limit=%d", o.n)), nil 52 | } 53 | 54 | type maxDaysClosed struct{ n int } 55 | 56 | func (o *maxDaysClosed) applyNotes(p []string) ([]string, error) { 57 | return append(p, fmt.Sprintf("closed=%d", o.n)), nil 58 | } 59 | 60 | func featureOptions(opts []FeatureOption) (string, error) { 61 | if len(opts) == 0 { 62 | return "", nil 63 | } 64 | 65 | params := make([]string, 0, len(opts)) 66 | 67 | var err error 68 | for _, o := range opts { 69 | params, err = o.applyFeature(params) 70 | if err != nil { 71 | return "", err 72 | } 73 | } 74 | 75 | return strings.Join(params, "&"), nil 76 | } 77 | -------------------------------------------------------------------------------- /osmapi/relation_test.go: -------------------------------------------------------------------------------- 1 | package osmapi 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | "github.com/paulmach/osm" 12 | ) 13 | 14 | func TestRelation_urls(t *testing.T) { 15 | ctx := context.Background() 16 | 17 | url := "" 18 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 19 | url = r.URL.String() 20 | w.Write([]byte(``)) 21 | })) 22 | defer ts.Close() 23 | 24 | DefaultDatasource.BaseURL = ts.URL 25 | defer func() { 26 | DefaultDatasource.BaseURL = BaseURL 27 | }() 28 | 29 | t.Run("relation", func(t *testing.T) { 30 | Relation(ctx, 1) 31 | if !strings.Contains(url, "relation/1") { 32 | t.Errorf("incorrect path: %v", url) 33 | } 34 | 35 | Relation(ctx, 1, At(time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC))) 36 | if !strings.Contains(url, "relation/1?at=2016-01-01T00:00:00Z") { 37 | t.Errorf("incorrect path: %v", url) 38 | } 39 | }) 40 | 41 | t.Run("relations", func(t *testing.T) { 42 | Relations(ctx, []osm.RelationID{1, 2}) 43 | if !strings.Contains(url, "relations?relations=1,2") { 44 | t.Errorf("incorrect path: %v", url) 45 | } 46 | 47 | Relations(ctx, []osm.RelationID{1, 2}, At(time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC))) 48 | if !strings.Contains(url, "relations?relations=1,2&at=2016-01-01T00:00:00Z") { 49 | t.Errorf("incorrect path: %v", url) 50 | } 51 | }) 52 | 53 | t.Run("relation version", func(t *testing.T) { 54 | RelationVersion(ctx, 1, 2) 55 | if !strings.Contains(url, "relation/1/2") { 56 | t.Errorf("incorrect path: %v", url) 57 | } 58 | }) 59 | 60 | t.Run("relation history", func(t *testing.T) { 61 | RelationHistory(ctx, 1) 62 | if !strings.Contains(url, "relation/1/history") { 63 | t.Errorf("incorrect path: %v", url) 64 | } 65 | }) 66 | 67 | t.Run("relation relations", func(t *testing.T) { 68 | RelationRelations(ctx, 1) 69 | if !strings.Contains(url, "relation/1/relations") { 70 | t.Errorf("incorrect path: %v", url) 71 | } 72 | 73 | RelationRelations(ctx, 1, At(time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC))) 74 | if !strings.Contains(url, "relation/1/relations?at=2016-01-01T00:00:00Z") { 75 | t.Errorf("incorrect path: %v", url) 76 | } 77 | }) 78 | 79 | t.Run("relation full", func(t *testing.T) { 80 | RelationFull(ctx, 1) 81 | if !strings.Contains(url, "relation/1/full") { 82 | t.Errorf("incorrect path: %v", url) 83 | } 84 | 85 | RelationFull(ctx, 1, At(time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC))) 86 | if !strings.Contains(url, "relation/1/full?at=2016-01-01T00:00:00Z") { 87 | t.Errorf("incorrect path: %v", url) 88 | } 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /osmapi/user.go: -------------------------------------------------------------------------------- 1 | package osmapi 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/paulmach/osm" 8 | ) 9 | 10 | // User returns the user from the osm rest api. 11 | // Delegates to the DefaultDatasource and uses its http.Client to make the request. 12 | func User(ctx context.Context, id osm.UserID) (*osm.User, error) { 13 | return DefaultDatasource.User(ctx, id) 14 | } 15 | 16 | // User returns the user from the osm rest api. 17 | func (ds *Datasource) User(ctx context.Context, id osm.UserID) (*osm.User, error) { 18 | url := fmt.Sprintf("%s/user/%d", ds.baseURL(), id) 19 | 20 | o := &osm.OSM{} 21 | if err := ds.getFromAPI(ctx, url, &o); err != nil { 22 | return nil, err 23 | } 24 | 25 | if l := len(o.Users); l != 1 { 26 | return nil, fmt.Errorf("wrong number of users, expected 1, got %v", l) 27 | } 28 | 29 | return o.Users[0], nil 30 | } 31 | -------------------------------------------------------------------------------- /osmapi/user_test.go: -------------------------------------------------------------------------------- 1 | package osmapi 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestUser_urls(t *testing.T) { 12 | ctx := context.Background() 13 | 14 | url := "" 15 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 16 | url = r.URL.String() 17 | w.Write([]byte(``)) 18 | })) 19 | defer ts.Close() 20 | 21 | DefaultDatasource.BaseURL = ts.URL 22 | defer func() { 23 | DefaultDatasource.BaseURL = BaseURL 24 | }() 25 | 26 | t.Run("user", func(t *testing.T) { 27 | User(ctx, 1) 28 | if !strings.Contains(url, "user/1") { 29 | t.Errorf("incorrect path: %v", url) 30 | } 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /osmapi/way_test.go: -------------------------------------------------------------------------------- 1 | package osmapi 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | "github.com/paulmach/osm" 12 | ) 13 | 14 | func TestWay_urls(t *testing.T) { 15 | ctx := context.Background() 16 | 17 | url := "" 18 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 19 | url = r.URL.String() 20 | w.Write([]byte(``)) 21 | })) 22 | defer ts.Close() 23 | 24 | DefaultDatasource.BaseURL = ts.URL 25 | defer func() { 26 | DefaultDatasource.BaseURL = BaseURL 27 | }() 28 | 29 | t.Run("way", func(t *testing.T) { 30 | Way(ctx, 1) 31 | if !strings.Contains(url, "way/1") { 32 | t.Errorf("incorrect path: %v", url) 33 | } 34 | 35 | Way(ctx, 1, At(time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC))) 36 | if !strings.Contains(url, "way/1?at=2016-01-01T00:00:00Z") { 37 | t.Errorf("incorrect path: %v", url) 38 | } 39 | }) 40 | 41 | t.Run("ways", func(t *testing.T) { 42 | Ways(ctx, []osm.WayID{1, 2}) 43 | if !strings.Contains(url, "ways?ways=1,2") { 44 | t.Errorf("incorrect path: %v", url) 45 | } 46 | 47 | Ways(ctx, []osm.WayID{1, 2}, At(time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC))) 48 | if !strings.Contains(url, "ways?ways=1,2&at=2016-01-01T00:00:00Z") { 49 | t.Errorf("incorrect path: %v", url) 50 | } 51 | }) 52 | 53 | t.Run("way version", func(t *testing.T) { 54 | WayVersion(ctx, 1, 2) 55 | if !strings.Contains(url, "way/1/2") { 56 | t.Errorf("incorrect path: %v", url) 57 | } 58 | }) 59 | 60 | t.Run("way history", func(t *testing.T) { 61 | WayHistory(ctx, 1) 62 | if !strings.Contains(url, "way/1/history") { 63 | t.Errorf("incorrect path: %v", url) 64 | } 65 | }) 66 | 67 | t.Run("way relations", func(t *testing.T) { 68 | WayRelations(ctx, 1) 69 | if !strings.Contains(url, "way/1/relations") { 70 | t.Errorf("incorrect path: %v", url) 71 | } 72 | 73 | WayRelations(ctx, 1, At(time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC))) 74 | if !strings.Contains(url, "way/1/relations?at=2016-01-01T00:00:00Z") { 75 | t.Errorf("incorrect path: %v", url) 76 | } 77 | }) 78 | 79 | t.Run("way full", func(t *testing.T) { 80 | WayFull(ctx, 1) 81 | if !strings.Contains(url, "way/1/full") { 82 | t.Errorf("incorrect path: %v", url) 83 | } 84 | 85 | WayFull(ctx, 1, At(time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC))) 86 | if !strings.Contains(url, "way/1/full?at=2016-01-01T00:00:00Z") { 87 | t.Errorf("incorrect path: %v", url) 88 | } 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /osmgeojson/README.md: -------------------------------------------------------------------------------- 1 | osm/osmgeojson [![Godoc Reference](https://godoc.org/github.com/paulmach/osm/osmgeojson?status.svg)](https://godoc.org/github.com/paulmach/osm/osmgeojson) 2 | ============== 3 | 4 | Package `osmgeojson` converts OSM data to GeoJSON. It is a **full** port of the 5 | nodejs library [osmtogeojson](https://github.com/tyrasd/osmtogeojson) and sports 6 | the same features and tests (plus more): 7 | 8 | * real OSM polygon detection 9 | * OSM multipolygon support, e.g. buildings with holes become proper multipolygons 10 | * supports annotated geometries 11 | * well tested 12 | 13 | ### Usage 14 | 15 | ```go 16 | delta := 0.0001 17 | 18 | lon, lat := -83.5997038, 41.5923682 19 | bounds := &osm.Bounds{ 20 | MinLat: lat - delta, MaxLat: lat + delta, 21 | MinLon: lon - delta, MaxLon: lon + delta, 22 | } 23 | 24 | o, _ := osmapi.Map(ctx, bounds) // fetch data from the osm api. 25 | 26 | // run the conversion 27 | fc, err := osmgeojson.Convert(o, opts) 28 | 29 | // marshal the json 30 | gj, _ := json.MarshalIndent(fc, "", " ") 31 | fmt.Println(string(gj)) 32 | ``` 33 | 34 | ### Options 35 | 36 | The package provides several options to control what is included in the feature properties. 37 | If possible, excluding some of the extra properties can greatly improve the performance. 38 | All of the options **default to false**, i.e. everything will be included. 39 | 40 | * `NoID(yes bool)` 41 | 42 | Controls whether to set the feature.ID to "type/id" e.g. "node/475373687". For some use cases 43 | this may be of limited use since the feature.Properies "type" and "id" are also set. 44 | 45 | * `NoMeta(yes bool)` 46 | 47 | Controls whether to populate the "meta" property which is a sub-map with the 48 | following values from the osm element: "timestamp", "version", "changeset", "user", "uid". 49 | 50 | * `NoRelationMembership(yes bool)` 51 | 52 | Controls whether to include a list of the relations the osm element is a member of. 53 | This info is set as the "relation" property which is an array of objects with the 54 | following values from the relation: "id", "role", "tags". 55 | 56 | * `IncludeInvalidPolygons(yes bool)` 57 | 58 | By default, inner rings of 'multipolygon' without a matching outer ring will be ignored. 59 | However, in some use cases the outer ring can be implied as the viewport bound and the inner rings 60 | can then be rendered correctly. Polygons with a nil first ring will be need to be updated such 61 | that the first ring is the viewport bound. This options will also include rings that do not 62 | have matching endpoints. Usually this means one or more of the outer ways are missing. 63 | 64 | 65 | ### Benchmarks 66 | 67 | These benchmarks are meant to show the performance impact of the different options. 68 | They were run on a 2012 MacBook Air with a 2 ghz processor and 8 gigs of ram. 69 | 70 | ``` 71 | BenchmarkConvert-4 10000 2520891 ns/op 935697 B/op 11299 allocs/op 72 | BenchmarkConvertAnnotated-4 10000 2196433 ns/op 853544 B/op 11239 allocs/op 73 | BenchmarkConvert_NoID-4 10000 2310816 ns/op 913915 B/op 9687 allocs/op 74 | BenchmarkConvert_NoMeta-4 10000 2026031 ns/op 716953 B/op 7546 allocs/op 75 | BenchmarkConvert_NoRelationMembership-4 10000 2397634 ns/op 912454 B/op 10716 allocs/op 76 | BenchmarkConvert_NoIDsMetaMembership-4 20000 1718224 ns/op 671984 B/op 5353 allocs/op 77 | ``` 78 | 79 | #### Similar libraries in other languages: 80 | 81 | * [osmtogeojson](https://github.com/tyrasd/osmtogeojson) - Node 82 | -------------------------------------------------------------------------------- /osmgeojson/benchmarks_test.go: -------------------------------------------------------------------------------- 1 | package osmgeojson 2 | 3 | import ( 4 | "encoding/xml" 5 | "os" 6 | "testing" 7 | 8 | "github.com/paulmach/osm" 9 | ) 10 | 11 | func BenchmarkConvert(b *testing.B) { 12 | o := parseFile(b, "testdata/benchmark.osm") 13 | 14 | b.ReportAllocs() 15 | b.ResetTimer() 16 | for n := 0; n < b.N; n++ { 17 | _, err := Convert(o) 18 | if err != nil { 19 | b.Fatalf("convert error: %v", err) 20 | } 21 | } 22 | } 23 | 24 | func BenchmarkConvertAnnotated(b *testing.B) { 25 | o := parseFile(b, "testdata/benchmark.osm") 26 | annotate(o) 27 | 28 | b.ReportAllocs() 29 | b.ResetTimer() 30 | for n := 0; n < b.N; n++ { 31 | _, err := Convert(o) 32 | if err != nil { 33 | b.Fatalf("convert error: %v", err) 34 | } 35 | } 36 | } 37 | 38 | func BenchmarkConvert_NoID(b *testing.B) { 39 | o := parseFile(b, "testdata/benchmark.osm") 40 | 41 | b.ReportAllocs() 42 | b.ResetTimer() 43 | for n := 0; n < b.N; n++ { 44 | _, err := Convert(o, NoID(true)) 45 | if err != nil { 46 | b.Fatalf("convert error: %v", err) 47 | } 48 | } 49 | } 50 | 51 | func BenchmarkConvert_NoMeta(b *testing.B) { 52 | o := parseFile(b, "testdata/benchmark.osm") 53 | 54 | b.ReportAllocs() 55 | b.ResetTimer() 56 | for n := 0; n < b.N; n++ { 57 | _, err := Convert(o, NoMeta(true)) 58 | if err != nil { 59 | b.Fatalf("convert error: %v", err) 60 | } 61 | } 62 | } 63 | 64 | func BenchmarkConvert_NoRelationMembership(b *testing.B) { 65 | o := parseFile(b, "testdata/benchmark.osm") 66 | 67 | b.ReportAllocs() 68 | b.ResetTimer() 69 | for n := 0; n < b.N; n++ { 70 | _, err := Convert(o, NoRelationMembership(true)) 71 | if err != nil { 72 | b.Fatalf("convert error: %v", err) 73 | } 74 | } 75 | } 76 | 77 | func BenchmarkConvert_NoIDsMetaMembership(b *testing.B) { 78 | o := parseFile(b, "testdata/benchmark.osm") 79 | 80 | b.ReportAllocs() 81 | b.ResetTimer() 82 | for n := 0; n < b.N; n++ { 83 | _, err := Convert(o, NoID(true), NoMeta(true), NoRelationMembership(true)) 84 | if err != nil { 85 | b.Fatalf("convert error: %v", err) 86 | } 87 | } 88 | } 89 | 90 | func parseFile(t testing.TB, filename string) *osm.OSM { 91 | data, err := os.ReadFile(filename) 92 | if err != nil { 93 | t.Fatalf("could not read file: %v", err) 94 | } 95 | 96 | o := &osm.OSM{} 97 | err = xml.Unmarshal(data, &o) 98 | if err != nil { 99 | t.Fatalf("failed to unmarshal %s: %v", filename, err) 100 | } 101 | 102 | return o 103 | } 104 | 105 | func annotate(o *osm.OSM) { 106 | nodes := make(map[osm.NodeID]*osm.Node) 107 | for _, n := range o.Nodes { 108 | nodes[n.ID] = n 109 | } 110 | 111 | for _, w := range o.Ways { 112 | for i, wn := range w.Nodes { 113 | n := nodes[wn.ID] 114 | if n == nil { 115 | continue 116 | } 117 | 118 | w.Nodes[i].Lat = n.Lat 119 | w.Nodes[i].Lon = n.Lon 120 | w.Nodes[i].Version = n.Version 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /osmgeojson/options.go: -------------------------------------------------------------------------------- 1 | package osmgeojson 2 | 3 | // An Option is a setting for creating the geojson. 4 | type Option func(*context) error 5 | 6 | // NoID will omit setting the geojson feature.ID 7 | func NoID(yes bool) Option { 8 | return func(ctx *context) error { 9 | ctx.noID = yes 10 | return nil 11 | } 12 | } 13 | 14 | // NoMeta will omit the meta (timestamp, user, changeset, etc) info 15 | // from the output geojson feature properties. 16 | func NoMeta(yes bool) Option { 17 | return func(ctx *context) error { 18 | ctx.noMeta = yes 19 | return nil 20 | } 21 | } 22 | 23 | // NoRelationMembership will omit the list of relations 24 | // an element is a member of from the output geojson features. 25 | func NoRelationMembership(yes bool) Option { 26 | return func(ctx *context) error { 27 | ctx.noRelationMembership = yes 28 | return nil 29 | } 30 | } 31 | 32 | // IncludeInvalidPolygons will return a polygon with nil outer/first ring 33 | // if the outer ringer is not found in the data. It may also return 34 | // rings whose endpoints do not match and are probably missing sections. 35 | func IncludeInvalidPolygons(yes bool) Option { 36 | return func(ctx *context) error { 37 | ctx.includeInvalidPolygons = yes 38 | return nil 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /osmgeojson/options_test.go: -------------------------------------------------------------------------------- 1 | package osmgeojson 2 | 3 | import ( 4 | "encoding/xml" 5 | "testing" 6 | 7 | "github.com/paulmach/orb/geojson" 8 | "github.com/paulmach/osm" 9 | ) 10 | 11 | var nodeXML = ` 12 | 13 | 14 | 15 | 16 | 24 | ` 25 | 26 | var wayXML = ` 27 | 28 | 29 | 30 | 31 | 39 | 40 | 41 | 42 | 43 | 44 | ` 45 | 46 | var relationXML = ` 47 | 48 | 49 | 50 | 51 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | ` 72 | 73 | func TestOptionNoID(t *testing.T) { 74 | test := func(t *testing.T, xml string) { 75 | feature := convertXML(t, xml).Features[0] 76 | if v := feature.ID; v == nil { 77 | t.Errorf("id should be set: %v", v) 78 | } 79 | 80 | feature = convertXML(t, xml, NoID(true)).Features[0] 81 | if v := feature.ID; v != nil { 82 | t.Errorf("id should be nil: %v", v) 83 | } 84 | } 85 | 86 | t.Run("node", func(t *testing.T) { 87 | test(t, nodeXML) 88 | }) 89 | 90 | t.Run("way", func(t *testing.T) { 91 | test(t, wayXML) 92 | }) 93 | 94 | t.Run("relation", func(t *testing.T) { 95 | test(t, relationXML) 96 | }) 97 | } 98 | 99 | func TestOptionNoMeta(t *testing.T) { 100 | test := func(t *testing.T, xml string) { 101 | feature := convertXML(t, xml).Features[0] 102 | if v := feature.Properties["meta"]; v == nil { 103 | t.Errorf("meta should be set: %v", v) 104 | } 105 | 106 | feature = convertXML(t, xml, NoMeta(true)).Features[0] 107 | if v := feature.Properties["meta"]; v != nil { 108 | t.Errorf("meta should be nil: %v", v) 109 | } 110 | } 111 | 112 | t.Run("node", func(t *testing.T) { 113 | test(t, nodeXML) 114 | }) 115 | 116 | t.Run("way", func(t *testing.T) { 117 | test(t, wayXML) 118 | }) 119 | 120 | t.Run("relation", func(t *testing.T) { 121 | test(t, relationXML) 122 | }) 123 | } 124 | 125 | func TestOptionNoRelationMembership(t *testing.T) { 126 | test := func(t *testing.T, xml string) { 127 | feature := convertXML(t, xml).Features[0] 128 | if v := feature.Properties["relations"]; v == nil { 129 | t.Errorf("relations should be set: %v", v) 130 | } 131 | 132 | feature = convertXML(t, xml, NoRelationMembership(true)).Features[0] 133 | if v := feature.Properties["relations"]; v != nil { 134 | t.Errorf("relations should be nil: %v", v) 135 | } 136 | } 137 | 138 | t.Run("node", func(t *testing.T) { 139 | test(t, nodeXML) 140 | }) 141 | 142 | t.Run("way", func(t *testing.T) { 143 | test(t, wayXML) 144 | }) 145 | 146 | t.Run("relation", func(t *testing.T) { 147 | test(t, relationXML) 148 | }) 149 | } 150 | 151 | func convertXML(t *testing.T, data string, opts ...Option) *geojson.FeatureCollection { 152 | o := &osm.OSM{} 153 | err := xml.Unmarshal([]byte(data), &o) 154 | if err != nil { 155 | t.Fatalf("failed to unmarshal xml: %v", err) 156 | } 157 | 158 | fc, err := Convert(o, opts...) 159 | if err != nil { 160 | t.Fatalf("failed to convert: %v", err) 161 | } 162 | 163 | return fc 164 | } 165 | -------------------------------------------------------------------------------- /osmpbf/README.md: -------------------------------------------------------------------------------- 1 | # osm/osmpbf [![Go Reference](https://pkg.go.dev/badge/github.com/paulmach/osm.svg)](https://pkg.go.dev/github.com/paulmach/osm/osmpbf) 2 | 3 | Package osmpbf provides a scanner for decoding large [OSM PBF](https://wiki.openstreetmap.org/wiki/PBF_Format) files. 4 | They are typically found at [planet.osm.org](https://planet.openstreetmap.org/) or [Geofabrik Download](https://download.geofabrik.de/). 5 | 6 | ## Example: 7 | 8 | ```go 9 | file, err := os.Open("./delaware-latest.osm.pbf") 10 | if err != nil { 11 | panic(err) 12 | } 13 | defer file.Close() 14 | 15 | // The third parameter is the number of parallel decoders to use. 16 | scanner := osmpbf.New(context.Background(), file, runtime.GOMAXPROCS(-1)) 17 | defer scanner.Close() 18 | 19 | for scanner.Scan() { 20 | switch o := scanner.Object().(type) { 21 | case *osm.Node: 22 | 23 | case *osm.Way: 24 | 25 | case *osm.Relation: 26 | 27 | } 28 | } 29 | 30 | if err := scanner.Err(); err != nil { 31 | panic(err) 32 | } 33 | ``` 34 | 35 | **Note:** Scanners are **not** safe for parallel use. One should feed the 36 | objects into a channel and have workers read from that. 37 | 38 | ## Skipping Types 39 | 40 | Sometimes only ways or relations are needed. In this case reading and creating 41 | those objects can be skipped completely. After creating the Scanner set the appropriate 42 | attributes to true. 43 | 44 | ``` 45 | type Scanner struct { 46 | // Skip element types that are not needed. The data is skipped 47 | // at the encoded protobuf level, but each block still 48 | // needs to be decompressed. 49 | SkipNodes bool 50 | SkipWays bool 51 | SkipRelations bool 52 | 53 | // contains filtered or unexported fields 54 | } 55 | ``` 56 | 57 | ## Filtering Elements 58 | 59 | The above skips all elements of a type. To filter based on the element's tags or 60 | other values, use the filter functions. These filter functions are called in parallel 61 | and not in a predefined order. This can be a performant way to filter for elements 62 | with a certain set of tags. 63 | 64 | ``` 65 | type Scanner struct { 66 | // If the Filter function is false, the element well be skipped 67 | // at the decoding level. The functions should be fast, they block the 68 | // decoder, there are `procs` number of concurrent decoders. 69 | // Elements can be stored if the function returns true. Memory is 70 | // reused if the filter returns false. 71 | FilterNode func(*osm.Node) bool 72 | FilterWay func(*osm.Way) bool 73 | FilterRelation func(*osm.Relation) bool 74 | 75 | // contains filtered or unexported fields 76 | } 77 | ``` 78 | 79 | ## OSM PBF files with node locations on ways 80 | 81 | This package supports reading OSM PBF files where the ways have been annotated with the coordinates of each node. Such files can be generated using [osmium](https://osmcode.org/osmium-tool), with the [add-locations-to-ways](https://docs.osmcode.org/osmium/latest/osmium-add-locations-to-ways.html) subcommand. This feature makes it possible to work with the ways and their geometries without having to keep all node locations in some index (which takes work and memory resources). 82 | 83 | Coordinates are stored in the `Lat` and `Lon` fields of each `WayNode`. There is no need to specify an explicit option; when the node locations are present on the ways, they are loaded automatically. For more info about the OSM PBF format extension, see [the original blog post](https://blog.jochentopf.com/2016-04-20-node-locations-on-ways.html). 84 | 85 | ## Using cgo/czlib for decompression 86 | 87 | OSM PBF files are a set of blocks that are zlib compressed. When using the pure golang 88 | implementation this can account for about 1/3 of the read time. When cgo is enabled 89 | the package will used [czlib](https://github.com/DataDog/czlib). 90 | 91 | ``` 92 | $ CGO_ENABLED=0 go test -bench . > disabled.txt 93 | $ CGO_ENABLED=1 go test -bench . > enabled.txt 94 | $ benchcmp disabled.txt enabled.txt 95 | benchmark old ns/op new ns/op delta 96 | BenchmarkLondon-12 312294630 229927205 -26.37% 97 | BenchmarkLondon_nodes-12 246562457 160021768 -35.10% 98 | BenchmarkLondon_ways-12 216803544 134747327 -37.85% 99 | BenchmarkLondon_relations-12 158722633 80560144 -49.24% 100 | 101 | benchmark old allocs new allocs delta 102 | BenchmarkLondon-12 2469128 2416804 -2.12% 103 | BenchmarkLondon_nodes-12 1056166 1003850 -4.95% 104 | BenchmarkLondon_ways-12 1845032 1792716 -2.84% 105 | BenchmarkLondon_relations-12 509090 456772 -10.28% 106 | 107 | benchmark old bytes new bytes delta 108 | BenchmarkLondon-12 963734544 954877896 -0.92% 109 | BenchmarkLondon_nodes-12 658337435 649482060 -1.35% 110 | BenchmarkLondon_ways-12 441674734 432819378 -2.00% 111 | BenchmarkLondon_relations-12 187941609 179086389 -4.71% 112 | ``` 113 | -------------------------------------------------------------------------------- /osmpbf/internal/osmpbf/README.md: -------------------------------------------------------------------------------- 1 | *.proto files were downloaded from https://github.com/scrosby/OSM-binary/tree/master/src and changed in following ways: 2 | 3 | * To eliminate continuous conversions from `[]byte` to `string`, this 4 | 5 | ```protobuf 6 | message StringTable { 7 | repeated bytes s = 1; 8 | } 9 | ``` 10 | 11 | was changed to 12 | 13 | ```protobuf 14 | message StringTable { 15 | repeated string s = 1; 16 | } 17 | ``` 18 | 19 | This changes is expected to be fully compatible with all PBF files. 20 | -------------------------------------------------------------------------------- /osmpbf/internal/osmpbf/fileformat.proto: -------------------------------------------------------------------------------- 1 | /** Copyright (c) 2010 Scott A. Crosby. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | */ 10 | 11 | syntax = "proto2"; 12 | 13 | option java_package = "crosby.binary"; 14 | package osmpbf; 15 | 16 | //protoc --java_out=../.. fileformat.proto 17 | option go_package = "github.com/paulmach/osmpbf/internal/osmpbf"; 18 | 19 | // 20 | // STORAGE LAYER: Storing primitives. 21 | // 22 | 23 | message Blob { 24 | optional bytes raw = 1; // No compression 25 | optional int32 raw_size = 2; // When compressed, the uncompressed size 26 | 27 | // Possible compressed versions of the data. 28 | optional bytes zlib_data = 3; 29 | 30 | // PROPOSED feature for LZMA compressed data. SUPPORT IS NOT REQUIRED. 31 | optional bytes lzma_data = 4; 32 | 33 | // Formerly used for bzip2 compressed data. Depreciated in 2010. 34 | optional bytes OBSOLETE_bzip2_data = 5 [deprecated=true]; // Don't reuse this tag number. 35 | } 36 | 37 | /* A file contains an sequence of fileblock headers, each prefixed by 38 | their length in network byte order, followed by a data block 39 | containing the actual data. types staring with a "_" are reserved. 40 | */ 41 | 42 | message BlobHeader { 43 | required string type = 1; 44 | optional bytes indexdata = 2; 45 | required int32 datasize = 3; 46 | } 47 | 48 | 49 | -------------------------------------------------------------------------------- /osmpbf/internal/osmpbf/generate.go: -------------------------------------------------------------------------------- 1 | package osmpbf 2 | 3 | //go:generate protoc --proto_path=. --go_opt=module=github.com/paulmach/osmpbf/internal/osmpbf --go_out=. fileformat.proto osmformat.proto 4 | -------------------------------------------------------------------------------- /osmpbf/scanner.go: -------------------------------------------------------------------------------- 1 | package osmpbf 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "sync/atomic" 7 | 8 | "github.com/paulmach/osm" 9 | ) 10 | 11 | var _ osm.Scanner = &Scanner{} 12 | 13 | // Scanner provides a convenient interface reading a stream of osm data 14 | // from a file or url. Successive calls to the Scan method will step through the data. 15 | // 16 | // Scanning stops unrecoverably at EOF, the first I/O error, the first xml error or 17 | // the context being cancelled. When a scan stops, the reader may have advanced 18 | // arbitrarily far past the last token. 19 | // 20 | // The Scanner API is based on bufio.Scanner 21 | // https://golang.org/pkg/bufio/#Scanner 22 | type Scanner struct { 23 | // Skip element types that are not needed. The data is skipped 24 | // at the encoded protobuf level, but each block still needs to be decompressed. 25 | SkipNodes bool 26 | SkipWays bool 27 | SkipRelations bool 28 | 29 | // If the Filter function is false, the element well be skipped 30 | // at the decoding level. The functions should be fast, they block the 31 | // decoder, there are `procs` number of concurrent decoders. 32 | // Elements can be stored if the function returns true. Memory is 33 | // reused if the filter returns false. 34 | FilterNode func(*osm.Node) bool 35 | FilterWay func(*osm.Way) bool 36 | FilterRelation func(*osm.Relation) bool 37 | 38 | ctx context.Context 39 | closed bool 40 | 41 | decoder *decoder 42 | started bool 43 | procs int 44 | next osm.Object 45 | err error 46 | } 47 | 48 | // New returns a new Scanner to read from r. 49 | // procs indicates amount of paralellism, when reading blocks 50 | // which will off load the unzipping/decoding to multiple cpus. 51 | func New(ctx context.Context, r io.Reader, procs int) *Scanner { 52 | if ctx == nil { 53 | ctx = context.Background() 54 | } 55 | 56 | s := &Scanner{ 57 | ctx: ctx, 58 | procs: procs, 59 | } 60 | s.decoder = newDecoder(ctx, s, r) 61 | return s 62 | } 63 | 64 | // FullyScannedBytes returns the number of bytes that have been read 65 | // and fully scanned. OSM protobuf files contain data blocks with 66 | // 8000 nodes each. The returned value contains the bytes for the blocks 67 | // that have been fully scanned. 68 | // 69 | // A user can use this number of seek forward in a file 70 | // and begin reading mid-data. Note that while elements are usually sorted 71 | // by Type, ID, Version in OSM protobuf files, versions of given element may 72 | // span blocks. 73 | func (s *Scanner) FullyScannedBytes() int64 { 74 | return atomic.LoadInt64(&s.decoder.cOffset) 75 | } 76 | 77 | // PreviousFullyScannedBytes returns the previous value of FullyScannedBytes. 78 | // This is interesting because it's not totally clear if a feature spans a block. 79 | // For example, if one quits after finding the first relation, upon restarting there 80 | // is no way of knowing if the first relation is complete, so skip it. But if this relation 81 | // is the first relation in the file we'll skip a full relation. 82 | func (s *Scanner) PreviousFullyScannedBytes() int64 { 83 | return atomic.LoadInt64(&s.decoder.pOffset) 84 | } 85 | 86 | // Close cleans up all the reading goroutines, it does not 87 | // close the underlying reader. 88 | func (s *Scanner) Close() error { 89 | s.closed = true 90 | return s.decoder.Close() 91 | } 92 | 93 | // Header returns the pbf file header with interesting information 94 | // about how it was created. 95 | func (s *Scanner) Header() (*Header, error) { 96 | if !s.started { 97 | s.started = true 98 | // the header gets read before Start returns 99 | s.err = s.decoder.Start(s.procs) 100 | } 101 | 102 | return s.decoder.header, s.err 103 | } 104 | 105 | // Scan advances the Scanner to the next element, which will then be available 106 | // through the Element method. It returns false when the scan stops, either 107 | // by reaching the end of the input, an io error, an xml error or the context 108 | // being cancelled. After Scan returns false, the Err method will return any 109 | // error that occurred during scanning, except that if it was io.EOF, Err will 110 | // return nil. 111 | func (s *Scanner) Scan() bool { 112 | if !s.started { 113 | s.started = true 114 | s.err = s.decoder.Start(s.procs) 115 | } 116 | 117 | if s.err != nil || s.closed || s.ctx.Err() != nil { 118 | return false 119 | } 120 | 121 | s.next, s.err = s.decoder.Next() 122 | return s.err == nil 123 | } 124 | 125 | // Object returns the most recent token generated by a call to Scan 126 | // as a new osm.Object. Currently osm.pbf files only contain nodes, ways and 127 | // relations. This method returns an object so match the osm.Scanner interface 128 | // and allows this Scanner to share an interface with osmxml.Scanner. 129 | func (s *Scanner) Object() osm.Object { 130 | return s.next 131 | } 132 | 133 | // Err returns the first non-EOF error that was encountered by the Scanner. 134 | func (s *Scanner) Err() error { 135 | if s.err == io.EOF { 136 | return nil 137 | } 138 | 139 | if s.err != nil { 140 | return s.err 141 | } 142 | 143 | if s.closed { 144 | return osm.ErrScannerClosed 145 | } 146 | 147 | return s.ctx.Err() 148 | } 149 | -------------------------------------------------------------------------------- /osmpbf/zlib_cgo.go: -------------------------------------------------------------------------------- 1 | // +build cgo 2 | 3 | package osmpbf 4 | 5 | import ( 6 | "bytes" 7 | "io" 8 | 9 | "github.com/datadog/czlib" 10 | ) 11 | 12 | func zlibReader(data []byte) (io.ReadCloser, error) { 13 | return czlib.NewReader(bytes.NewReader(data)) 14 | } 15 | -------------------------------------------------------------------------------- /osmpbf/zlib_go.go: -------------------------------------------------------------------------------- 1 | // +build !cgo 2 | 3 | package osmpbf 4 | 5 | import ( 6 | "bytes" 7 | "compress/zlib" 8 | "io" 9 | ) 10 | 11 | func zlibReader(data []byte) (io.ReadCloser, error) { 12 | return zlib.NewReader(bytes.NewReader(data)) 13 | } 14 | -------------------------------------------------------------------------------- /osmtest/scanner.go: -------------------------------------------------------------------------------- 1 | package osmtest 2 | 3 | import "github.com/paulmach/osm" 4 | 5 | // Scanner implements the osm.Scanner interface with 6 | // just a list of objects. 7 | type Scanner struct { 8 | // ScanError can be used to trigger an error. 9 | // If non-nil, Next() will return false and Err() will 10 | // return this error. 11 | ScanError error 12 | 13 | offset int 14 | objects osm.Objects 15 | } 16 | 17 | var _ osm.Scanner = &Scanner{} 18 | 19 | // NewScanner creates a new test scanner useful for test stubbing. 20 | func NewScanner(objects osm.Objects) *Scanner { 21 | return &Scanner{ 22 | offset: -1, 23 | objects: objects, 24 | } 25 | } 26 | 27 | // Scan progresses the scanner to the next object. 28 | func (s *Scanner) Scan() bool { 29 | if s.ScanError != nil { 30 | return false 31 | } 32 | 33 | s.offset++ 34 | return s.offset < len(s.objects) 35 | } 36 | 37 | // Object returns the current object. 38 | func (s *Scanner) Object() osm.Object { 39 | return s.objects[s.offset] 40 | } 41 | 42 | // Err returns the scanner.ScanError. 43 | func (s *Scanner) Err() error { 44 | return s.ScanError 45 | } 46 | 47 | // Close is a stub for this test scanner. 48 | func (s *Scanner) Close() error { 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /osmtest/scanner_test.go: -------------------------------------------------------------------------------- 1 | package osmtest 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/paulmach/osm" 9 | ) 10 | 11 | func TestScanner(t *testing.T) { 12 | objs := osm.Objects{ 13 | &osm.Node{ID: 1, Version: 4}, 14 | &osm.Way{ID: 2, Version: 5}, 15 | &osm.Relation{ID: 3, Version: 6}, 16 | } 17 | 18 | scanner := NewScanner(objs) 19 | defer scanner.Close() 20 | 21 | expected := osm.ObjectIDs{ 22 | osm.NodeID(1).ObjectID(4), 23 | osm.WayID(2).ObjectID(5), 24 | osm.RelationID(3).ObjectID(6), 25 | } 26 | 27 | ids := osm.ObjectIDs{} 28 | for scanner.Scan() { 29 | ids = append(ids, scanner.Object().ObjectID()) 30 | } 31 | 32 | if !reflect.DeepEqual(ids, expected) { 33 | t.Errorf("incorrect ids: %v", ids) 34 | } 35 | } 36 | 37 | func TestScanner_error(t *testing.T) { 38 | objs := osm.Objects{ 39 | &osm.Node{ID: 1, Version: 4}, 40 | &osm.Way{ID: 2, Version: 5}, 41 | &osm.Relation{ID: 3, Version: 6}, 42 | } 43 | 44 | scanner := NewScanner(objs) 45 | defer scanner.Close() 46 | 47 | if scanner.Err() != nil { 48 | t.Errorf("error should not be set initially") 49 | } 50 | 51 | if !scanner.Scan() { 52 | t.Errorf("should be true initially") 53 | } 54 | 55 | scanner.ScanError = errors.New("some error") 56 | 57 | if scanner.Scan() { 58 | t.Errorf("should be false after error") 59 | } 60 | 61 | if scanner.Err() == nil { 62 | t.Errorf("should return error if there is one") 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /osmxml/example_test.go: -------------------------------------------------------------------------------- 1 | package osmxml_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/paulmach/osm" 9 | "github.com/paulmach/osm/osmxml" 10 | ) 11 | 12 | func ExampleScanner() { 13 | scanner := osmxml.New(context.Background(), os.Stdin) 14 | for scanner.Scan() { 15 | fmt.Println(scanner.Object().(*osm.Changeset)) 16 | } 17 | 18 | if err := scanner.Err(); err != nil { 19 | fmt.Fprintln(os.Stderr, "reading standard input:", err) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /osmxml/scanner.go: -------------------------------------------------------------------------------- 1 | package osmxml 2 | 3 | import ( 4 | "context" 5 | "encoding/xml" 6 | "io" 7 | "strings" 8 | 9 | "github.com/paulmach/osm" 10 | ) 11 | 12 | var _ osm.Scanner = &Scanner{} 13 | 14 | // Scanner provides a convenient interface reading a stream of osm data 15 | // from a file or url. Successive calls to the Scan method will step through the data. 16 | // 17 | // Scanning stops unrecoverably at EOF, the first I/O error, the first xml error or 18 | // the context being cancelled. When a scan stops, the reader may have advanced 19 | // arbitrarily far past the last token. 20 | // 21 | // The Scanner API is based on bufio.Scanner 22 | // https://golang.org/pkg/bufio/#Scanner 23 | type Scanner struct { 24 | ctx context.Context 25 | done context.CancelFunc 26 | closed bool 27 | 28 | decoder *xml.Decoder 29 | next osm.Object 30 | err error 31 | } 32 | 33 | // New returns a new Scanner to read from r. 34 | func New(ctx context.Context, r io.Reader) *Scanner { 35 | if ctx == nil { 36 | ctx = context.Background() 37 | } 38 | 39 | s := &Scanner{ 40 | decoder: xml.NewDecoder(r), 41 | } 42 | 43 | s.ctx, s.done = context.WithCancel(ctx) 44 | return s 45 | } 46 | 47 | // Close causes all future calls to Scan to return false. 48 | // Does not close the underlying reader. 49 | func (s *Scanner) Close() error { 50 | s.closed = true 51 | s.done() 52 | 53 | return nil 54 | } 55 | 56 | // Scan advances the Scanner to the next element, which will then be available 57 | // through the Object method. It returns false when the scan stops, either 58 | // by reaching the end of the input, an io error, an xml error or the context 59 | // being cancelled. After Scan returns false, the Err method will return any 60 | // error that occurred during scanning, except if it was io.EOF, Err will 61 | // return nil. 62 | func (s *Scanner) Scan() bool { 63 | if s.err != nil { 64 | return false 65 | } 66 | 67 | Loop: 68 | for { 69 | if s.ctx.Err() != nil { 70 | return false 71 | } 72 | 73 | t, err := s.decoder.Token() 74 | if err != nil { 75 | s.err = err 76 | return false 77 | } 78 | 79 | se, ok := t.(xml.StartElement) 80 | if !ok { 81 | continue 82 | } 83 | 84 | s.next = nil 85 | switch strings.ToLower(se.Name.Local) { 86 | case "bounds": 87 | bounds := &osm.Bounds{} 88 | err = s.decoder.DecodeElement(&bounds, &se) 89 | s.next = bounds 90 | case "node": 91 | node := &osm.Node{} 92 | err = s.decoder.DecodeElement(&node, &se) 93 | s.next = node 94 | case "way": 95 | way := &osm.Way{} 96 | err = s.decoder.DecodeElement(&way, &se) 97 | s.next = way 98 | case "relation": 99 | relation := &osm.Relation{} 100 | err = s.decoder.DecodeElement(&relation, &se) 101 | s.next = relation 102 | case "changeset": 103 | cs := &osm.Changeset{} 104 | err = s.decoder.DecodeElement(&cs, &se) 105 | s.next = cs 106 | case "note": 107 | n := &osm.Note{} 108 | err = s.decoder.DecodeElement(&n, &se) 109 | s.next = n 110 | case "user": 111 | u := &osm.User{} 112 | err = s.decoder.DecodeElement(&u, &se) 113 | s.next = u 114 | default: 115 | continue Loop 116 | } 117 | 118 | if err != nil { 119 | s.err = err 120 | return false 121 | } 122 | 123 | return true 124 | } 125 | } 126 | 127 | // Object returns the most recent token generated by a call to Scan 128 | // as a new osm.Object. This interface is implemented by: 129 | // *osm.Bounds 130 | // *osm.Node 131 | // *osm.Way 132 | // *osm.Relation 133 | // *osm.Changeset 134 | // *osm.Note 135 | // *osm.User 136 | func (s *Scanner) Object() osm.Object { 137 | return s.next 138 | } 139 | 140 | // Err returns the first non-EOF error that was encountered by the Scanner. 141 | func (s *Scanner) Err() error { 142 | if s.err == io.EOF { 143 | return nil 144 | } 145 | 146 | if s.err != nil { 147 | return s.err 148 | } 149 | 150 | if s.closed { 151 | return osm.ErrScannerClosed 152 | } 153 | 154 | return s.ctx.Err() 155 | } 156 | -------------------------------------------------------------------------------- /polygon_test.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestWay_Polygon(t *testing.T) { 9 | w := &Way{} 10 | w.Nodes = WayNodes{ 11 | {ID: 1}, {ID: 2}, 12 | {ID: 3}, {ID: 1}, 13 | } 14 | 15 | w2 := &Way{Nodes: w.Nodes[:3]} 16 | if w2.Polygon() { 17 | t.Errorf("should be over 3 nodes to be polygon") 18 | } 19 | 20 | w.Nodes[3].ID = 10 21 | if w2.Polygon() { 22 | t.Errorf("first and last node must have same id") 23 | } 24 | 25 | c := polyConditions[1].Values 26 | if !reflect.DeepEqual(c, []string{"elevator", "escape", "rest_area", "services"}) { 27 | t.Errorf("values not sorted") 28 | } 29 | 30 | cases := []struct { 31 | name string 32 | tags []Tag 33 | value bool 34 | }{ 35 | { 36 | name: "area no overrides", 37 | tags: []Tag{ 38 | {Key: "area", Value: "no"}, 39 | {Key: "building", Value: "yes"}, 40 | }, 41 | value: false, 42 | }, 43 | { 44 | name: "non-empty area and not no", 45 | tags: []Tag{ 46 | {Key: "area", Value: "maybe"}, 47 | {Key: "building", Value: "no"}, 48 | }, 49 | value: true, 50 | }, 51 | { 52 | name: "at least one condition met", 53 | tags: []Tag{ 54 | {Key: "building", Value: "no"}, 55 | {Key: "boundary", Value: "yes"}, 56 | }, 57 | value: true, 58 | }, 59 | { 60 | name: "match within whitelist", 61 | tags: []Tag{ 62 | {Key: "railway", Value: "station"}, 63 | }, 64 | value: true, 65 | }, 66 | { 67 | name: "not match if not within whitelist", 68 | tags: []Tag{ 69 | {Key: "railway", Value: "line"}, 70 | }, 71 | value: false, 72 | }, 73 | { 74 | name: "not match within blacklist", 75 | tags: []Tag{ 76 | {Key: "man_made", Value: "cutline"}, 77 | }, 78 | value: false, 79 | }, 80 | { 81 | name: "match if not within blacklist", 82 | tags: []Tag{ 83 | {Key: "man_made", Value: "thing"}, 84 | }, 85 | value: true, 86 | }, 87 | { 88 | name: "indoor anything is a polygon", 89 | tags: []Tag{ 90 | {Key: "indoor", Value: "anything"}, 91 | }, 92 | value: true, 93 | }, 94 | } 95 | 96 | for _, tc := range cases { 97 | t.Run(tc.name, func(t *testing.T) { 98 | w := &Way{ 99 | Nodes: WayNodes{ 100 | {ID: 1}, {ID: 2}, 101 | {ID: 3}, {ID: 1}, 102 | }, 103 | Tags: Tags(tc.tags), 104 | } 105 | 106 | if v := w.Polygon(); v != tc.value { 107 | t.Errorf("not correctly detected, %v != %v", v, tc.value) 108 | } 109 | }) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /replication/README.md: -------------------------------------------------------------------------------- 1 | # osm/replication [![Godoc Reference](https://pkg.go.dev/badge/github.com/paulmach/osm)](https://pkg.go.dev/github.com/paulmach/osm/replication) 2 | 3 | Package `replication` handles fetching the Minute, Hour, Day and Changeset replication 4 | and the associated state value from [Planet OSM](http://planet.osm.org). 5 | 6 | For example, to fetch the current Minute replication state: 7 | 8 | ```go 9 | num, fullState, err := replication.CurrentMinuteState(ctx) 10 | ``` 11 | 12 | This is the data in [http://planet.osm.org/replication/minute/state.txt](http://planet.osm.org/replication/minute/state.txt) 13 | updated every minute. 14 | 15 | Once you know the change number you want, fetch the change using: 16 | 17 | ```go 18 | change, err := replication.Minute(ctx, num) 19 | ``` 20 | 21 | ## Finding sequences numbers by timestamp 22 | 23 | It's also possible to find the sequence number by timestamp. 24 | These calls make multiple requests for state to find the one matching the given timestamp. 25 | 26 | ```go 27 | MinuteStateAt(ctx context.Context, timestamp time.Time) (MinuteSeqNum, *State, error) 28 | HourStateAt(ctx context.Context, timestamp time.Time) (HourSeqNum, *State, error) 29 | DayStateAt(ctx context.Context, timestamp time.Time) (DaySeqNum, *State, error) 30 | ChangesetStateAt(ctx context.Context, timestamp time.Time) (ChangesetSeqNum, *State, error) 31 | ``` 32 | -------------------------------------------------------------------------------- /replication/changesets_test.go: -------------------------------------------------------------------------------- 1 | package replication 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "context" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestDecodeChangesetState(t *testing.T) { 12 | data := []byte(`--- 13 | last_run: 2016-07-02 22:46:01.422137422 Z 14 | sequence: 1912325 15 | `) 16 | 17 | state, err := decodeChangesetState(data) 18 | if v := ChangesetSeqNum(state.SeqNum); v != 1912325 { 19 | t.Errorf("incorrect sequence number, got %v", v) 20 | } 21 | 22 | if !state.Timestamp.Equal(time.Date(2016, 7, 2, 22, 46, 1, 422137422, time.UTC)) { 23 | t.Errorf("incorrect time, got %v", state.Timestamp) 24 | } 25 | 26 | if err != nil { 27 | t.Errorf("got error: %v", err) 28 | } 29 | } 30 | 31 | func TestChangesetDecoder(t *testing.T) { 32 | ctx := context.Background() 33 | 34 | buf := bytes.NewBuffer(nil) 35 | w := gzip.NewWriter(buf) 36 | w.Write([]byte(` 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | `)) 52 | w.Close() 53 | 54 | changesets, err := changesetDecoder(ctx, buf) 55 | if err != nil { 56 | t.Fatalf("decode error: %v", err) 57 | } 58 | 59 | if len(changesets) != 2 { 60 | t.Errorf("incorrect number of changes: %d", len(changesets)) 61 | } 62 | } 63 | 64 | func TestBaseChangesetURL(t *testing.T) { 65 | url := DefaultDatasource.baseChangesetURL(123456789) 66 | if url != "https://planet.osm.org/replication/changesets/123/456/789" { 67 | t.Errorf("incorrect url, got %v", url) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /replication/datasource.go: -------------------------------------------------------------------------------- 1 | package replication 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | // BaseURL defines the default planet server to hit. 10 | const BaseURL = "https://planet.osm.org" 11 | 12 | // Datasource defines context around replication data requests. 13 | type Datasource struct { 14 | BaseURL string // will use package level BaseURL if empty 15 | Client *http.Client 16 | } 17 | 18 | // DefaultDatasource is the Datasource used by the package level convenience functions. 19 | var DefaultDatasource = &Datasource{ 20 | Client: &http.Client{ 21 | Timeout: 30 * time.Minute, 22 | }, 23 | } 24 | 25 | // NewDatasource creates a Datasource using the given client. 26 | func NewDatasource(client *http.Client) *Datasource { 27 | return &Datasource{ 28 | Client: client, 29 | } 30 | } 31 | 32 | func (ds Datasource) baseURL() string { 33 | if ds.BaseURL != "" { 34 | return ds.BaseURL 35 | } 36 | 37 | return BaseURL 38 | } 39 | 40 | func (ds Datasource) client() *http.Client { 41 | if ds.Client != nil { 42 | return ds.Client 43 | } 44 | 45 | if DefaultDatasource.Client != nil { 46 | return DefaultDatasource.Client 47 | } 48 | 49 | return http.DefaultClient 50 | } 51 | 52 | // UnexpectedStatusCodeError is return for a non 200 or 404 status code. 53 | type UnexpectedStatusCodeError struct { 54 | Code int 55 | URL string 56 | } 57 | 58 | // Error returns an error message with some information. 59 | func (e *UnexpectedStatusCodeError) Error() string { 60 | return fmt.Sprintf("replication: unexpected status code of %d for url %s", e.Code, e.URL) 61 | } 62 | 63 | // NotFound will return try if the error from one of the methods was due 64 | // to the file not found on the remote host. 65 | func NotFound(err error) bool { 66 | if err == nil { 67 | return false 68 | } 69 | 70 | if e, ok := err.(*UnexpectedStatusCodeError); ok { 71 | return e.Code == http.StatusNotFound 72 | } 73 | 74 | return false 75 | } 76 | 77 | // timeFormats contains the set of different formats we've see the time in. 78 | var timeFormats = []string{ 79 | "2006-01-02 15:04:05.999999999 Z", 80 | "2006-01-02 15:04:05.999999999 +00:00", 81 | "2006-01-02T15\\:04\\:05Z", 82 | } 83 | 84 | func decodeTime(s string) (time.Time, error) { 85 | var ( 86 | t time.Time 87 | err error 88 | ) 89 | for _, format := range timeFormats { 90 | t, err = time.Parse(format, s) 91 | if err == nil { 92 | return t, nil 93 | } 94 | } 95 | 96 | return t, err 97 | } 98 | -------------------------------------------------------------------------------- /replication/interval_test.go: -------------------------------------------------------------------------------- 1 | package replication 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestDecodeIntervalState(t *testing.T) { 9 | data := []byte(`#Sat Jul 16 06:28:03 UTC 2016 10 | txnMaxQueried=836441250 11 | sequenceNumber=2010594 12 | timestamp=2016-07-16T06\:28\:02Z 13 | txnReadyList= 14 | txnMax=836441259 15 | txnActiveList=836441203 16 | `) 17 | 18 | state, err := decodeIntervalState(data) 19 | if v := MinuteSeqNum(state.SeqNum); v != 2010594 { 20 | t.Errorf("incorrect id, got %v", v) 21 | } 22 | 23 | if !state.Timestamp.Equal(time.Date(2016, 7, 16, 6, 28, 2, 0, time.UTC)) { 24 | t.Errorf("incorrect time, got %v", state.Timestamp) 25 | } 26 | 27 | if v := state.TxnMax; v != 836441259 { 28 | t.Errorf("incorrect txnMax, got %v", v) 29 | } 30 | 31 | if v := state.TxnMaxQueried; v != 836441250 { 32 | t.Errorf("incorrect txnMaxQueried, got %v", v) 33 | } 34 | 35 | if err != nil { 36 | t.Errorf("got error: %v", err) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /replication/live_test.go: -------------------------------------------------------------------------------- 1 | package replication 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func liveOnly(t testing.TB) { 10 | if os.Getenv("LIVE_TEST") != "true" { 11 | t.Skipf("skipping live test, set LIVE_TEST=true to enable") 12 | } 13 | } 14 | 15 | func TestCurrentState(t *testing.T) { 16 | liveOnly(t) 17 | ctx := context.Background() 18 | 19 | _, _, err := CurrentMinuteState(ctx) 20 | if err != nil { 21 | t.Fatalf("request error: %v", err) 22 | } 23 | 24 | _, _, err = CurrentHourState(ctx) 25 | if err != nil { 26 | t.Fatalf("request error: %v", err) 27 | } 28 | 29 | _, _, err = CurrentDayState(ctx) 30 | if err != nil { 31 | t.Fatalf("request error: %v", err) 32 | } 33 | } 34 | 35 | func TestDownloadChanges(t *testing.T) { 36 | liveOnly(t) 37 | ctx := context.Background() 38 | 39 | _, err := Minute(ctx, 10) 40 | if err != nil { 41 | t.Fatalf("request error: %v", err) 42 | } 43 | 44 | _, err = Hour(ctx, 10) 45 | if err != nil { 46 | t.Fatalf("request error: %v", err) 47 | } 48 | 49 | _, err = Day(ctx, 1) 50 | if err != nil { 51 | t.Fatalf("request error: %v", err) 52 | } 53 | } 54 | 55 | func TestCurrentChangesetState(t *testing.T) { 56 | liveOnly(t) 57 | 58 | ctx := context.Background() 59 | _, _, err := CurrentChangesetState(ctx) 60 | if err != nil { 61 | t.Fatalf("request error: %v", err) 62 | } 63 | } 64 | 65 | func TestChangesets(t *testing.T) { 66 | liveOnly(t) 67 | 68 | ctx := context.Background() 69 | sets, err := Changesets(ctx, 100) 70 | if err != nil { 71 | t.Fatalf("request error: %v", err) 72 | } 73 | 74 | if l := len(sets); l != 12 { 75 | t.Errorf("incorrect number of changesets: %v", l) 76 | } 77 | } 78 | 79 | func TestChangesetState(t *testing.T) { 80 | liveOnly(t) 81 | 82 | ctx := context.Background() 83 | state, err := ChangesetState(ctx, 5001990) 84 | if err != nil { 85 | t.Fatalf("request error: %v", err) 86 | } 87 | 88 | if state.SeqNum != 5001990 { 89 | t.Errorf("incorrect state: %+v", state) 90 | } 91 | 92 | // current state 93 | n, state, err := CurrentChangesetState(ctx) 94 | if err != nil { 95 | t.Fatalf("request error: %v", err) 96 | } 97 | 98 | changes, err := Changesets(ctx, n) 99 | if err != nil { 100 | t.Fatalf("request error: %v", err) 101 | } 102 | 103 | for _, c := range changes { 104 | if c.CreatedAt.After(state.Timestamp) { 105 | t.Errorf("data is after the state file?") 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tag.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import ( 4 | "encoding/json" 5 | "sort" 6 | ) 7 | 8 | // UninterestingTags are boring tags. If an element only has 9 | // these tags it does not usually need to be displayed. 10 | // For example, if a node with just these tags is part of a way, it 11 | // probably does not need its own icon along the way. 12 | var UninterestingTags = map[string]bool{ 13 | "source": true, 14 | "source_ref": true, 15 | "source:ref": true, 16 | "history": true, 17 | "attribution": true, 18 | "created_by": true, 19 | "tiger:county": true, 20 | "tiger:tlid": true, 21 | "tiger:upload_uuid": true, 22 | } 23 | 24 | // Tag is a key+value item attached to osm nodes, ways and relations. 25 | type Tag struct { 26 | Key string `xml:"k,attr"` 27 | Value string `xml:"v,attr"` 28 | } 29 | 30 | // Tags is a collection of Tag objects with some helper functions. 31 | type Tags []Tag 32 | 33 | // Find will return the value for the key. 34 | // Will return an empty string if not found. 35 | func (ts Tags) Find(k string) string { 36 | for _, t := range ts { 37 | if t.Key == k { 38 | return t.Value 39 | } 40 | } 41 | 42 | return "" 43 | } 44 | 45 | // FindTag will return the Tag for the given key. 46 | // Can be used to determine if a key exists, even with an empty value. 47 | // Returns nil if not found. 48 | func (ts Tags) FindTag(k string) *Tag { 49 | for _, t := range ts { 50 | if t.Key == k { 51 | return &t 52 | } 53 | } 54 | 55 | return nil 56 | } 57 | 58 | // HasTag will return the true if a tag exists for the given key. 59 | func (ts Tags) HasTag(k string) bool { 60 | for _, t := range ts { 61 | if t.Key == k { 62 | return true 63 | } 64 | } 65 | 66 | return false 67 | } 68 | 69 | // Map returns the tags as a key/value map. 70 | func (ts Tags) Map() map[string]string { 71 | result := make(map[string]string, len(ts)) 72 | for _, t := range ts { 73 | result[t.Key] = t.Value 74 | } 75 | 76 | return result 77 | } 78 | 79 | // AnyInteresting will return true if there is at last one interesting tag. 80 | func (ts Tags) AnyInteresting() bool { 81 | for _, t := range ts { 82 | if !UninterestingTags[t.Key] { 83 | return true 84 | } 85 | } 86 | 87 | return false 88 | } 89 | 90 | // MarshalJSON allows the tags to be marshalled as a key/value object, 91 | // as defined by the overpass osmjson. 92 | func (ts Tags) MarshalJSON() ([]byte, error) { 93 | return marshalJSON(ts.Map()) 94 | } 95 | 96 | // UnmarshalJSON allows the tags to be unmarshalled from a key/value object, 97 | // as defined by the overpass osmjson. 98 | func (ts *Tags) UnmarshalJSON(data []byte) error { 99 | o := make(map[string]string) 100 | err := json.Unmarshal(data, &o) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | tags := make(Tags, 0, len(o)) 106 | 107 | for k, v := range o { 108 | tags = append(tags, Tag{Key: k, Value: v}) 109 | } 110 | 111 | *ts = tags 112 | return nil 113 | } 114 | 115 | type tagsSort Tags 116 | 117 | // SortByKeyValue will do an inplace sort of the tags. 118 | func (ts Tags) SortByKeyValue() { 119 | sort.Sort(tagsSort(ts)) 120 | } 121 | func (ts tagsSort) Len() int { return len(ts) } 122 | func (ts tagsSort) Swap(i, j int) { ts[i], ts[j] = ts[j], ts[i] } 123 | func (ts tagsSort) Less(i, j int) bool { 124 | if ts[i].Key == ts[j].Key { 125 | return ts[i].Value < ts[j].Value 126 | } 127 | 128 | return ts[i].Key < ts[j].Key 129 | } 130 | -------------------------------------------------------------------------------- /tag_test.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestTags_FindTag(t *testing.T) { 10 | cases := []struct { 11 | name string 12 | tags Tags 13 | key string 14 | tag *Tag 15 | }{ 16 | { 17 | name: "find tag", 18 | tags: Tags{ 19 | {Key: "area", Value: "true"}, 20 | {Key: "building", Value: "yes"}, 21 | }, 22 | key: "building", 23 | tag: &Tag{Key: "building", Value: "yes"}, 24 | }, 25 | { 26 | name: "not found", 27 | tags: Tags{ 28 | {Key: "building", Value: "yes"}, 29 | }, 30 | key: "not found", 31 | tag: nil, 32 | }, 33 | { 34 | name: "empty value", 35 | tags: Tags{ 36 | {Key: "present", Value: ""}, 37 | }, 38 | key: "present", 39 | tag: &Tag{Key: "present", Value: ""}, 40 | }, 41 | } 42 | 43 | for _, tc := range cases { 44 | t.Run(tc.name, func(t *testing.T) { 45 | v := tc.tags.FindTag(tc.key) 46 | if !reflect.DeepEqual(v, tc.tag) { 47 | t.Errorf("incorrect find tag: %v != %v", v, tc.tag) 48 | } 49 | }) 50 | } 51 | } 52 | 53 | func TestTags_HasTag(t *testing.T) { 54 | cases := []struct { 55 | name string 56 | tags Tags 57 | key string 58 | has bool 59 | }{ 60 | { 61 | name: "has tag", 62 | tags: Tags{ 63 | {Key: "area", Value: "true"}, 64 | {Key: "building", Value: "yes"}, 65 | }, 66 | key: "building", 67 | has: true, 68 | }, 69 | { 70 | name: "not found", 71 | tags: Tags{ 72 | {Key: "building", Value: "yes"}, 73 | }, 74 | key: "not found", 75 | has: false, 76 | }, 77 | { 78 | name: "empty value", 79 | tags: Tags{ 80 | {Key: "present", Value: ""}, 81 | }, 82 | key: "present", 83 | has: true, 84 | }, 85 | } 86 | 87 | for _, tc := range cases { 88 | t.Run(tc.name, func(t *testing.T) { 89 | v := tc.tags.HasTag(tc.key) 90 | if v != tc.has { 91 | t.Errorf("incorrect has tag: %v != %v", v, tc.has) 92 | } 93 | }) 94 | } 95 | } 96 | 97 | func TestTags_AnyInteresting(t *testing.T) { 98 | cases := []struct { 99 | name string 100 | tags Tags 101 | interesting bool 102 | }{ 103 | { 104 | name: "has interesting", 105 | tags: Tags{ 106 | {Key: "building", Value: "yes"}, 107 | }, 108 | interesting: true, 109 | }, 110 | { 111 | name: "no tags", 112 | tags: Tags{}, 113 | interesting: false, 114 | }, 115 | { 116 | name: "no interesting tags", 117 | tags: Tags{ 118 | {Key: "source", Value: "whatever"}, 119 | {Key: "history", Value: "lots"}, 120 | }, 121 | interesting: false, 122 | }, 123 | } 124 | 125 | for _, tc := range cases { 126 | t.Run(tc.name, func(t *testing.T) { 127 | v := tc.tags.AnyInteresting() 128 | if v != tc.interesting { 129 | t.Errorf("incorrect interesting: %v != %v", v, tc.interesting) 130 | } 131 | }) 132 | } 133 | } 134 | 135 | func TestTags_MarshalJSON(t *testing.T) { 136 | data, err := Tags{}.MarshalJSON() 137 | if err != nil { 138 | t.Errorf("marshal error: %v", err) 139 | } 140 | 141 | if !bytes.Equal(data, []byte(`{}`)) { 142 | t.Errorf("incorrect data, got: %v", string(data)) 143 | } 144 | 145 | t2 := Tags{ 146 | Tag{Key: "highway 🏤 ", Value: "crossing"}, 147 | Tag{Key: "source", Value: "Bind 🏤 "}, 148 | } 149 | 150 | data, err = t2.MarshalJSON() 151 | if err != nil { 152 | t.Errorf("marshal error: %v", err) 153 | } 154 | if !bytes.Equal(data, []byte(`{"highway 🏤 ":"crossing","source":"Bind 🏤 "}`)) { 155 | t.Errorf("incorrect data, got: %v", string(data)) 156 | } 157 | } 158 | 159 | func TestTags_UnmarshalJSON(t *testing.T) { 160 | tags := Tags{} 161 | data := []byte(`{"highway 🏤 ":"crossing","source":"Bind 🏤 "}`) 162 | 163 | err := tags.UnmarshalJSON(data) 164 | if err != nil { 165 | t.Errorf("unmarshal error: %v", err) 166 | } 167 | 168 | tags.SortByKeyValue() 169 | t2 := Tags{ 170 | Tag{Key: "highway 🏤 ", Value: "crossing"}, 171 | Tag{Key: "source", Value: "Bind 🏤 "}, 172 | } 173 | 174 | if !reflect.DeepEqual(tags, t2) { 175 | t.Errorf("incorrect tags: %v", tags) 176 | } 177 | } 178 | 179 | func TestTags_SortByKeyValue(t *testing.T) { 180 | tags := Tags{ 181 | Tag{Key: "highway", Value: "crossing"}, 182 | Tag{Key: "source", Value: "Bind"}, 183 | } 184 | 185 | tags.SortByKeyValue() 186 | if v := tags[0].Key; v != "highway" { 187 | t.Errorf("incorrect sort got %v", v) 188 | } 189 | 190 | if v := tags[1].Key; v != "source" { 191 | t.Errorf("incorrect sort got %v", v) 192 | } 193 | 194 | tags = Tags{ 195 | Tag{Key: "source", Value: "Bind"}, 196 | Tag{Key: "highway", Value: "crossing"}, 197 | } 198 | 199 | tags.SortByKeyValue() 200 | if v := tags[0].Key; v != "highway" { 201 | t.Errorf("incorrect sort got %v", v) 202 | } 203 | 204 | if v := tags[1].Key; v != "source" { 205 | t.Errorf("incorrect sort got %v", v) 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /testdata/andorra-latest.osm.bz2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulmach/osm/db9d8ebc69cde0241f4989a74ea94ca9e7066e12/testdata/andorra-latest.osm.bz2 -------------------------------------------------------------------------------- /testdata/compare_scanners.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "compress/bzip2" 5 | "context" 6 | "log" 7 | "math" 8 | "os" 9 | "reflect" 10 | 11 | "github.com/paulmach/osm" 12 | "github.com/paulmach/osm/osmpbf" 13 | "github.com/paulmach/osm/osmxml" 14 | ) 15 | 16 | func main() { 17 | // these can be downloaded at http://download.geofabrik.de/north-america.html 18 | pbffile := "delaware-latest.osm.pbf" 19 | bz2file := "delaware-latest.osm.bz2" 20 | 21 | o1 := readPbf(pbffile) 22 | o2 := readBz2(bz2file) 23 | 24 | log.Printf("Are they the same? %v", reflect.DeepEqual(o1, o2)) 25 | 26 | log.Printf("nodes: %v %v", len(o1.Nodes), len(o2.Nodes)) 27 | if len(o1.Nodes) == len(o2.Nodes) { 28 | for i := range o1.Nodes { 29 | if !reflect.DeepEqual(o1.Nodes[i], o2.Nodes[i]) { 30 | log.Printf("unequal nodes") 31 | log.Printf("%v", o1.Nodes[i]) 32 | log.Printf("%v", o2.Nodes[i]) 33 | } 34 | } 35 | } 36 | 37 | log.Printf("ways: %v %v", len(o1.Ways), len(o2.Ways)) 38 | if len(o1.Ways) == len(o2.Ways) { 39 | for i := range o1.Ways { 40 | if !reflect.DeepEqual(o1.Ways[i], o2.Ways[i]) { 41 | log.Printf("unequal ways") 42 | log.Printf("%v", o1.Ways[i]) 43 | log.Printf("%v", o2.Ways[i]) 44 | } 45 | } 46 | } 47 | 48 | log.Printf("relations: %v %v", len(o1.Relations), len(o2.Relations)) 49 | if len(o1.Relations) == len(o2.Relations) { 50 | for i := range o1.Relations { 51 | if !reflect.DeepEqual(o1.Relations[i], o2.Relations[i]) { 52 | log.Printf("unequal relations") 53 | log.Printf("%v", o1.Relations[i]) 54 | log.Printf("%v", o2.Relations[i]) 55 | } 56 | } 57 | } 58 | } 59 | 60 | func readPbf(filename string) *osm.OSM { 61 | log.Printf("Reading pbf file %v", filename) 62 | f, err := os.Open(filename) 63 | if err != nil { 64 | panic(err) 65 | } 66 | defer f.Close() 67 | 68 | scanner := osmpbf.New(context.Background(), f, 1) 69 | return scanner2osm(scanner) 70 | } 71 | 72 | func readBz2(filename string) *osm.OSM { 73 | log.Printf("Reading bz2 file %v", filename) 74 | f, err := os.Open(filename) 75 | if err != nil { 76 | panic(err) 77 | } 78 | defer f.Close() 79 | 80 | r := bzip2.NewReader(f) 81 | scanner := osmxml.New(context.Background(), r) 82 | return scanner2osm(scanner) 83 | } 84 | 85 | func scanner2osm(scanner osm.Scanner) *osm.OSM { 86 | o := &osm.OSM{} 87 | for scanner.Scan() { 88 | e := scanner.Element() 89 | 90 | if e.Node != nil { 91 | e.Node.Lat = math.Floor(e.Node.Lat*1e7+0.5) / 1e7 92 | e.Node.Lon = math.Floor(e.Node.Lon*1e7+0.5) / 1e7 93 | e.Node.Visible = true 94 | e.Node.Tags.SortByKeyValue() 95 | o.Nodes = append(o.Nodes, e.Node) 96 | } 97 | 98 | if e.Way != nil { 99 | e.Way.Visible = true 100 | e.Way.Tags.SortByKeyValue() 101 | o.Ways = append(o.Ways, e.Way) 102 | } 103 | 104 | if e.Relation != nil { 105 | e.Relation.Visible = true 106 | e.Relation.Tags.SortByKeyValue() 107 | o.Relations = append(o.Relations, e.Relation) 108 | } 109 | } 110 | 111 | if err := scanner.Err(); err != nil { 112 | panic(err) 113 | } 114 | 115 | o.Nodes.SortByIDVersion() 116 | o.Ways.SortByIDVersion() 117 | o.Relations.SortByIDVersion() 118 | 119 | return o 120 | } 121 | -------------------------------------------------------------------------------- /testdata/delaware-latest.osm.pbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulmach/osm/db9d8ebc69cde0241f4989a74ea94ca9e7066e12/testdata/delaware-latest.osm.pbf -------------------------------------------------------------------------------- /testdata/relation-updates.osm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /testdata/way-updates.osm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /update.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "time" 7 | ) 8 | 9 | // CommitInfoStart is the start time when we know committed at information. 10 | // Any update.Timestamp >= this date is a committed at time. Anything before 11 | // this date is the element timestamp. 12 | var CommitInfoStart = time.Date(2012, 9, 12, 9, 30, 3, 0, time.UTC) 13 | 14 | // An Update is a change to children of a way or relation. 15 | // The child type, id, ref and/or role are the same as the child 16 | // at the given index. Lon/Lat are only updated for nodes. 17 | type Update struct { 18 | Index int `xml:"index,attr" json:"index"` 19 | Version int `xml:"version,attr" json:"version"` 20 | 21 | // Timestamp is the committed at time if time > CommitInfoStart or the 22 | // element timestamp if before that date. 23 | Timestamp time.Time `xml:"timestamp,attr" json:"timestamp"` 24 | 25 | ChangesetID ChangesetID `xml:"changeset,attr,omitempty" json:"changeset,omitempty"` 26 | Lat float64 `xml:"lat,attr,omitempty" json:"lat,omitempty"` 27 | Lon float64 `xml:"lon,attr,omitempty" json:"lon,omitempty"` 28 | Reverse bool `xml:"reverse,attr,omitempty" json:"reverse,omitempty"` 29 | } 30 | 31 | // Updates are collections of updates. 32 | type Updates []Update 33 | 34 | // UpTo will return the subset of updates taking place upto and on 35 | // the given time. 36 | func (us Updates) UpTo(t time.Time) Updates { 37 | var result Updates 38 | 39 | for _, u := range us { 40 | if u.Timestamp.After(t) { 41 | continue 42 | } 43 | 44 | result = append(result, u) 45 | } 46 | 47 | return result 48 | } 49 | 50 | // UpdateIndexOutOfRangeError is return when applying an update to an object 51 | // and the update index is out of range. 52 | type UpdateIndexOutOfRangeError struct { 53 | Index int 54 | } 55 | 56 | var _ error = &UpdateIndexOutOfRangeError{} 57 | 58 | // Error returns a string representation of the error. 59 | func (e *UpdateIndexOutOfRangeError) Error() string { 60 | return fmt.Sprintf("osm: index %d is out of range", e.Index) 61 | } 62 | 63 | type updatesSortTS Updates 64 | 65 | // SortByTimestamp will sort the updates by timestamp in ascending order. 66 | func (us Updates) SortByTimestamp() { sort.Sort(updatesSortTS(us)) } 67 | func (us updatesSortTS) Len() int { return len(us) } 68 | func (us updatesSortTS) Swap(i, j int) { us[i], us[j] = us[j], us[i] } 69 | func (us updatesSortTS) Less(i, j int) bool { 70 | return us[i].Timestamp.Before(us[j].Timestamp) 71 | } 72 | 73 | type updatesSortIndex Updates 74 | 75 | // SortByIndex will sort the updates by index in ascending order. 76 | func (us Updates) SortByIndex() { sort.Sort(updatesSortIndex(us)) } 77 | func (us updatesSortIndex) Len() int { return len(us) } 78 | func (us updatesSortIndex) Swap(i, j int) { us[i], us[j] = us[j], us[i] } 79 | func (us updatesSortIndex) Less(i, j int) bool { 80 | if us[i].Index != us[j].Index { 81 | return us[i].Index < us[j].Index 82 | } 83 | 84 | return us[i].Timestamp.Before(us[j].Timestamp) 85 | } 86 | -------------------------------------------------------------------------------- /update_test.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestUpdates_UpTo(t *testing.T) { 9 | us := Updates{ 10 | {Index: 1, Timestamp: time.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC)}, 11 | {Index: 2, Timestamp: time.Date(2014, 1, 1, 0, 0, 0, 0, time.UTC)}, 12 | {Index: 3, Timestamp: time.Date(2013, 1, 1, 0, 0, 0, 0, time.UTC)}, 13 | } 14 | 15 | if v := len(us.UpTo(time.Date(2011, 1, 1, 0, 0, 0, 0, time.UTC))); v != 0 { 16 | t.Errorf("incorrect number of updates, got %v", v) 17 | } 18 | 19 | u := us.UpTo(time.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC)) 20 | if v := len(u); v != 1 { 21 | t.Errorf("incorrect number of updates, got %v", v) 22 | } 23 | 24 | if v := u[0].Index; v != 1 { 25 | t.Errorf("incorrect value, got index: %v", v) 26 | } 27 | 28 | u = us.UpTo(time.Date(2013, 1, 1, 0, 0, 0, 0, time.UTC)) 29 | if v := len(u); v != 2 { 30 | t.Errorf("incorrect number of updates, got %v", v) 31 | } 32 | 33 | if v := u[0].Index; v != 1 { 34 | t.Errorf("incorrect value, got index: %v", v) 35 | } 36 | 37 | if v := u[1].Index; v != 3 { 38 | t.Errorf("incorrect value, got index: %v", v) 39 | } 40 | 41 | if v := len(us.UpTo(time.Date(2013, 2, 1, 0, 0, 0, 0, time.UTC))); v != 2 { 42 | t.Errorf("incorrect number of updates, got %v", v) 43 | } 44 | 45 | if v := len(us.UpTo(time.Date(2015, 1, 1, 0, 0, 0, 0, time.UTC))); v != 3 { 46 | t.Errorf("incorrect number of updates, got %v", v) 47 | } 48 | } 49 | 50 | func TestUpdates_SortByIndex(t *testing.T) { 51 | us := Updates{ 52 | {Index: 1, Version: 0, Timestamp: time.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC)}, 53 | {Index: 1, Version: 2, Timestamp: time.Date(2014, 1, 1, 0, 0, 0, 0, time.UTC)}, 54 | {Index: 1, Version: 1, Timestamp: time.Date(2013, 1, 1, 0, 0, 0, 0, time.UTC)}, 55 | } 56 | 57 | us.SortByIndex() 58 | 59 | for i, u := range us { 60 | if u.Version != i { 61 | t.Fatalf("incorrect sort: %v", us) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /user.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // UserID is the primary key for a user. 8 | // This is unique as the display name may change. 9 | type UserID int64 10 | 11 | // ObjectID is a helper returning the object id for this user id. 12 | func (id UserID) ObjectID() ObjectID { 13 | return ObjectID(userMask | (id << versionBits)) 14 | } 15 | 16 | // Users is a collection of users with some helpers attached. 17 | type Users []*User 18 | 19 | // A User is a registered OSM user. 20 | type User struct { 21 | XMLName xmlNameJSONTypeUser `xml:"user" json:"type"` 22 | ID UserID `xml:"id,attr" json:"id"` 23 | Name string `xml:"display_name,attr" json:"name"` 24 | Description string `xml:"description" json:"description,omitempty"` 25 | Img struct { 26 | Href string `xml:"href,attr" json:"href"` 27 | } `xml:"img" json:"img"` 28 | Changesets struct { 29 | Count int `xml:"count,attr" json:"count"` 30 | } `xml:"changesets" json:"changesets"` 31 | Traces struct { 32 | Count int `xml:"count,attr" json:"count"` 33 | } `xml:"traces" json:"traces"` 34 | Home struct { 35 | Lat float64 `xml:"lat,attr" json:"lat"` 36 | Lon float64 `xml:"lon,attr" json:"lon"` 37 | Zoom int `xml:"zoom,attr" json:"zoom"` 38 | } `xml:"home" json:"home"` 39 | Languages []string `xml:"languages>lang" json:"languages"` 40 | Blocks struct { 41 | Received struct { 42 | Count int `xml:"count,attr" json:"count"` 43 | Active int `xml:"active,attr" json:"active"` 44 | } `xml:"received" json:"received"` 45 | } `xml:"blocks" json:"blocks"` 46 | Messages struct { 47 | Received struct { 48 | Count int `xml:"count,attr" json:"count"` 49 | Unread int `xml:"unread,attr" json:"unread"` 50 | } `xml:"received" json:"received"` 51 | Sent struct { 52 | Count int `xml:"count,attr" json:"count"` 53 | } `xml:"sent" json:"sent"` 54 | } `xml:"messages" json:"messages"` 55 | CreatedAt time.Time `xml:"account_created,attr" json:"created_at"` 56 | } 57 | 58 | // ObjectID returns the object id of the user. 59 | func (u *User) ObjectID() ObjectID { 60 | return u.ID.ObjectID() 61 | } 62 | -------------------------------------------------------------------------------- /user_test.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "encoding/xml" 7 | "reflect" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestUser_UnmarshalXML(t *testing.T) { 13 | rawXML := []byte(` 14 | 15 | mapper 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | en-UK 25 | en 26 | 27 | 28 | 29 | 30 | 31 | `) 32 | u := &User{} 33 | 34 | err := xml.Unmarshal(rawXML, &u) 35 | if err != nil { 36 | t.Fatalf("unmarshal error: %v", err) 37 | } 38 | 39 | if v := u.ID; v != 91499 { 40 | t.Errorf("incorrect value: %v", v) 41 | } 42 | 43 | if v := u.Name; v != "pm" { 44 | t.Errorf("incorrect value: %v", v) 45 | } 46 | 47 | if v := u.Description; v != "mapper" { 48 | t.Errorf("incorrect value: %v", v) 49 | } 50 | 51 | if v := u.Img.Href; v != "image url" { 52 | t.Errorf("incorrect value: %v", v) 53 | } 54 | 55 | if v := u.Changesets.Count; v != 2638 { 56 | t.Errorf("incorrect value: %v", v) 57 | } 58 | 59 | if v := u.Traces.Count; v != 1 { 60 | t.Errorf("incorrect value: %v", v) 61 | } 62 | 63 | // home 64 | if v := u.Home.Lat; v != 37.793 { 65 | t.Errorf("incorrect value: %v", v) 66 | } 67 | 68 | if v := u.Home.Lon; v != -122.2712 { 69 | t.Errorf("incorrect value: %v", v) 70 | } 71 | 72 | if v := u.Home.Zoom; v != 3 { 73 | t.Errorf("incorrect value: %v", v) 74 | } 75 | 76 | if v := u.Languages; !reflect.DeepEqual(v, []string{"en-UK", "en"}) { 77 | t.Errorf("incorrect value: %v", v) 78 | } 79 | 80 | // blocks 81 | if v := u.Blocks.Received.Count; v != 5 { 82 | t.Errorf("incorrect value: %v", v) 83 | } 84 | 85 | if v := u.Blocks.Received.Active; v != 6 { 86 | t.Errorf("incorrect value: %v", v) 87 | } 88 | 89 | // messages 90 | if v := u.Messages.Received.Count; v != 15 { 91 | t.Errorf("incorrect value: %v", v) 92 | } 93 | 94 | if v := u.Messages.Received.Unread; v != 3 { 95 | t.Errorf("incorrect value: %v", v) 96 | } 97 | 98 | if v := u.Messages.Sent.Count; v != 7 { 99 | t.Errorf("incorrect value: %v", v) 100 | } 101 | 102 | // created 103 | if v := u.CreatedAt; !v.Equal(time.Date(2009, 1, 13, 19, 49, 59, 0, time.UTC)) { 104 | t.Errorf("incorrect value: %v", v) 105 | } 106 | 107 | // should marshal correctly. 108 | data, err := xml.Marshal(u) 109 | if err != nil { 110 | t.Fatalf("xml marshal error: %v", err) 111 | } 112 | 113 | nu := &User{} 114 | err = xml.Unmarshal(data, &nu) 115 | if err != nil { 116 | t.Fatalf("xml unmarshal error: %v", err) 117 | } 118 | 119 | if !reflect.DeepEqual(nu, u) { 120 | t.Errorf("incorrect marshal") 121 | t.Log(nu) 122 | t.Log(u) 123 | } 124 | } 125 | 126 | func TestUser_ObjectID(t *testing.T) { 127 | u := User{ID: 123} 128 | id := u.ObjectID() 129 | 130 | if v := id.Type(); v != TypeUser { 131 | t.Errorf("incorrect type: %v", v) 132 | } 133 | 134 | if v := id.Ref(); v != 123 { 135 | t.Errorf("incorrect ref: %v", 123) 136 | } 137 | } 138 | 139 | func TestUser_MarshalJSON(t *testing.T) { 140 | u := User{ 141 | ID: 123, 142 | Name: "user", 143 | } 144 | 145 | data, err := json.Marshal(u) 146 | if err != nil { 147 | t.Fatalf("marshal error: %v", err) 148 | } 149 | 150 | if !bytes.Equal(data, []byte(`{"type":"user","id":123,"name":"user","img":{"href":""},"changesets":{"count":0},"traces":{"count":0},"home":{"lat":0,"lon":0,"zoom":0},"languages":null,"blocks":{"received":{"count":0,"active":0}},"messages":{"received":{"count":0,"unread":0},"sent":{"count":0}},"created_at":"0001-01-01T00:00:00Z"}`)) { 151 | t.Errorf("incorrect json: %v", string(data)) 152 | } 153 | } 154 | --------------------------------------------------------------------------------