OpenStreetMap (OSM) - запрашивает пользовательские данные для областей (полигонов) с помощью Golang и Protobuf.
В текущем проекте передо мной стояла задача получить конкретные данные для определенных областей (округов и городов), например зеленых зон (парки, леса и т.д.). С самими данными мы производим некоторые вычисления, но здесь не об этом. Более захватывающим является понимание того, как мы получаем эти данные. Для этого мы прошли несколько итераций и должны были найти лучший для нас способ. Я хотел бы показать вам, какой способ был для нас наиболее успешным, на примере Golang и некоторого Python. Мы разделили проект на две части.
- Часть 1. Получение метаданных (полигоны) - Python
- Часть 2: Запрос данных OpenStreetMap (OSM) - Golang
Мы получим метаданные с помощью небольшого скрипта Python. Захватывающая часть будет запрограммирована с помощью Golang. Репозиторий, включая окончательный код, находится здесь.
OpenStreetMap (OSM) - Query custom data for areas (polygons) with Golang and Protocol Buffers - GitHub - AICDEV/osm_poly_harvester: OpenStreetMap (OSM) - Query custom data for areas (polygons) with Golang and Protocol Buffers
Метаданные
В дальнейшем мы будем работать с городом Эссен (Германия, Рурская область). Не всегда должен быть Берлин! Первым делом нужно получить полигон Эссена. Для этого в игру входит Номинатим.
Номинатим
Nominatim - это инструмент для поиска данных OSM по имени и адресу (геокодирование) и для генерации синтетических адресов точек OSM (обратное геокодирование). Его можно найти на сайте nominatim.openstreetmap.org.
Пример для Эссена можно найти здесь: https://nominatim.openstreetmap.org/ui/details.html?osmtype=R&osmid=62713&class=boundary
Мы не будем работать с веб-интерфейсом, вместо этого мы будем использовать API от Nominatim. Вы можете найти полную документацию по API здесь: https://nominatim.org/release-docs/develop/api/Search/
Рекорд Эссена
Вот небольшой пример postman, который показывает, как структурирован ответ API и как выглядит полигон:
Вы можете попробовать, отправив запрос GET на адрес https://nominatim.openstreetmap.org/search?country=germany&county=essen&format=geojson&polygon_geojson=1.
Первое, на что следует обратить внимание при представлении полигонов, это то, что мы имеем дело со структурой GeoJSON. Теперь, когда мы знаем, как выглядит ответ API, мы можем определить буфер нашего протокола.
Protocol Buffer
Protocol Buffer - это не зависящий от языка и платформы расширяемый механизм Google для сериализации структурированных данных - как XML, но меньше, быстрее и проще. Вы определяете, как вы хотите, чтобы ваши данные были структурированы один раз, а затем вы можете использовать специальный сгенерированный исходный код, чтобы легко записывать и считывать структурированные данные в различные потоки данных и из них, используя множество языков.
Protocol buffers are a language-neutral, platform-neutral extensible mechanism for serializing structured data.
Если вы раньше не работали с Protocol Buffers, лучше всего заглянуть в демонстрационный репозиторий Google. В противном случае вы можете просто работать с JSON в своем коде. Вот руководство по языку: https://developers.google.com/protocol-buffers/docs/overview .
Прежде чем мы начнем, давайте посмотрим на структуру нашего проекта:
Внутри папки proto мы определяем буфер нашего протокола. Первый тестируемый черновик выглядит так:
syntax = "proto3";
option go_package = "github.com/aicdev/osm_poly_harvester/osm";
package osm;
// Nominatim
message Nominatim {
string type = 1;
NominatimProperties properties = 2;
NominatimBbox bbox = 3;
repeated NominatimGeometry geometry = 4;
}
message NominatimProperties {
int64 placeId = 1;
int32 osmId = 2;
string displayName = 3;
int32 placeRank = 4;
string category = 5;
string type = 6;
string osmType = 7;
}
message NominatimBbox {
repeated float entry = 1;
}
message NominatimGeometry {
string type = 1;
repeated NominatimCoordinates coordinates = 2;
}
message NominatimCoordinates {
float lat = 1;
float lon = 2;
}
Чтобы прояснить ситуацию, вот изображение, на котором показано сопоставление:
Теперь, когда у нас есть начальное определение, нам все еще нужно скомпилировать буфер протокола в Python. Для этого мы готовим небольшой сценарий оболочки и устанавливаем необходимые зависимости для python. Но это в следующем разделе.
Python и Protocol Buffer
Во-первых, давайте посмотрим, какие зависимости нам нужны. По сути, у вас должен быть установлен компилятор Protocol Buffer от Google. Учебник для этого можно найти здесь: https://grpc.io/docs/protoc-installation/
Я также создал виртуальную среду для Python. Просто чтобы разделить мои зависимости. Вы можете найти руководство здесь: https://docs.python.org/3/library/venv.html
Нам нужны следующие зависимости (запросы предназначены для дальнейшего использования) для компиляции в буфер протокола в python.
Вы можете просто установить эти зависимости, запустив:
#only when your work with venv
source ./env/bin/activate
./env/bin/pip install -r requirements.txt
#without env
pip install -r requirements.txt
Следующим шагом является определение сценария сборки (build_python_proto_buffer.sh).
python -m grpc_tools.protoc -I ../proto --python_out=. osm.proto --grpc_python_out=. -I ../proto -I .
Пришло время запустить скрипт и посмотреть результат. Рекомендую посмотреть в выводе. Это изображение является лишь кратким превью:
Теперь внутри папки nominatim у нас должны быть следующие файлы и структура:
В следующем разделе мы получим данные из nominatim, преобразуем их в буфер протокола и сохраним их на нашем диске.
Python и Nominatim
Теперь, когда мы закончили определение буфера протокола и скомпилировали его в python, мы можем запрашивать и преобразовывать данные из Nomimatim.
Для этого мы создаем файл «nominatim.py». Папка моего проекта внутри папки nominatim теперь выглядит так:
Наша задача - создать файл «nominatim.pbf» (буфер протокола). Вот мой скрипт на Python для этого.
import requests
import osm_pb2
def fetch_osm(city):
# to test multipolygon your can fetch Hamburg as an example. Just uncomment this request for that
# res = requests.get('https://nominatim.openstreetmap.org/search', params = {
# 'state': 'Hamburg',
# 'country': 'germany',
# 'format': 'geojson',
# 'polygon_geojson': 1
# })
res = requests.get('https://nominatim.openstreetmap.org/search', params = {
'county': city,
'country': 'germany',
'format': 'geojson',
'polygon_geojson': 1
})
return res.json()
def res_geometry_to_pbf(geometry):
# check if city has multipolygon like Hamburg
geom_arr = []
if geometry['type'] == 'Polygon':
n_geometry = osm_pb2.NominatimGeometry()
n_geometry.type = geometry['type']
for geom in geometry['coordinates']:
for sub_geom in geom:
coordinate = osm_pb2.NominatimCoordinates()
coordinate.lat = sub_geom[1]
coordinate.lon = sub_geom[0]
n_geometry.coordinates.extend([coordinate])
geom_arr.append(n_geometry)
return geom_arr
elif geometry['type'] == 'MultiPolygon':
n_geometry = osm_pb2.NominatimGeometry()
n_geometry.type = geometry['type']
for geom in geometry['coordinates']:
for sub_geom in geom:
for entry_geom in sub_geom:
coordinate = osm_pb2.NominatimCoordinates()
coordinate.lat = entry_geom[1]
coordinate.lon = entry_geom[0]
n_geometry.coordinates.extend([coordinate])
geom_arr.append(n_geometry)
return geom_arr
else:
return osm_pb2.NominatimGeometry()
def res_json_to_pbf(res_json):
n_entry = osm_pb2.Nominatim()
# get the first entry from nominatim response
feature = res_json['features'][0]
# map nominatim properties
n_entry.type = feature['type']
n_entry.properties.placeId = feature['properties']['place_id']
n_entry.properties.osmId = feature['properties']['osm_id']
n_entry.properties.displayName = feature['properties']['display_name']
n_entry.properties.placeRank = feature['properties']['place_rank']
n_entry.properties.category = feature['properties']['category']
n_entry.properties.type = feature['properties']['type']
n_entry.properties.osmType = feature['properties']['osm_type']
# map nominatim bounding box
n_entry.bbox.entry.extend(feature['bbox'])
# map nominatim geometry
n_entry.geometry.extend(res_geometry_to_pbf(feature['geometry']))
return n_entry
def pbf_to_disk(pbf):
with open ('./nominatim.pbf', 'wb+') as pbf_out:
pbf_out.write(pbf)
if __name__ == '__main__':
res_json = fetch_osm('essen')
pbf = res_json_to_pbf(res_json)
pbf_to_disk(pbf.SerializeToString(True))
У Google есть отличная документация о буферах python и протоколов здесь: https://developers.google.com/protocol-buffers/docs/pythontutorial. Пример вывода сгенерированного pbf выглядит следующим образом:
type: "Feature"
properties {
placeId: 258262283
osmId: 62713
displayName: "Essen, Nordrhein-Westfalen, Deutschland"
placeRank: 12
category: "boundary"
type: "administrative"
osmType: "relation"
}
bbox {
entry: 6.894344329833984
entry: 51.347572326660156
entry: 7.137650012969971
entry: 51.534202575683594
}
geometry {
type: "Polygon"
coordinates {
lat: 51.476253509521484
lon: 6.894344329833984
}
coordinates {
lat: 51.47611999511719
lon: 6.8943562507629395
},
....
}
Все идет нормально. Итак, мы справились с первой частью. Подведем итоги еще раз вкратце:
- У нас есть определение буфера протокола для ответов Nominatim API.
- У нас есть способ скомпилировать буфер протокола в Python
- Мы можем использовать скрипт Python для запроса данных из Nominatim API, преобразования их в буфер протокола, сериализации и сохранения на нашем диске.
Затем мы хотим прочитать буфер протокола в нашем приложении Golang и запросить артефакты (леса, луга и т.д.) из OpenStreetMap для этой области (полигона).
OpenStreetMap
С этого момента мы будем работать с OSM API. Вы можете найти хороший обзор здесь: https://wiki.openstreetmap.org/wiki/Overpass_API. Но учтите, что это проект с открытым исходным кодом. Поэтому, если вы хотите регулярно получать данные из OSM, вы можете просто развернуть свой собственный экземпляр и не вызывать ненужный трафик. Вы можете найти руководство здесь: https://wiki.openstreetmap.org/wiki/Overpass_API/Installation
Golang и Protocol Buffer
Внутри нашей папки osm мы запускаем:
go mod init
для инициализации нашего нового проекта go. Затем, как мы делали это раньше в python, нам нужно установить некоторые зависимости для компиляции буфера протокола. Убедитесь, что вы установили следующие зависимости (если у вас возникли проблемы с golang и буфером протокола, проверьте: https://grpc.io/docs/languages/go/quickstart/):
go get -u google.golang.org/protobuf/cmd/protoc-gen-go
go get -u google.golang.org/grpc/cmd/protoc-gen-go-grpc
go get -u google.golang.org/grpc
Следующим шагом будет создание нашего сценария сборки. Поэтому мы создаем файл «build_golang_proto.sh» и папку «proto» внутри папки osm и вставляем следующее содержимое:
protoc --go_out=./proto --go_opt=paths=source_relative --go-grpc_out=./proto --go-grpc_opt=paths=source_relative osm.proto -I ../proto -I .
Затем запустите скрипт и посмотрите результат в папке ./proto. Рекомендую посмотреть в выводе. Это изображение является лишь кратким превью:
Теперь, когда у нас есть буфер протокола, мы можем сосредоточиться на извлечении данных из OSM. Давайте сделаем это.
Golang и OpenStreetMap
Опять же, кратко резюмируя, какова наша цель:
- Буфер протокола, прочитанный с диска
- Преобразование полигона в запрос OSM
- Запрос и отображение артефактов в области
Первая часть очень проста и может быть реализована несколькими строками кода:
package main
import (
"fmt"
"io/ioutil"
"log"
"github.com/aicdev/osm_poly_harvester/osm/app"
"github.com/aicdev/osm_poly_harvester/osm/overpass"
pb "github.com/aicdev/osm_poly_harvester/osm/proto"
"google.golang.org/protobuf/proto"
)
func main() {
app.StartApplication()
/******************************************************************
* read the nominatim.pbf from ../nominatim/nominatim.pbf
* and parse the content to our pb struct
*******************************************************************/
in, err := ioutil.ReadFile("../nominatim/nominatim.pbf")
if err != nil {
log.Fatalln("Error reading file:", err)
}
nominatim := &pb.Nominatim{}
if err := proto.Unmarshal(in, nominatim); err != nil {
log.Fatalln("Failed to parse nominatim:", err)
}
log.Printf("successfully deserialized nominatim pbf for: %s", nominatim.GetProperties().GetDisplayName())
overpassService := overpass.NewOverpassService(nominatim.GetGeometry())
overpassService.Init()
overpassService.FetchOSMData()
for _, ovr := range overpassService.GetOverpassResponse() {
fmt.Println(ovr)
}
}
Как видите, мы просто импортируем сгенерированный nominatim.pbf из нашей папки nominatim (то, что мы сделали с помощью скрипта python), а также импортируем pb «github.com/aicdev/osm_poly_harvester/osm/proto», который является скомпилированным буфер протокола nominatim в голанге. Затем я назвал что-то «OSM Query». Но что это такое? Проще говоря, это язык запросов, который понимает интерпретатор Overpass. Я должен признать, что к этому языку запросов немного сложно привыкнуть, но с поиском в Google и попытками он всегда срабатывает. Есть онлайн-площадка: https://overpass-turbo.eu/. Теперь мы хотим запросить следующие артефакты из нашей области:
"way[\"leisure\"=\"park\"](poly: \"%s\");",
"way[\"leisure\"=\"forest\"](poly: \"%s\");",
"way[\"landuse\"=\"meadow\"](poly: \"%s\");",
"rel[\"leisure\"=\"park\"](poly: \"%s\");",
"rel[\"leisure\"=\"nature_reserve\"](poly: \"%s\");",
"rel[\"landuse\"=\"forest\"](poly: \"%s\");",
Полигон в конце каждой линии - это прежний многоугольник, который был запрошен от nominatim. Затем нам понадобится несколько строк перехода, чтобы создать шаблон динамического запроса и сохранить ответ. Код выглядит так (не беспокойтесь, весь проект находится на github; ссылка в конце этого поста):
package overpass
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"time"
pb "github.com/aicdev/osm_poly_harvester/osm/proto"
)
type overpassService struct {
QueryFragements []string
AreaPolyString string
Templates []string
OverpassResponse map[string]interface{}
OverpassResponseArr []map[string]interface{}
}
type OverpassServiceInterface interface {
Init()
FetchOSMData()
GetOverpassResponse() []map[string]interface{}
}
func NewOverpassService(area []*pb.NominatimGeometry) OverpassServiceInterface {
return &overpassService{
QueryFragements: []string{
"way[\"leisure\"=\"park\"](poly: \"%s\");",
"way[\"leisure\"=\"forest\"](poly: \"%s\");",
"way[\"landuse\"=\"meadow\"](poly: \"%s\");",
"rel[\"leisure\"=\"park\"](poly: \"%s\");",
"rel[\"leisure\"=\"nature_reserve\"](poly: \"%s\");",
"rel[\"landuse\"=\"forest\"](poly: \"%s\");",
},
AreaPolyString: parseAreaCoordinatesToOSMPoly(area),
}
}
func (os *overpassService) Init() {
os.Templates = make([]string, 0)
for _, v := range os.QueryFragements {
rawTpl := `
[out:json];
(
` + fmt.Sprintf(v, os.AreaPolyString) + `
);
out body geom;
>;
out skel geom;
`
os.Templates = append(os.Templates, rawTpl)
}
}
/*******************************************************************
* fetch data from api/interpreter
*******************************************************************/
func (os *overpassService) FetchOSMData() {
for _, v := range os.Templates {
data := url.Values{}
data.Set("data", v)
req, _ := http.NewRequest("POST", "https://overpass-api.de/api/interpreter", strings.NewReader(data.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
client := &http.Client{
Timeout: 180 * time.Second,
}
res, err := client.Do(req)
if err != nil {
log.Printf("error from osm: %s", err.Error())
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatal(err.Error())
} else {
ovr := os.OverpassResponse
json.Unmarshal(body, &ovr)
os.OverpassResponseArr = append(os.OverpassResponseArr, ovr)
}
}
}
func (os *overpassService) GetOverpassResponse() []map[string]interface{} {
return os.OverpassResponseArr
}
func parseAreaCoordinatesToOSMPoly(area []*pb.NominatimGeometry) string {
parsedCoordinates := ""
for _, c := range area {
for i, v := range c.GetCoordinates() {
if i == len(c.GetCoordinates())-1 {
parsedCoordinates += fmt.Sprintf("%f %f", v.GetLat(), v.GetLon())
} else {
parsedCoordinates += fmt.Sprintf("%f %f ", v.GetLat(), v.GetLon())
}
}
}
return parsedCoordinates
И вуаля вот наш результат:
Выглядит немного запутанно. Мы запускаем один из наших шаблонов запросов в Overpass Turbo, чтобы лучше понять, как выглядит ответ:
Резюме
Я надеюсь, что этот пост показал то или иное, как запрашивать очень конкретные области, точно определенные артефакты. Также я надеюсь, что люди, которые еще не связались с темой протокольных буферов, теперь захотят заняться этим более глубоко. Что в итоге происходит с данными запроса из OSM, всегда зависит от варианта использования. Например, в нашем проекте мы также сохранили ответ в буфере протокола и провели дальнейшие вычисления. Спасибо за чтение и получайте удовольствие от работы с Nominatim или Open Street Map.