package osm import ( "context" "fmt" "io" "log" "math" "net/http" "os" "path/filepath" "runtime" "strconv" "sync" "time" ) var ( userLat = 50.06465 userLon = 19.94598 radiusM = 2000.0 maxZoom = 17 userAgent = "krakow-tiles-downloader/1.0 (+your_email@example.com)" osmTileURL = "https://tile.openstreetmap.org/%d/%d/%d.png" tilesDir = "tiles" ) const earthRadius = 6378137.0 func offsetLatLon(lat, lon, distance, bearingRad float64) (float64, float64) { r := earthRadius latRad := lat * math.Pi / 180.0 lonRad := lon * math.Pi / 180.0 angDist := distance / r newLatRad := math.Asin(math.Sin(latRad)*math.Cos(angDist) + math.Cos(latRad)*math.Sin(angDist)*math.Cos(bearingRad)) newLonRad := lonRad + math.Atan2(math.Sin(bearingRad)*math.Sin(angDist)*math.Cos(latRad), math.Cos(angDist)-math.Sin(latRad)*math.Sin(newLatRad)) return newLatRad * 180.0 / math.Pi, newLonRad * 180.0 / math.Pi } func boundingBoxForCircle(lat, lon, radius float64) (minLat, maxLat, minLon, maxLon float64) { maxLat, _ = offsetLatLon(lat, lon, radius, 0) minLat, _ = offsetLatLon(lat, lon, radius, math.Pi) _, maxLon = offsetLatLon(lat, lon, radius, math.Pi/2) _, minLon = offsetLatLon(lat, lon, radius, 3*math.Pi/2) return } func latLonToTile(lat, lon float64, z int) (x, y int) { latRad := lat * math.Pi / 180.0 n := math.Pow(2.0, float64(z)) xFloat := (lon + 180.0) / 360.0 * n yFloat := (1.0 - math.Log(math.Tan(latRad)+1.0/math.Cos(latRad))/math.Pi) / 2.0 * n return int(math.Floor(xFloat)), int(math.Floor(yFloat)) } func tileXYBounds(minLat, maxLat, minLon, maxLon float64, z int) (minX, maxX, minY, maxY int) { x1, y1 := latLonToTile(maxLat, minLon, z) x2, y2 := latLonToTile(minLat, maxLon, z) if x1 > x2 { minX, maxX = x2, x1 } else { minX, maxX = x1, x2 } if y1 > y2 { minY, maxY = y2, y1 } else { minY, maxY = y1, y2 } return } func downloadTile(ctx context.Context, z, x, y int) error { url := fmt.Sprintf(osmTileURL, z, x, y) req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) req.Header.Set("User-Agent", userAgent) resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode == 429 || resp.StatusCode == 403 { time.Sleep(5 * time.Second) return nil } if resp.StatusCode != 200 { return fmt.Errorf("HTTP %d for %s", resp.StatusCode, url) } tileCount := 1 << uint(z) yFlipped := tileCount - 1 - y path := filepath.Join(tilesDir, strconv.Itoa(z), strconv.Itoa(x)) if err := os.MkdirAll(path, 0o755); err != nil { return err } filePath := filepath.Join(path, fmt.Sprintf("%d.png", yFlipped)) f, err := os.Create(filePath) if err != nil { return err } defer f.Close() _, err = io.Copy(f, resp.Body) return err } func downloadRange(ctx context.Context, z, minX, maxX, minY, maxY int) { log.Printf("Downloading tiles zoom %d: x %d..%d, y %d..%d", z, minX, maxX, minY, maxY) sem := make(chan struct{}, runtime.NumCPU()) var wg sync.WaitGroup rate := time.NewTicker(1 * time.Second) defer rate.Stop() for x := minX; x <= maxX; x++ { for y := minY; y <= maxY; y++ { wg.Add(1) sem <- struct{}{} <-rate.C go func(x, y int) { defer wg.Done() defer func() { <-sem }() filePath := filepath.Join(tilesDir, strconv.Itoa(z), strconv.Itoa(x), fmt.Sprintf("%d.png", (1<