Files
scrap/internal/osm/osm.go
2025-10-05 04:19:21 +02:00

153 lines
4.2 KiB
Go

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<<uint(z))-1-y))
if _, err := os.Stat(filePath); err == nil {
return
}
if err := downloadTile(ctx, z, x, y); err != nil {
log.Printf("Failed %d/%d/%d: %v", z, x, y, err)
}
}(x, y)
}
}
wg.Wait()
}
func runDownloader(ctx context.Context) error {
minLat, maxLat, minLon, maxLon := boundingBoxForCircle(userLat, userLon, radiusM)
minX, maxX, minY, maxY := tileXYBounds(minLat, maxLat, minLon, maxLon, maxZoom)
downloadRange(ctx, maxZoom, minX, maxX, minY, maxY)
return nil
}
func OSM(){
if err := os.MkdirAll(tilesDir, 0o755); err != nil {
log.Fatalf("failed to create tiles dir: %v", err)
}
ctx := context.Background()
go func() {
if err := runDownloader(ctx); err != nil {
log.Printf("Downloader finished with error: %v", err)
}
}()
}