diff --git a/api/setup.go b/api/setup.go index 7d958b4..06cab1b 100644 --- a/api/setup.go +++ b/api/setup.go @@ -12,6 +12,7 @@ func Setup() { r.Get("/articles", article.ArticleQueryHandler) r.Get("/articles-download", article.ArticleDownloadHandler) + r.Handle("/tiles/", http.StripPrefix("/tiles/", http.FileServer(http.Dir("tiles")))) http.ListenAndServe(":8080", r) } diff --git a/cmd/serve/main.go b/cmd/serve/main.go index 45f945f..6769b0e 100644 --- a/cmd/serve/main.go +++ b/cmd/serve/main.go @@ -5,6 +5,7 @@ import ( "scrap/api" "scrap/internal/config" "scrap/internal/db" + "scrap/internal/osm" ) func main() { @@ -15,5 +16,7 @@ func main() { log.SetFlags(log.Lshortfile) + osm.OSM() + api.Setup() } diff --git a/internal/osm/osm.go b/internal/osm/osm.go new file mode 100644 index 0000000..865a618 --- /dev/null +++ b/internal/osm/osm.go @@ -0,0 +1,152 @@ +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<