├── LICENSE ├── README.md ├── go.mod ├── go.sum └── main.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Richard Ulmer 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A small tool for querying OSM data from PBF files. 2 | 3 | # Installation 4 | You can download the tool from the releases page: 5 | https://github.com/codesoap/osmar/releases 6 | 7 | If you have the Go toolchain installed and prefer to build osmar 8 | yourself, you can get it by running this: 9 | 10 | ```bash 11 | go install github.com/codesoap/osmar/v3@latest 12 | # The binary is now at ~/go/bin/osmar. 13 | ``` 14 | 15 | # Basic Usage 16 | Before you can use osmar, you must set the environment variable 17 | `OSMAR_PBF_FILE`; it must contain the path to a PBF 18 | file. These files can be downloaded, for example, from 19 | [download.geofabrik.de](https://download.geofabrik.de/). An example would be: 20 | ```console 21 | $ cd /tmp 22 | $ wget 'https://download.geofabrik.de/europe/germany/bremen-latest.osm.pbf' 23 | $ export OSMAR_PBF_FILE='/tmp/bremen-latest.osm.pbf' 24 | ``` 25 | 26 | ```console 27 | $ # Find all entries within 50m of the center of Bremen, Germany: 28 | $ osmar 53.076 8.807 50 29 | meta:distance: 5m 30 | meta:id: 163740903 31 | meta:type: way 32 | meta:link: https://www.openstreetmap.org/way/163740903 33 | addr:city: Bremen 34 | addr:country: DE 35 | addr:housenumber: 1 36 | addr:postcode: 28195 37 | addr:street: Am Markt 38 | building: retail 39 | ... 40 | 41 | $ # Filter by tags to find a bicycle shop near the center of Bremen: 42 | $ osmar 53.076 8.807 500 shop=bicycle 43 | meta:distance: 243m 44 | meta:id: 834082330 45 | meta:type: node 46 | meta:link: https://www.openstreetmap.org/node/834082330 47 | addr:city: Bremen 48 | addr:country: DE 49 | addr:housenumber: 30-32 50 | addr:postcode: 28195 51 | addr:street: Martinistraße 52 | check_date:opening_hours: 2024-04-28 53 | email: velo-sport@nord-com.net 54 | fax: +49 421 18225 55 | name: Velo-Sport 56 | ... 57 | 58 | $ # Use UNIX tools to compact the output: 59 | $ osmar 53.076 8.807 200 shop=clothes | grep -e '^$' -e distance -e meta:link -e name 60 | meta:distance: 65m 61 | meta:link: https://www.openstreetmap.org/node/410450005 62 | name: Peek & Cloppenburg 63 | short_name: P&C 64 | 65 | meta:distance: 98m 66 | meta:link: https://www.openstreetmap.org/node/3560745513 67 | name: CALIDA 68 | 69 | meta:distance: 99m 70 | meta:link: https://www.openstreetmap.org/node/718963532 71 | name: zero 72 | ... 73 | ``` 74 | 75 | # More Examples 76 | You can find the documentation on all available tags at 77 | [wiki.openstreetmap.org/wiki/Map_Features](https://wiki.openstreetmap.org/wiki/Map_Features). 78 | Here are a few more examples: 79 | 80 | ```bash 81 | # Find a bakery: 82 | osmar 53.076 8.807 200 shop=bakery 83 | 84 | # Find nearby public transport stations: 85 | osmar 53.076 8.807 200 public_transport=stop_position 86 | 87 | # Find nearby hiking routes: 88 | osmar 53.076 8.807 500 route=hiking 89 | 90 | # Searching for multiple values of the same tag is also possible: 91 | osmar 53.076 8.807 3000 sport=climbing sport=swimming 92 | 93 | # Pro tip: Use "*" to search for any value: 94 | osmar 53.076 8.807 500 'sport=*' 95 | 96 | # Learn about the population of the city and its urban districts: 97 | osmar 53.076 8.807 10000 'population=*' 98 | ``` 99 | 100 | # Performance 101 | Because osmar is parsing compressed PBF files on the fly, performance is 102 | somewhat limited, but should be good enough for a few queries now and 103 | then. Try to use the smallest extract that is available for your area. 104 | 105 | The performance can be improved slightly by converting PBF files to zstd compression with 106 | the [zstd-pbf tool](https://github.com/codesoap/zstd-pbf). 107 | 108 | Here are some quick measurements; better results will probably be 109 | achieved with more modern hardware: 110 | 111 | | PBF file | Query | CPU | Runtime | RAM usage | 112 | | --- | --- | --- | --- | --- | 113 | | bremen-latest.osm.pbf (19.3MiB) | `osmar 53.076 8.807 50 'shop=*'` | i5-8250U (4x1.6GHz) | ~0.24s | ~65MiB | 114 | | bremen-latest.osm.pbf (19.3MiB) | `osmar 53.076 8.807 50 'shop=*'` | AMD Ryzen 5 3600 (6x4.2GHz) | ~0.10s | ~125MiB | 115 | | bremen-latest.zstd.osm.pbf (19.4MiB) | `osmar 53.076 8.807 50 'shop=*'` | i5-8250U (4x1.6GHz) | ~0.20s | ~70MiB | 116 | | bremen-latest.zstd.osm.pbf (19.4MiB) | `osmar 53.076 8.807 50 'shop=*'` | AMD Ryzen 5 3600 (6x4.2GHz) | ~0.09s | ~140MiB | 117 | | czech-republic-latest.osm.pbf (828MiB) | `osmar 49.743 13.379 200 'shop=*'` | i5-8250U (4x1.6GHz) | ~7s | ~320MiB | 118 | | czech-republic-latest.osm.pbf (828MiB) | `osmar 49.743 13.379 200 'shop=*'` | AMD Ryzen 5 3600 (6x4.2GHz) | ~2.9s | ~600MiB | 119 | | czech-republic-latest.zstd.osm.pbf (847MiB) | `osmar 49.743 13.379 200 'shop=*'` | i5-8250U (4x1.6GHz) | ~5s | ~350MiB | 120 | | czech-republic-latest.zstd.osm.pbf (847MiB) | `osmar 49.743 13.379 200 'shop=*'` | AMD Ryzen 5 3600 (6x4.2GHz) | ~2.5s | ~650MiB | 121 | 122 | PS: Previously osmar accessed a PostgreSQL database. This was much 123 | faster and had some other benefits, but the database was annoying to set 124 | up, so I abandoned this approach. You can find this version of osmar here: 125 | [github.com/codesoap/osmar/tree/v2](https://github.com/codesoap/osmar/tree/v2) 126 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/codesoap/osmar/v3 2 | 3 | go 1.22.1 4 | 5 | require github.com/codesoap/pbf v0.1.2 6 | 7 | require ( 8 | github.com/codesoap/lineworker v0.2.0 // indirect 9 | github.com/klauspost/compress v1.17.10 // indirect 10 | github.com/planetscale/vtprotobuf v0.6.0 // indirect 11 | google.golang.org/protobuf v1.34.2 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/codesoap/lineworker v0.2.0 h1:hy8dj55dwRCr6PspJM3ISV533P9TKWrvD3Mx66uSAbg= 2 | github.com/codesoap/lineworker v0.2.0/go.mod h1:aatQ4DVq3bEOUG0O5lAcBhSiwH2BmwmSYQPJjrYCv6w= 3 | github.com/codesoap/pbf v0.1.2 h1:DP2hgEtSDcYfVH5LpiqeAX2C17pIvb1tB0y9A84pjd8= 4 | github.com/codesoap/pbf v0.1.2/go.mod h1:7DnX49CF9HZpZVF/d6E43/tDxLoO+AMomF6zhg5fS/M= 5 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 6 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 7 | github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= 8 | github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 9 | github.com/planetscale/vtprotobuf v0.6.0 h1:nBeETjudeJ5ZgBHUz1fVHvbqUKnYOXNhsIEabROxmNA= 10 | github.com/planetscale/vtprotobuf v0.6.0/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= 11 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 12 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 13 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 14 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 15 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "os" 7 | "sort" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/codesoap/pbf" 12 | ) 13 | 14 | const usage = `Usage: osmar [=]... 15 | Info about tags: https://wiki.openstreetmap.org/wiki/Map_Features 16 | 17 | Environment: 18 | OSMAR_PBF_FILE The path to the PBF file. 19 | ` 20 | 21 | var pbfFile = "" 22 | 23 | type entity struct { 24 | e *pbf.Entity 25 | distance int // distance in meters 26 | } 27 | 28 | func init() { 29 | if pbfFile = os.Getenv("OSMAR_PBF_FILE"); pbfFile == "" { 30 | fmt.Fprintln(os.Stderr, "The OSMAR_PBF_FILE environment variable must be set.") 31 | os.Exit(1) 32 | } 33 | } 34 | 35 | func main() { 36 | if len(os.Args) < 4 { 37 | fmt.Fprintf(os.Stderr, usage) 38 | os.Exit(1) 39 | } 40 | lat, err := strconv.ParseFloat(os.Args[1], 64) 41 | dieOnErr("Could not parse lat: %s\n", err) 42 | lon, err := strconv.ParseFloat(os.Args[2], 64) 43 | dieOnErr("Could not parse lon: %s\n", err) 44 | radius, err := strconv.ParseFloat(os.Args[3], 64) 45 | dieOnErr("Could not parse radius: %s\n", err) 46 | tags, err := getTags() 47 | dieOnErr("Could not parse tags: %s\n", err) 48 | 49 | res, err := getResults(lat, lon, radius, tags) 50 | dieOnErr("Failed to query database: %s\n", err) 51 | sort.Slice(res, func(i, j int) bool { return res[i].distance < res[j].distance }) 52 | printResults(res) 53 | } 54 | 55 | func dieOnErr(msg string, err error) { 56 | if err != nil { 57 | fmt.Fprintf(os.Stderr, msg, err.Error()) 58 | os.Exit(1) 59 | } 60 | } 61 | 62 | func getTags() (map[string][]string, error) { 63 | tags := make(map[string][]string) 64 | for _, arg := range os.Args[4:] { 65 | split := strings.SplitN(arg, "=", 2) 66 | if len(split) != 2 { 67 | err := fmt.Errorf("tag without value: %s", arg) 68 | return nil, err 69 | } 70 | if split[1] == "*" { 71 | tags[split[0]] = []string{} 72 | } else { 73 | tags[split[0]] = append(tags[split[0]], split[1]) 74 | } 75 | } 76 | return tags, nil 77 | } 78 | 79 | func getResults(lat, lon, radius float64, tags map[string][]string) ([]entity, error) { 80 | // convert meters roughly to nanodegrees: 81 | radiusLatInt := int64(1_000_000_000 * radius / 111_000) 82 | radiusLonInt := int64(1_000_000_000 * radius / (6_367_000 * math.Cos(lat*math.Pi/180) * math.Pi / 180)) 83 | 84 | latInt := int64(1_000_000_000 * lat) // convert to nanodegrees 85 | lonInt := int64(1_000_000_000 * lon) // convert to nanodegrees 86 | maxLat, minLat := latInt+radiusLatInt, latInt-radiusLatInt 87 | maxLon, minLon := lonInt+radiusLonInt, lonInt-radiusLonInt 88 | locFilter := func(lat, lon int64) bool { 89 | // Just do a square here; expensive (sqrt) filtering is done again 90 | // later, when the entities have been "pre-filtered". 91 | return lat >= minLat && lat <= maxLat && 92 | lon >= minLon && lon <= maxLon 93 | } 94 | filter := pbf.Filter{ 95 | Location: locFilter, 96 | ExcludePartial: true, 97 | Tags: tags, 98 | } 99 | entities, err := pbf.ExtractEntities(pbfFile, filter) 100 | if err != nil { 101 | return nil, err 102 | } 103 | ret := make([]entity, 0, len(entities.Nodes)+len(entities.Ways)+len(entities.Relations)) 104 | radiusInt := int(radius) 105 | for _, e := range entities.Nodes { 106 | ent := pbf.Entity(e) 107 | dist := getDistance(latInt, lonInt, ent, entities) 108 | if dist <= radiusInt { 109 | // Filtering by radius again, as we just did a square filter 110 | // for performance earlier. 111 | ret = append(ret, entity{e: &ent, distance: dist}) 112 | } 113 | } 114 | for _, e := range entities.Ways { 115 | ent := pbf.Entity(e) 116 | dist := getDistance(latInt, lonInt, ent, entities) 117 | if dist <= radiusInt { 118 | // Filtering by radius again, as we just did a square filter 119 | // for performance earlier. 120 | ret = append(ret, entity{e: &ent, distance: dist}) 121 | } 122 | } 123 | for _, e := range entities.Relations { 124 | ent := pbf.Entity(e) 125 | dist := getDistance(latInt, lonInt, ent, entities) 126 | if dist <= radiusInt { 127 | // Filtering by radius again, as we just did a square filter 128 | // for performance earlier. 129 | ret = append(ret, entity{e: &ent, distance: dist}) 130 | } 131 | } 132 | return ret, nil 133 | } 134 | 135 | // getDistance determines the distance in meters from latA, lonA to the 136 | // closest point of e. 137 | // 138 | // TODO: Use ancillary entities to determine distance, once this 139 | // feature is available in github.com/codesoap/pbf. Right now, ways and 140 | // relations will often have an unknown distance, because their members 141 | // didn't match the filter. 142 | func getDistance(latA, lonA int64, e pbf.Entity, entities pbf.Entities) int { 143 | closest := -1 144 | switch t := e.(type) { 145 | case pbf.Node: 146 | latB, lonB := t.Coords() 147 | return calculateDistance(latA, lonA, latB, lonB) 148 | case pbf.Way: 149 | for _, nodeID := range t.Nodes() { 150 | if node, ok := entities.Nodes[nodeID]; ok { 151 | latB, lonB := node.Coords() 152 | dist := calculateDistance(latA, lonA, latB, lonB) 153 | if closest == -1 || dist < closest { 154 | closest = dist 155 | } 156 | } 157 | } 158 | case pbf.Relation: 159 | for _, nodeID := range t.Nodes() { 160 | if node, ok := entities.Nodes[nodeID]; ok { 161 | latB, lonB := node.Coords() 162 | dist := calculateDistance(latA, lonA, latB, lonB) 163 | if closest == -1 || dist < closest { 164 | closest = dist 165 | } 166 | } 167 | } 168 | for _, wayID := range t.Ways() { 169 | if way, ok := entities.Ways[wayID]; ok { 170 | e2 := pbf.Entity(way) 171 | dist := getDistance(latA, lonA, e2, entities) 172 | if closest == -1 || dist < closest { 173 | closest = dist 174 | } 175 | } 176 | } 177 | for _, relationID := range t.Relations() { 178 | if relation, ok := entities.Relations[relationID]; ok { 179 | e2 := pbf.Entity(relation) 180 | dist := getDistance(latA, lonA, e2, entities) 181 | if closest == -1 || dist < closest { 182 | closest = dist 183 | } 184 | } 185 | } 186 | } 187 | return closest 188 | } 189 | 190 | func calculateDistance(latA, lonA, latB, lonB int64) int { 191 | latAf := float64(latA) / 1_000_000_000 192 | latBf := float64(latB) / 1_000_000_000 193 | lonAf := float64(lonA) / 1_000_000_000 194 | lonBf := float64(lonB) / 1_000_000_000 195 | y := (latBf - latAf) * 111_000 196 | x := (lonBf - lonAf) * 6_367_000 * math.Cos(latAf*math.Pi/180) * math.Pi / 180 197 | return int(math.Sqrt(y*y + x*x)) 198 | } 199 | 200 | func printResults(entities []entity) { 201 | for i, entityWithDist := range entities { 202 | entity := *entityWithDist.e 203 | if i > 0 { 204 | fmt.Println() 205 | } 206 | eType := "unknown" 207 | switch entity.(type) { 208 | case pbf.Node: 209 | eType = "node" 210 | case pbf.Way: 211 | eType = "way" 212 | case pbf.Relation: 213 | eType = "relation" 214 | } 215 | if entityWithDist.distance >= 0 { 216 | fmt.Printf("meta:distance: %dm\n", entityWithDist.distance) 217 | } else { 218 | fmt.Println("meta:distance: unknown") 219 | } 220 | fmt.Println("meta:id:", entity.ID()) 221 | fmt.Println("meta:type:", eType) 222 | fmt.Printf("meta:link: https://www.openstreetmap.org/%s/%d\n", eType, entity.ID()) 223 | tags := entity.Tags() 224 | keys := make([]string, 0, len(tags)) 225 | for k := range tags { 226 | keys = append(keys, k) 227 | } 228 | sort.StringSlice(keys).Sort() 229 | for _, tag := range keys { 230 | fmt.Printf("%s: %s\n", tag, tags[tag]) 231 | } 232 | } 233 | } 234 | --------------------------------------------------------------------------------