2016-05-06 23:32:32 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"os"
|
|
|
|
"strconv"
|
|
|
|
|
|
|
|
"github.com/Luzifer/go_helpers/position"
|
|
|
|
"github.com/Luzifer/gpxhydrant/gpx"
|
|
|
|
"github.com/Luzifer/gpxhydrant/osm"
|
|
|
|
"github.com/Luzifer/rconfig"
|
2017-06-05 15:23:52 +00:00
|
|
|
log "github.com/Sirupsen/logrus"
|
2016-05-06 23:32:32 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
cfg = struct {
|
2016-05-07 12:54:44 +00:00
|
|
|
Comment string `flag:"comment,c" default:"Added hydrants from GPX file" description:"Comment for the changeset"`
|
2017-06-05 15:23:52 +00:00
|
|
|
Debug bool `flag:"debug,d" default:"false" description:"Enable debug logging (Deprecated: Use --log-level=debug)"`
|
2016-05-07 12:54:44 +00:00
|
|
|
GPXFile string `flag:"gpx-file,f" description:"File containing GPX waypoints"`
|
2017-06-05 15:23:52 +00:00
|
|
|
LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error)"`
|
2016-05-07 14:33:06 +00:00
|
|
|
MachRange int64 `flag:"match-range" default:"5" description:"Range of meters to match GPX hydrants to OSM nodes"`
|
2016-05-07 14:32:05 +00:00
|
|
|
NoOp bool `flag:"noop,n" default:"false" description:"Fetch data from OSM but do not write"`
|
2016-05-07 12:54:44 +00:00
|
|
|
OSM struct {
|
2018-06-17 10:46:02 +00:00
|
|
|
APIURL string `flag:"osm-apiurl" default:"https://api.openstreetmap.org/api/0.6" description:"API base url to contact"`
|
2016-05-06 23:32:32 +00:00
|
|
|
Username string `flag:"osm-user" description:"Username to log into OSM"`
|
|
|
|
Password string `flag:"osm-pass" description:"Password for osm-user"`
|
2018-06-17 10:46:02 +00:00
|
|
|
UseDev bool `flag:"osm-dev" default:"false" description:"Switch to dev API (Deprecated: Use --osm-apiurl)"`
|
2016-05-06 23:32:32 +00:00
|
|
|
}
|
2016-05-07 12:54:44 +00:00
|
|
|
Pressure int64 `flag:"pressure" default:"4" description:"Pressure of the water grid"`
|
|
|
|
VersionAndExit bool `flag:"version" default:"false" description:"Print version and exit"`
|
2016-05-06 23:32:32 +00:00
|
|
|
}{}
|
|
|
|
version = "dev"
|
|
|
|
|
2016-05-07 14:29:42 +00:00
|
|
|
changeset *osm.Changeset
|
|
|
|
|
2016-05-07 12:51:51 +00:00
|
|
|
errWrongGPXComment = errors.New("GPX comment does not match expected format")
|
2016-05-06 23:32:32 +00:00
|
|
|
)
|
|
|
|
|
2016-05-07 14:22:26 +00:00
|
|
|
type bounds struct{ MinLat, MinLon, MaxLat, MaxLon float64 }
|
|
|
|
|
|
|
|
func (b *bounds) Update(lat, lon float64) {
|
|
|
|
if b.MinLat > lat {
|
|
|
|
b.MinLat = lat
|
|
|
|
}
|
|
|
|
if b.MaxLat < lat {
|
|
|
|
b.MaxLat = lat
|
|
|
|
}
|
|
|
|
if b.MinLon > lon {
|
|
|
|
b.MinLon = lon
|
|
|
|
}
|
|
|
|
if b.MaxLon < lon {
|
|
|
|
b.MaxLon = lon
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-05-06 23:32:32 +00:00
|
|
|
func init() {
|
|
|
|
rconfig.Parse(&cfg)
|
|
|
|
|
|
|
|
if cfg.VersionAndExit {
|
|
|
|
fmt.Printf("gpxhydrant %s\n", version)
|
2016-05-07 12:55:13 +00:00
|
|
|
os.Exit(0)
|
2016-05-06 23:32:32 +00:00
|
|
|
}
|
|
|
|
|
2017-06-05 15:23:52 +00:00
|
|
|
if l, err := log.ParseLevel(cfg.LogLevel); err == nil {
|
|
|
|
log.SetLevel(l)
|
|
|
|
} else {
|
|
|
|
log.Fatalf("Unable to parse log level: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Support deprecated parameter to overwrite log level
|
|
|
|
if cfg.Debug {
|
|
|
|
log.SetLevel(log.DebugLevel)
|
|
|
|
}
|
|
|
|
|
2016-05-06 23:32:32 +00:00
|
|
|
if cfg.GPXFile == "" {
|
|
|
|
log.Fatalf("gpx-file is a required parameter")
|
|
|
|
}
|
2016-05-07 12:56:37 +00:00
|
|
|
|
|
|
|
if cfg.OSM.Password == "" || cfg.OSM.Username == "" {
|
|
|
|
log.Fatalf("osm-pass / osm-user are required parameters")
|
|
|
|
}
|
2018-06-17 10:46:02 +00:00
|
|
|
|
|
|
|
if cfg.OSM.UseDev {
|
|
|
|
// Migration for deprecated flag
|
|
|
|
cfg.OSM.APIURL = "https://api06.dev.openstreetmap.org/api/0.6"
|
|
|
|
}
|
2016-05-06 23:32:32 +00:00
|
|
|
}
|
|
|
|
|
2016-05-07 14:22:26 +00:00
|
|
|
func hydrantsFromGPXFile() ([]*hydrant, bounds) {
|
|
|
|
// Read and parse GPX file
|
2016-05-06 23:32:32 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2016-05-07 14:22:26 +00:00
|
|
|
bds := bounds{MinLat: 9999, MinLon: 9999}
|
2016-05-06 23:32:32 +00:00
|
|
|
hydrants := []*hydrant{}
|
2016-05-07 14:22:26 +00:00
|
|
|
|
2016-05-06 23:32:32 +00:00
|
|
|
for _, wp := range gpxData.Waypoints {
|
|
|
|
h, e := parseWaypoint(wp)
|
|
|
|
if e != nil {
|
2017-06-05 15:23:52 +00:00
|
|
|
if e != errWrongGPXComment {
|
|
|
|
log.Debugf("Found waypoint not suitable for converting: %s (Reason: %s)", wp.Name, e)
|
2016-05-06 23:32:32 +00:00
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
2017-06-05 15:23:52 +00:00
|
|
|
log.Debugf("Found a hydrant from waypoint %s: %#v", wp.Name, h)
|
2016-05-06 23:32:32 +00:00
|
|
|
hydrants = append(hydrants, h)
|
|
|
|
|
2016-05-07 14:22:26 +00:00
|
|
|
bds.Update(h.Latitude, h.Longitude)
|
2016-05-06 23:32:32 +00:00
|
|
|
}
|
|
|
|
|
2016-05-07 14:22:26 +00:00
|
|
|
return hydrants, bds
|
|
|
|
}
|
2016-05-06 23:32:32 +00:00
|
|
|
|
2016-05-07 14:22:26 +00:00
|
|
|
func createChangeset(osmClient *osm.Client) *osm.Changeset {
|
2016-05-07 14:29:42 +00:00
|
|
|
if changeset != nil {
|
|
|
|
return changeset
|
|
|
|
}
|
|
|
|
|
2016-05-07 14:22:26 +00:00
|
|
|
cs, err := osmClient.CreateChangeset()
|
2016-05-06 23:32:32 +00:00
|
|
|
if err != nil {
|
2016-05-07 14:22:26 +00:00
|
|
|
log.Fatalf("Unable to create changeset: %s", err)
|
2016-05-06 23:32:32 +00:00
|
|
|
}
|
|
|
|
|
2017-06-05 15:23:52 +00:00
|
|
|
log.Debugf("Working on Changeset %d", cs.ID)
|
2016-05-07 12:15:38 +00:00
|
|
|
|
2016-05-07 12:49:53 +00:00
|
|
|
cs.Tags = []osm.Tag{
|
2018-06-12 06:04:10 +00:00
|
|
|
{Key: "comment", Value: cfg.Comment},
|
2016-05-07 12:49:53 +00:00
|
|
|
{Key: "created_by", Value: fmt.Sprintf("gpxhydrant %s", version)},
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := osmClient.SaveChangeset(cs); err != nil {
|
|
|
|
log.Fatalf("Unable to save changeset: %s", err)
|
|
|
|
}
|
|
|
|
|
2016-05-07 14:29:42 +00:00
|
|
|
changeset = cs
|
|
|
|
|
2016-05-07 14:22:26 +00:00
|
|
|
return cs
|
|
|
|
}
|
|
|
|
|
|
|
|
func getHydrantsFromOSM(osmClient *osm.Client, bds bounds) []*hydrant {
|
2016-05-07 12:16:01 +00:00
|
|
|
border := 0.0009 // Equals ~100m using haversine formula
|
2016-05-07 18:22:10 +00:00
|
|
|
mapData, err := osmClient.RetrieveMapObjects(bds.MinLon-border, bds.MinLat-border, bds.MaxLon+border, bds.MaxLat+border)
|
2016-05-06 23:32:32 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("Unable to get map data: %s", err)
|
|
|
|
}
|
|
|
|
|
2017-06-05 15:23:52 +00:00
|
|
|
log.Debugf("Retrieved %d nodes from map", len(mapData.Nodes))
|
2016-05-06 23:32:32 +00:00
|
|
|
|
|
|
|
availableHydrants := []*hydrant{}
|
|
|
|
for _, n := range mapData.Nodes {
|
|
|
|
h, e := fromNode(n)
|
|
|
|
if e != nil {
|
2016-05-07 14:22:26 +00:00
|
|
|
continue // Not a hydrant, ignore that node
|
2016-05-06 23:32:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
availableHydrants = append(availableHydrants, h)
|
|
|
|
}
|
|
|
|
|
2016-05-07 14:22:26 +00:00
|
|
|
return availableHydrants
|
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
// Convert waypoints from GPX file to hydrants
|
|
|
|
hydrants, bds := hydrantsFromGPXFile()
|
|
|
|
|
2018-06-17 10:46:02 +00:00
|
|
|
osmClient, err := osm.NewWithAPIEndpoint(cfg.OSM.Username, cfg.OSM.Password, cfg.OSM.APIURL)
|
2016-05-07 14:22:26 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("Unable to log into OSM: %s", err)
|
|
|
|
}
|
|
|
|
|
2017-06-05 15:23:52 +00:00
|
|
|
osmClient.DebugHTTPRequests = log.GetLevel() == log.DebugLevel
|
|
|
|
|
2016-05-07 14:22:26 +00:00
|
|
|
// Retrieve currently available information from OSM
|
|
|
|
availableHydrants := getHydrantsFromOSM(osmClient, bds)
|
|
|
|
|
2016-05-07 14:29:42 +00:00
|
|
|
updateOrCreateHydrants(hydrants, availableHydrants, osmClient)
|
2016-05-07 14:22:26 +00:00
|
|
|
}
|
|
|
|
|
2016-05-07 14:29:42 +00:00
|
|
|
func updateOrCreateHydrants(hydrants, availableHydrants []*hydrant, osmClient *osm.Client) {
|
2016-05-06 23:32:32 +00:00
|
|
|
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
|
2016-05-07 14:22:26 +00:00
|
|
|
doNoOp(
|
2016-05-07 14:29:42 +00:00
|
|
|
fmt.Sprintf("[NOOP] Would send a create to OSM (Changeset %d): %#v", createChangeset(osmClient).ID, h.ToNode()),
|
2016-05-07 14:22:26 +00:00
|
|
|
func() {
|
2016-05-07 14:29:42 +00:00
|
|
|
if err := osmClient.SaveNode(h.ToNode(), createChangeset(osmClient)); err != nil {
|
2016-05-07 14:22:26 +00:00
|
|
|
log.Fatalf("Unable to create node using the OSM API: %s", err)
|
|
|
|
}
|
2017-06-05 15:23:52 +00:00
|
|
|
log.Debugf("Created a hydrant: %s", h.Name)
|
2016-05-07 14:22:26 +00:00
|
|
|
},
|
|
|
|
)
|
2016-05-06 23:32:32 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2016-05-07 14:22:26 +00:00
|
|
|
// Special case: If the diameter of the recorded hydrant is unknown but previously known keep the previous version
|
2016-05-06 23:32:32 +00:00
|
|
|
if h.Diameter == 0 && found.Diameter > 0 {
|
|
|
|
h.Diameter = found.Diameter
|
|
|
|
}
|
|
|
|
|
2016-05-07 14:22:26 +00:00
|
|
|
if !found.NeedsUpdate(h) {
|
2017-06-05 15:23:52 +00:00
|
|
|
log.Debugf("Found a good looking hydrant which needs no update: %#v", h)
|
2016-05-06 23:32:32 +00:00
|
|
|
// Everything matches, we don't care
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
h.ID = found.ID
|
|
|
|
h.Version = found.Version
|
2016-05-07 14:22:26 +00:00
|
|
|
doNoOp(
|
2016-05-07 14:29:42 +00:00
|
|
|
fmt.Sprintf("[NOOP] Would send a change to OSM (Changeset %d): To=%#v From=%#v", createChangeset(osmClient).ID, h.ToNode(), found.ToNode()),
|
2016-05-07 14:22:26 +00:00
|
|
|
func() {
|
2016-05-07 14:29:42 +00:00
|
|
|
if err := osmClient.SaveNode(h.ToNode(), createChangeset(osmClient)); err != nil {
|
2016-05-07 14:22:26 +00:00
|
|
|
log.Fatalf("Unable to create node using the OSM API: %s", err)
|
|
|
|
}
|
2017-06-05 15:23:52 +00:00
|
|
|
log.Debugf("Changed a hydrant: %s", h.Name)
|
2016-05-07 14:22:26 +00:00
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func doNoOp(message string, execution func()) {
|
|
|
|
if cfg.NoOp {
|
|
|
|
log.Println(message)
|
|
|
|
return
|
2016-05-06 23:32:32 +00:00
|
|
|
}
|
2016-05-07 14:22:26 +00:00
|
|
|
|
|
|
|
execution()
|
2016-05-06 23:32:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|