mirror of
https://github.com/Luzifer/gpxhydrant.git
synced 2024-12-22 12:11:17 +00:00
First runnable version
This commit is contained in:
commit
ace7819472
4 changed files with 555 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
Makefile
|
43
gpx/gpx.go
Normal file
43
gpx/gpx.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package gpx
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
type GPX struct {
|
||||
XMLName xml.Name `xml:"gpx"`
|
||||
Metadata struct {
|
||||
Link struct {
|
||||
Href string `xml:"href,attr"`
|
||||
Text string `xml:"text"`
|
||||
} `xml:"link"`
|
||||
Time time.Time `xml:"time"`
|
||||
Bounds struct {
|
||||
MaxLat float64 `xml:"maxlat,attr"`
|
||||
MaxLon float64 `xml:"maxlon,attr"`
|
||||
MinLat float64 `xml:"minlat,attr"`
|
||||
MinLon float64 `xml:"minlon,attr"`
|
||||
} `xml:"bounds"`
|
||||
} `xml:"metadata"`
|
||||
Waypoints []Waypoint `xml:"wpt"`
|
||||
}
|
||||
|
||||
type Waypoint struct {
|
||||
XMLName xml.Name `xml:"wpt"`
|
||||
Latitude float64 `xml:"lat,attr"`
|
||||
Longitude float64 `xml:"lon,attr"`
|
||||
Elevation float64 `xml:"ele"`
|
||||
Time time.Time `xml:"time"`
|
||||
Name string `xml:"name"`
|
||||
Comment string `xml:"cmt"`
|
||||
Description string `xml:"desc"`
|
||||
Symbol string `xml:"sym"`
|
||||
Type string `xml:"type"`
|
||||
}
|
||||
|
||||
func ParseGPXData(in io.Reader) (*GPX, error) {
|
||||
out := &GPX{}
|
||||
return out, xml.NewDecoder(in).Decode(out)
|
||||
}
|
304
main.go
Normal file
304
main.go
Normal file
|
@ -0,0 +1,304 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/Luzifer/go_helpers/position"
|
||||
"github.com/Luzifer/gpxhydrant/gpx"
|
||||
"github.com/Luzifer/gpxhydrant/osm"
|
||||
"github.com/Luzifer/rconfig"
|
||||
)
|
||||
|
||||
var (
|
||||
cfg = struct {
|
||||
GPXFile string `flag:"gpx-file,f" description:"File containing GPX waypoints"`
|
||||
NoOp bool `flag:"noop,n" default:"true" description:"Fetch data from OSM but do not write"`
|
||||
VersionAndExit bool `flag:"version" default:"false" description:"Print version and exit"`
|
||||
Pressure int64 `flag:"pressure" default:"4" description:"Pressure of the water grid"`
|
||||
Debug bool `flag:"debug,d" default:"false" description:"Enable debug logging"`
|
||||
OSM struct {
|
||||
Username string `flag:"osm-user" description:"Username to log into OSM"`
|
||||
Password string `flag:"osm-pass" description:"Password for osm-user"`
|
||||
UseDev bool `flag:"osm-dev" default:"false" description:"Switch to dev API"`
|
||||
}
|
||||
MachRange int64 `flag:"match-range" default:"20" description:"Range of meters to match GPX hydrants to OSM nodes"`
|
||||
Comment string `flag:"comment,c" description:"Comment for the changeset"`
|
||||
}{}
|
||||
version = "dev"
|
||||
|
||||
wrongGPXComment = errors.New("GPX comment does not match expected format")
|
||||
)
|
||||
|
||||
type hydrant struct {
|
||||
/*
|
||||
<node lat="53.58963" lon="9.70838">
|
||||
<tag k="emergency" v="fire_hydrant" />
|
||||
<tag k="fire_hydrant:diameter" v="100" />
|
||||
<tag k="fire_hydrant:position" v="sidewalk" />
|
||||
<tag k="fire_hydrant:pressure" v="4" />
|
||||
<tag k="fire_hydrant:type" v="underground" />
|
||||
<tag k="operator" v="Stadtwerke Wedel" />
|
||||
</node>
|
||||
*/
|
||||
ID int64
|
||||
Name string
|
||||
Latitude float64
|
||||
Longitude float64
|
||||
Diameter int64
|
||||
Position string
|
||||
Pressure int64
|
||||
Type string
|
||||
Version int64
|
||||
}
|
||||
|
||||
func parseWaypoint(in gpx.Waypoint) (*hydrant, error) {
|
||||
infoRegex := regexp.MustCompile(`([SPLG])([UOWP])(\?|[0-9]{2,3})`)
|
||||
if !infoRegex.MatchString(in.Comment) {
|
||||
return nil, wrongGPXComment
|
||||
}
|
||||
|
||||
matches := infoRegex.FindStringSubmatch(in.Comment)
|
||||
out := &hydrant{
|
||||
Name: in.Name,
|
||||
Latitude: roundPrec(in.Latitude, 7),
|
||||
Longitude: roundPrec(in.Longitude, 7),
|
||||
Pressure: cfg.Pressure,
|
||||
}
|
||||
|
||||
switch matches[1] {
|
||||
case "S":
|
||||
out.Position = "sidewalk"
|
||||
case "P":
|
||||
out.Position = "parking_lot"
|
||||
case "L":
|
||||
out.Position = "lane"
|
||||
case "G":
|
||||
out.Position = "green"
|
||||
}
|
||||
|
||||
switch matches[2] {
|
||||
case "U":
|
||||
out.Type = "underground"
|
||||
case "O":
|
||||
out.Type = "pillar"
|
||||
case "W":
|
||||
out.Type = "wall"
|
||||
case "P":
|
||||
out.Type = "pond"
|
||||
}
|
||||
|
||||
if matches[3] != "?" {
|
||||
diameter, err := strconv.ParseInt(matches[3], 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out.Diameter = diameter
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func fromNode(in *osm.Node) (*hydrant, error) {
|
||||
var e error
|
||||
|
||||
out := &hydrant{
|
||||
ID: in.ID,
|
||||
Version: in.Version,
|
||||
Latitude: in.Latitude,
|
||||
Longitude: in.Longitude,
|
||||
}
|
||||
|
||||
validFireHydrant := false
|
||||
|
||||
for _, t := range in.Tags {
|
||||
switch t.Key {
|
||||
case "emergency":
|
||||
if t.Value == "fire_hydrant" {
|
||||
validFireHydrant = true
|
||||
}
|
||||
case "fire_hydrant:diameter":
|
||||
if out.Diameter, e = strconv.ParseInt(t.Value, 10, 64); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
case "fire_hydrant:position":
|
||||
out.Position = t.Value
|
||||
case "fire_hydrant:pressure":
|
||||
if out.Pressure, e = strconv.ParseInt(t.Value, 10, 64); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
case "fire_hydrant:type":
|
||||
out.Type = t.Value
|
||||
}
|
||||
}
|
||||
|
||||
if !validFireHydrant {
|
||||
return nil, fmt.Errorf("Did not find required 'emergency=fire_hydrant' tag.")
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (h hydrant) ToNode() *osm.Node {
|
||||
out := &osm.Node{
|
||||
ID: h.ID,
|
||||
Version: h.Version,
|
||||
Latitude: h.Latitude,
|
||||
Longitude: h.Longitude,
|
||||
}
|
||||
|
||||
out.Tags = append(out.Tags, osm.Tag{Key: "emergency", Value: "fire_hydrant"})
|
||||
if h.Diameter > 0 {
|
||||
out.Tags = append(out.Tags, osm.Tag{Key: "fire_hydrant:diameter", Value: strconv.FormatInt(h.Diameter, 10)})
|
||||
}
|
||||
out.Tags = append(out.Tags, osm.Tag{Key: "fire_hydrant:position", Value: h.Position})
|
||||
out.Tags = append(out.Tags, osm.Tag{Key: "fire_hydrant:pressure", Value: strconv.FormatInt(h.Pressure, 10)})
|
||||
out.Tags = append(out.Tags, osm.Tag{Key: "fire_hydrant:type", Value: h.Type})
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func init() {
|
||||
rconfig.Parse(&cfg)
|
||||
|
||||
if cfg.VersionAndExit {
|
||||
fmt.Printf("gpxhydrant %s\n", version)
|
||||
}
|
||||
|
||||
if cfg.GPXFile == "" {
|
||||
log.Fatalf("gpx-file is a required parameter")
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
gpsFile, err := os.Open(cfg.GPXFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to open your GPX file: %s", err)
|
||||
}
|
||||
defer gpsFile.Close()
|
||||
|
||||
gpxData, err := gpx.ParseGPXData(gpsFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to parse your GPX file: %s", err)
|
||||
}
|
||||
|
||||
hydrants := []*hydrant{}
|
||||
var (
|
||||
minLat = 9999.0
|
||||
minLon = 9999.0
|
||||
maxLat, maxLon float64
|
||||
)
|
||||
for _, wp := range gpxData.Waypoints {
|
||||
h, e := parseWaypoint(wp)
|
||||
if e != nil {
|
||||
if cfg.Debug || e != wrongGPXComment {
|
||||
log.Printf("Found waypoint not suitable for converting: %s (Reason: %s)", wp.Name, e)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if cfg.Debug {
|
||||
log.Printf("Found a hydrant from waypoint %s: %#v", wp.Name, h)
|
||||
}
|
||||
hydrants = append(hydrants, h)
|
||||
|
||||
switch {
|
||||
case minLat > h.Latitude:
|
||||
minLat = h.Latitude
|
||||
case maxLat < h.Latitude:
|
||||
maxLat = h.Latitude
|
||||
case minLon > h.Longitude:
|
||||
minLon = h.Longitude
|
||||
case maxLon < h.Longitude:
|
||||
maxLon = h.Longitude
|
||||
}
|
||||
}
|
||||
|
||||
osmClient, err := osm.New(cfg.OSM.Username, cfg.OSM.Password, cfg.OSM.UseDev)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to log into OSM: %s", err)
|
||||
}
|
||||
|
||||
changeSets, err := osmClient.GetMyChangesets(true)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to get changesets: %s", err)
|
||||
}
|
||||
|
||||
var cs *osm.Changeset
|
||||
if len(changeSets) > 0 {
|
||||
cs = changeSets[0]
|
||||
} else {
|
||||
cs, err = osmClient.CreateChangeset(cfg.Comment)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to create changeset: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
mapData, err := osmClient.RetrieveMapObjects(minLon, minLat, maxLon, maxLat)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to get map data: %s", err)
|
||||
}
|
||||
|
||||
if cfg.Debug {
|
||||
log.Printf("Retrieved %d nodes from map", len(mapData.Nodes))
|
||||
}
|
||||
|
||||
availableHydrants := []*hydrant{}
|
||||
for _, n := range mapData.Nodes {
|
||||
h, e := fromNode(n)
|
||||
if e != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
availableHydrants = append(availableHydrants, h)
|
||||
}
|
||||
|
||||
for _, h := range hydrants {
|
||||
var found *hydrant
|
||||
for _, a := range availableHydrants {
|
||||
dist := position.Haversine(h.Longitude, h.Latitude, a.Longitude, a.Latitude)
|
||||
if dist <= float64(cfg.MachRange)/1000.0 {
|
||||
found = a
|
||||
}
|
||||
}
|
||||
|
||||
if found == nil {
|
||||
// No matched hydrant: Lets create one
|
||||
if cfg.NoOp {
|
||||
log.Printf("[NOOP] Would send a create to OSM (Changeset %d): %#v", cs.ID, h.ToNode())
|
||||
} else {
|
||||
osmClient.SaveNode(h.ToNode(), cs)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if h.Diameter == 0 && found.Diameter > 0 {
|
||||
h.Diameter = found.Diameter
|
||||
}
|
||||
|
||||
if h.Diameter == found.Diameter && h.Position == found.Position && h.Pressure == found.Pressure && h.Type == found.Type {
|
||||
if cfg.Debug {
|
||||
log.Printf("Found a good looking hydrant which needs no update: %#v", h)
|
||||
}
|
||||
// Everything matches, we don't care
|
||||
continue
|
||||
}
|
||||
|
||||
h.ID = found.ID
|
||||
h.Version = found.Version
|
||||
if cfg.NoOp {
|
||||
log.Printf("[NOOP] Would send a change to OSM (Changeset %d): To=%#v From=%#v", cs.ID, h.ToNode(), found.ToNode())
|
||||
} else {
|
||||
osmClient.SaveNode(h.ToNode(), cs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func roundPrec(in float64, nd int) float64 {
|
||||
// Quite ugly but working way to reduce number of digits after decimal point
|
||||
o, _ := strconv.ParseFloat(strconv.FormatFloat(in, 'f', nd, 64), 64)
|
||||
return o
|
||||
}
|
207
osm/osm.go
Normal file
207
osm/osm.go
Normal file
|
@ -0,0 +1,207 @@
|
|||
package osm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
liveAPIBaseURL = "http://api.openstreetmap.org/api/0.6"
|
||||
devAPIBaseURL = "http://api06.dev.openstreetmap.org/api/0.6"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
username string
|
||||
password string
|
||||
|
||||
APIBaseURL string
|
||||
HTTPClient *http.Client
|
||||
CurrentUser *User
|
||||
}
|
||||
|
||||
func New(username, password string, useDevServer bool) (*Client, error) {
|
||||
out := &Client{
|
||||
username: username,
|
||||
password: password,
|
||||
|
||||
APIBaseURL: liveAPIBaseURL,
|
||||
HTTPClient: http.DefaultClient,
|
||||
}
|
||||
|
||||
if useDevServer {
|
||||
out.APIBaseURL = devAPIBaseURL
|
||||
}
|
||||
|
||||
u := &Wrap{User: &User{}}
|
||||
if err := out.doParse("GET", "/user/details", nil, u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out.CurrentUser = u.User
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *Client) doPlain(method, path string, body io.Reader) (string, error) {
|
||||
responseBody, err := c.do(method, path, body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer responseBody.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(responseBody)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func (c *Client) do(method, path string, body io.Reader) (io.ReadCloser, error) {
|
||||
req, _ := http.NewRequest(method, c.APIBaseURL+path, body)
|
||||
req.SetBasicAuth(c.username, c.password)
|
||||
|
||||
res, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
d, e := ioutil.ReadAll(res.Body)
|
||||
if e != nil {
|
||||
return nil, fmt.Errorf("OSM API responded with status code %d and reading response failed.", res.StatusCode)
|
||||
}
|
||||
|
||||
res.Body.Close()
|
||||
return nil, fmt.Errorf("OSM API responded with status code %d (%s)", res.StatusCode, d)
|
||||
}
|
||||
|
||||
return res.Body, nil
|
||||
}
|
||||
|
||||
func (c *Client) doParse(method, path string, body io.Reader, output interface{}) error {
|
||||
responseBody, err := c.do(method, path, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer responseBody.Close()
|
||||
|
||||
if output != nil {
|
||||
return xml.NewDecoder(responseBody).Decode(output)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Wrap struct {
|
||||
XMLName xml.Name `xml:"osm"`
|
||||
User *User `xml:"user,omitempty"`
|
||||
Changesets []*Changeset `xml:"changeset,omitempty"`
|
||||
Nodes []*Node `xml:"node,omitempty"`
|
||||
}
|
||||
|
||||
type Changeset struct {
|
||||
XMLName xml.Name `xml:"changeset"`
|
||||
ID int64 `xml:"id,attr,omitempty"`
|
||||
User string `xml:"user,attr,omitempty"`
|
||||
UID int64 `xml:"uid,attr,omitempty"`
|
||||
CreatedAt time.Time `xml:"created_at,attr,omitempty"`
|
||||
ClosedAt time.Time `xml:"closed_at,attr,omitempty"`
|
||||
Open bool `xml:"open,attr,omitempty"`
|
||||
MinLat float64 `xml:"min_lat,attr,omitempty"`
|
||||
MinLon float64 `xml:"min_lon,attr,omitempty"`
|
||||
MaxLat float64 `xml:"max_lat,attr,omitempty"`
|
||||
MaxLon float64 `xml:"max_lon,attr,omitempty"`
|
||||
CommentCount int64 `xml:"comments_count,attr,omitempty"`
|
||||
|
||||
Tags []Tag `xml:"tag"`
|
||||
}
|
||||
|
||||
func (c *Client) GetMyChangesets(onlyOpen bool) ([]*Changeset, error) {
|
||||
urlPath := fmt.Sprintf("/changesets?user=%d&open=%s", c.CurrentUser.ID, strconv.FormatBool(onlyOpen))
|
||||
|
||||
r := &Wrap{}
|
||||
return r.Changesets, c.doParse("GET", urlPath, nil, r)
|
||||
}
|
||||
|
||||
func (c *Client) CreateChangeset(comment string) (*Changeset, error) {
|
||||
body := bytes.NewBuffer([]byte{})
|
||||
if err := xml.NewEncoder(body).Encode(Wrap{Changesets: []*Changeset{{}}}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := c.doPlain("PUT", "/changeset/create", body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cs := &Wrap{}
|
||||
if err := c.doParse("GET", fmt.Sprintf("/changeset/%s", res), nil, cs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(cs.Changesets) != 1 {
|
||||
return nil, fmt.Errorf("Unable to retrieve new changeset #%s", res)
|
||||
}
|
||||
|
||||
return cs.Changesets[0], nil
|
||||
}
|
||||
|
||||
func (c *Client) RetrieveMapObjects(minLat, minLon, maxLat, maxLon float64) (*Wrap, error) {
|
||||
urlPath := fmt.Sprintf("/map?bbox=%.7f,%.7f,%.7f,%.7f", minLat, minLon, maxLat, maxLon)
|
||||
res := &Wrap{}
|
||||
return res, c.doParse("GET", urlPath, nil, res)
|
||||
}
|
||||
|
||||
type User struct {
|
||||
XMLName xml.Name `xml:"user"`
|
||||
ID int64 `xml:"id,attr"`
|
||||
DisplayName string `xml:"display_name,attr"`
|
||||
AccountCreated time.Time `xml:"account_created,attr"`
|
||||
|
||||
Description string `xml:"description"`
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
XMLName xml.Name `xml:"node"`
|
||||
ID int64 `xml:"id,attr,omitempty"`
|
||||
Version int64 `xml:"version,attr,omitempty"`
|
||||
Changeset int64 `xml:"changeset,attr,omitempty"`
|
||||
User string `xml:"user,attr,omitempty"`
|
||||
UID int64 `xml:"uid,attr,omitempty"`
|
||||
Latitude float64 `xml:"lat,attr"`
|
||||
Longitude float64 `xml:"lon,attr"`
|
||||
|
||||
Tags []Tag `xml:"tag"`
|
||||
}
|
||||
|
||||
func (c *Client) SaveNode(n *Node, cs *Changeset) error {
|
||||
if n.ID > 0 && n.Version == 0 {
|
||||
return fmt.Errorf("When an ID is set the version must be present")
|
||||
}
|
||||
|
||||
urlPath := "/node/create"
|
||||
|
||||
if n.ID > 0 {
|
||||
urlPath = fmt.Sprintf("/node/%d", n.ID)
|
||||
}
|
||||
|
||||
body := bytes.NewBuffer([]byte{})
|
||||
if err := xml.NewEncoder(body).Encode(n); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := c.doPlain("PUT", urlPath, body)
|
||||
return err
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
XMLName xml.Name `xml:"tag"`
|
||||
Key string `xml:"k,attr"`
|
||||
Value string `xml:"v,attr"`
|
||||
}
|
Loading…
Reference in a new issue