This commit is contained in:
2025-10-05 04:19:21 +02:00
parent 6df63dc4c1
commit f5040fd91e
4 changed files with 158 additions and 2 deletions

View File

@@ -12,6 +12,7 @@ func Setup() {
r.Get("/articles", article.ArticleQueryHandler) r.Get("/articles", article.ArticleQueryHandler)
r.Get("/articles-download", article.ArticleDownloadHandler) r.Get("/articles-download", article.ArticleDownloadHandler)
r.Handle("/tiles/", http.StripPrefix("/tiles/", http.FileServer(http.Dir("tiles"))))
http.ListenAndServe(":8080", r) http.ListenAndServe(":8080", r)
} }

View File

@@ -5,6 +5,7 @@ import (
"scrap/api" "scrap/api"
"scrap/internal/config" "scrap/internal/config"
"scrap/internal/db" "scrap/internal/db"
"scrap/internal/osm"
) )
func main() { func main() {
@@ -15,5 +16,7 @@ func main() {
log.SetFlags(log.Lshortfile) log.SetFlags(log.Lshortfile)
osm.OSM()
api.Setup() api.Setup()
} }

152
internal/osm/osm.go Normal file
View File

@@ -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<<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)
}
}()
}

View File

@@ -1,5 +1,5 @@
CREATE TABLE IF NOT EXISTS articles( CREATE TABLE IF NOT EXISTS articles(
uuid CHAR(36) PRIMARY KEY, uuid CHAR(36) PRIMARY KEY,
title VARCHAR(255), title VARCHAR(255) NOT NULL,
content TEXT content TEXT NOT NULL
) )