OSM
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
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(
|
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
|
||||||
)
|
)
|
||||||
Reference in New Issue
Block a user