1
0
Fork 0
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:
Knut Ahlers 2016-05-07 01:32:32 +02:00
commit ace7819472
Signed by: luzifer
GPG key ID: DC2729FDD34BE99E
4 changed files with 555 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
Makefile

43
gpx/gpx.go Normal file
View 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
View 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
View 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"`
}