OSM
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
152
internal/osm/osm.go
Normal file
152
internal/osm/osm.go
Normal 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)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
CREATE TABLE IF NOT EXISTS articles(
|
||||
uuid CHAR(36) PRIMARY KEY,
|
||||
title VARCHAR(255),
|
||||
content TEXT
|
||||
title VARCHAR(255) NOT NULL,
|
||||
content TEXT NOT NULL
|
||||
)
|
||||
Reference in New Issue
Block a user