package main import ( "bufio" "compress/bzip2" "compress/gzip" "fmt" "io" "math" "os" "path" "strconv" "time" //"github.com/pkg/profile" xmlparser "github.com/tamerh/xml-stream-parser" // Dá se získat pomocí "go get -u github.com/tamerh/xml-stream-parser" ) type node struct { id int lat float64 lon float64 } type area struct { borderWays map[int][]*node // all ways that are borders of this area borderNodes map[int]*node // links to all nodes contained in border ways lonMin float64 lonMax float64 latMin float64 latMax float64 } // Funkce volaná na oblasti pro spočítání její min/max lon/lat func (a *area) computeBoundary() { a.lonMin, a.lonMax = math.Inf(1), math.Inf(-1) a.latMin, a.latMax = math.Inf(1), math.Inf(-1) for _, way := range a.borderWays { for _, nd := range way { a.latMin = math.Min(a.latMin, nd.lat) a.latMax = math.Max(a.latMax, nd.lat) a.lonMin = math.Min(a.lonMin, nd.lon) a.lonMax = math.Max(a.lonMax, nd.lon) } } // fmt.Fprintf(os.Stderr, "Lon min: %f | Lon max: %f\n", a.lonMin, a.lonMax) // fmt.Fprintf(os.Stderr, "Lat min: %f | Lat max: %f\n", a.latMin, a.latMax) } // Funkce volaná na oblasti pro zjištění, jestli je zadaný bod uvnitř, nebo ne func (a *area) isInside(point node) bool { if point.lat < a.latMin || point.lat > a.latMax || point.lon < a.lonMin || point.lon > a.lonMax { // Je mimo obdélník, nemá smysl kontrolovat podrobněji return false } // Kontrolu uděláme pomocí klasického přístupu s polopřímkou vypálenou // náhodným směrem od bodu, budeme počítat počet průniků s hranami. // Pro zjednodušení nám stačí vzít horizontální polopřímku. horizontalLine := point.lat counter := 0 for _, way := range a.borderWays { for i := 1; i < len(way); i++ { // Pro eliminaci chyb při průchodu horizontální čáry přímo koncem hrany // potřebujeme, aby všechny hrany vedly vždy stejně. Nechť je to "zdola nahoru". a, b := way[i-1], way[i] if a.lat > b.lat { a, b = b, a } // Protínat se mohou jenom pokud je jeden bod hrany pod (včetně) a druhý nad linkou (nevčetně) if (a.lat <= horizontalLine) && (b.lat > horizontalLine) { // Spočítáme bod, kde se hrana protíná s horizontální přímkou // 1. Vezmeme si vektor (a-b) a spočítáme, ve které jeho části protne horizontalLine t := (a.lat - horizontalLine) / (a.lat - b.lat) // t by mělo vyjít mezi 0 a 1 // 2. Spočítáme longtitude v bodě protnutí crossLon := a.lon + t*(b.lon-a.lon) // 3. Pokud se protínají na polopřímce doprava od zkoumaného bodu, tak započítáme if crossLon >= point.lon { counter++ } } } } // Pokud je počet protnutí lichý -> je v oblasti, jinak ne return (counter%2 == 1) } // Funkce na kontrolu jestli má tag elementu danou hodnotu func hasTagValue(el *xmlparser.XMLElement, name string, value string) bool { for _, tag := range el.Childs["tag"] { if tag.Attrs["k"] == name { return tag.Attrs["v"] == value } } return false } // Pomocná funkce vykonávající pravidelně jinou funkci func doEvery(d time.Duration, f func()) { for range time.Tick(d) { f() } } func die(exitcode int, format string, a ...interface{}) { fmt.Fprintf(os.Stderr, format, a...) os.Exit(1) } func main() { if len(os.Args) != 3 { die(1, "Usage: %s \n", os.Args[0]) } areaID := os.Args[2] // 1. Otevřeme soubor file, err := os.Open(os.Args[1]) if err != nil { die(1, "Cannot open file: %v\n", err) } // Připravíme si funkci na resetování readeru, protože budeme muset XML soubor projít vícekrát var archiveReader io.Reader var parser *xmlparser.XMLParser resetReader := func(elements ...string) { if _, err := file.Seek(0, 0); err != nil { die(1, "Cannot seek to beginning of the file") } // Podle typu dat (podle přípony) si pořídíme buď gzip nebo bzip2 reader switch path.Ext(os.Args[1]) { case ".gz", "gzip": if archiveReader, err = gzip.NewReader(file); err != nil { die(1, "Cannot open the gzip reader: %v", err) } case ".bz2": archiveReader = bzip2.NewReader(file) default: die(1, "Unknown typ of the archive: %s\n", os.Args[1]) } // Dopustíme se triku - odzipování dat spustíme v samostatném vlákně // (u .gz souborů to má vliv na zrychlení tak 10%, protože gzip knihovna je velmi // rychlá a "brzdou" je parsování XML, u .bz2 je zrychlení o více než třetinu, // protože bzip knihovna v aktuálním Go ještě není tolik odladěná a brzdí ona) r, w := io.Pipe() go func() { // write everything into the pipe. Decompression happens in this goroutine. io.Copy(w, archiveReader) w.Close() }() parser = xmlparser.NewXMLParser(bufio.NewReaderSize(r, 1024*1024*256), elements...) } start := time.Now() phaseStart := start // Funkce na vypisování statistiky, spustíme ji tentokrát na pozadí printStat := func() { megabytes := float64(parser.TotalReadSize) / 1024 / 1024 elapsed := time.Now().Sub(phaseStart) fmt.Fprintf(os.Stderr, "Elapsed: %-20s Read: %10.2fMB (%.2fMB/s)\r", elapsed, megabytes, megabytes/elapsed.Seconds()) } go doEvery(500*time.Millisecond, printStat) //////////////////////////////////////////////////////////////////////// // PRVNÍ PRŮCHOD - získání relace zadávající oblast //////////////////// fmt.Fprintf(os.Stderr, "[Phase 1] Getting relation with given ID to determine which ways are needed\n") area := &area{ borderWays: map[int][]*node{}, borderNodes: map[int]*node{}, } // 1. Pořídíme si reader od začátku souboru a budeme chtít teď jen tagy relací resetReader("relation") parser.SkipElements([]string{"node", "way"}).SkipOuterElements() // Najdeme relaci se zadaným ID a načteme její cesty (vynecháme ty, které mají roli "inner") found := false for xml := range parser.Stream() { switch xml.Name { case "relation": if xml.Attrs["id"] == areaID { for _, member := range xml.Childs["member"] { if member.Attrs["type"] == "way" && member.Attrs["role"] != "inner" { ID, _ := strconv.Atoi(member.Attrs["ref"]) area.borderWays[ID] = []*node{} } } found = true break // Není potřeba tento průchod dále protahovat } } } if !found { die(2, "\nArea with ID '%s' not found\n", areaID) } printStat() fmt.Fprintf(os.Stderr, "\nArea with %d ways found\n", len(area.borderWays)) //////////////////////////////////////////////////////////////////////// // DRUHÝ PRŮCHOD /////////////////////////////////////////////////////// fmt.Fprintf(os.Stderr, "[Phase 2] Getting node IDs of all ways of the area\n") // 1. Resetujeme se na začátek souboru a připravíme se na další průchod resetReader("way") parser.SkipElements([]string{"node", "relation"}).SkipOuterElements() // 2. Poznamenáme si ID všech nodů, které chceme (mimo uložení do cest se // nám hodí i mapa všech nodů, ve které se dá rychle hledat) phaseStart = time.Now() for xml := range parser.Stream() { switch xml.Name { case "way": ID, _ := strconv.Atoi(xml.Attrs["id"]) if _, found := area.borderWays[ID]; found { nodes := []*node{} for _, nd := range xml.Childs["nd"] { nodeID, _ := strconv.Atoi(nd.Attrs["ref"]) // Každý node si pamatujeme jenom jednou a z cesty na něj odkazujeme // (to nám poté pomůže při plnění vlastností nodů) if _, found := area.borderNodes[nodeID]; !found { area.borderNodes[nodeID] = &node{id: nodeID} } nodes = append(nodes, area.borderNodes[nodeID]) } area.borderWays[ID] = nodes } } } printStat() fmt.Fprintf(os.Stderr, "\nFound %d nodes around the area\n", len(area.borderNodes)) //////////////////////////////////////////////////////////////////////// // TŘETÍ PRŮCHOD /////////////////////////////////////////////////////// fmt.Fprintf(os.Stderr, "[Phase 3] Getting all nodes around the area\n") // 1. Resetujeme se na začátek souboru a připravíme se na další průchod resetReader("node") parser.SkipElements([]string{"way", "relation"}).SkipOuterElements() // 2. Projdeme všechny nodes a zaznamenáme si ty, které chceme foundCounter := 0 phaseStart = time.Now() for xml := range parser.Stream() { switch xml.Name { case "node": id, _ := strconv.Atoi(xml.Attrs["id"]) if nd, found := area.borderNodes[id]; found { nd.lat, _ = strconv.ParseFloat(xml.Attrs["lat"], 64) nd.lon, _ = strconv.ParseFloat(xml.Attrs["lon"], 64) foundCounter++ if foundCounter == len(area.borderNodes) { break } } } } // 3. Spočítáme min/max lat/lon area.computeBoundary() printStat() fmt.Fprintf(os.Stderr, "\nCoordinates of %d nodes of area found\n", foundCounter) //////////////////////////////////////////////////////////////////////// // ČTVRTÝ PRŮCHOD ////////////////////////////////////////////////////// fmt.Fprintf(os.Stderr, "[Phase 4] Getting all wanted nodes and comparing them with parsed area\n") // 1. Resetujeme se na začátek souboru a připravíme se na další průchod resetReader("node") parser.SkipElements([]string{"way", "relation"}).SkipOuterElements() // 2. Projdeme všechny nodes a zaznamenáme si ty, které jsou v oblasti a mají patřičný tag foundCounter = 0 phaseStart = time.Now() for xml := range parser.Stream() { switch xml.Name { case "node": if hasTagValue(xml, "amenity", "drinking_water") || hasTagValue(xml, "drinking_water", "yes") { lat, _ := strconv.ParseFloat(xml.Attrs["lat"], 64) lon, _ := strconv.ParseFloat(xml.Attrs["lon"], 64) // Podrobnější kontrola if area.isInside(node{lat: lat, lon: lon}) { fmt.Printf("%s\n", xml.Attrs["id"]) foundCounter++ } } } } printStat() fmt.Fprintf(os.Stderr, "\nFound %d 'amenity=drinkig_water' or 'drinking_water=yes' nodes\n", foundCounter) }