mirror of
https://github.com/Cyclenerd/offline-map-tile-downloader.git
synced 2026-06-01 12:24:53 +02:00
go init
This commit is contained in:
@@ -0,0 +1,790 @@
|
||||
// The main package is the entry point for the application.
|
||||
package main
|
||||
|
||||
// Import necessary libraries.
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"embed" // Used for embedding files into the binary.
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket" // WebSocket library for real-time communication.
|
||||
)
|
||||
|
||||
//go:embed templates/index.html
|
||||
var indexHTML []byte // Embeds the index.html file into the binary.
|
||||
|
||||
//go:embed config/map_sources.json
|
||||
var mapSourcesJSON []byte // Embeds the map_sources.json file into the binary.
|
||||
|
||||
//go:embed static/*
|
||||
var staticFiles embed.FS
|
||||
|
||||
// upgrader is used to upgrade HTTP connections to WebSocket connections.
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024, // Size of the read buffer.
|
||||
WriteBufferSize: 1024, // Size of the write buffer.
|
||||
}
|
||||
|
||||
// Global variables used throughout the application.
|
||||
var (
|
||||
mapSources map[string]string // Stores the available map sources.
|
||||
downloadCancel context.CancelFunc // Function to cancel an ongoing download.
|
||||
downloading bool // Flag to indicate if a download is in progress.
|
||||
downloadingMutex sync.Mutex // Mutex to protect access to the downloading flag.
|
||||
cacheDir *string
|
||||
maxWorkers *int
|
||||
rateLimit *int
|
||||
maxRetries *int
|
||||
)
|
||||
|
||||
// Tile represents a single map tile with X, Y coordinates and zoom level Z.
|
||||
type Tile struct {
|
||||
X, Y, Z uint32
|
||||
}
|
||||
|
||||
// BoundingBox represents a geographical area with North, South, East, and West boundaries.
|
||||
type BoundingBox struct {
|
||||
North, South, East, West float64
|
||||
}
|
||||
|
||||
// LatLng represents a geographical point with latitude and longitude.
|
||||
type LatLng struct {
|
||||
Lat float64 `json:"lat"` // Latitude
|
||||
Lng float64 `json:"lng"` // Longitude
|
||||
}
|
||||
|
||||
// DownloadRequest represents a request to download map tiles for a specific area.
|
||||
type DownloadRequest struct {
|
||||
Polygons [][]LatLng `json:"polygons"` // The polygons defining the download area.
|
||||
MinZoom int `json:"min_zoom"` // The minimum zoom level to download.
|
||||
MaxZoom int `json:"max_zoom"` // The maximum zoom level to download.
|
||||
MapStyle string `json:"map_style"` // The URL of the map tile server.
|
||||
ConvertTo8Bit bool `json:"convert_to_8bit"` // Whether to convert images to 8-bit PNG.
|
||||
}
|
||||
|
||||
// WorldDownloadRequest represents a request to download map tiles for the entire world.
|
||||
type WorldDownloadRequest struct {
|
||||
MapStyle string `json:"map_style"` // The URL of the map tile server.
|
||||
ConvertTo8Bit bool `json:"convert_to_8bit"` // Whether to convert images to 8-bit PNG.
|
||||
}
|
||||
|
||||
// WSMessage represents a WebSocket message with a type and data.
|
||||
type WSMessage struct {
|
||||
Type string `json:"type"` // The type of the message (e.g., "start_download").
|
||||
Data interface{} `json:"data"` // The data associated with the message.
|
||||
}
|
||||
|
||||
// main is the entry point of the application.
|
||||
func main() {
|
||||
// Command line flags
|
||||
port := flag.Int("port", 8080, "Port number for the server")
|
||||
cacheDir = flag.String("maps-directory", "maps", "Directory for storing map tiles. This is where the downloaded tiles will be saved.")
|
||||
maxWorkers = flag.Int("max-workers", 10, "Number of concurrent download workers")
|
||||
rateLimit = flag.Int("rate-limit", 50, "Maximum number of tiles to download per second")
|
||||
maxRetries = flag.Int("max-retries", 3, "Maximum number of retries for downloading a tile")
|
||||
help := flag.Bool("help", false, "Show help message")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if *help {
|
||||
flag.Usage()
|
||||
return
|
||||
}
|
||||
|
||||
// Create cache directory if it doesn't exist.
|
||||
if err := os.MkdirAll(*cacheDir, 0755); err != nil {
|
||||
log.Fatalf("Failed to create cache directory: %v", err)
|
||||
}
|
||||
|
||||
// Load map sources from the embedded JSON file.
|
||||
if err := json.Unmarshal(mapSourcesJSON, &mapSources); err != nil {
|
||||
log.Fatalf("Failed to load map sources: %v", err)
|
||||
}
|
||||
|
||||
// Register HTTP handlers for different routes.
|
||||
http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
||||
r.URL.Path = "/static/favicon.ico"
|
||||
http.FileServer(http.FS(staticFiles)).ServeHTTP(w, r)
|
||||
})
|
||||
http.HandleFunc("/", serveHome)
|
||||
http.HandleFunc("/get_map_sources", getMapSources)
|
||||
http.HandleFunc("/ws", wsHandler)
|
||||
|
||||
http.HandleFunc("/tiles/", serveTile)
|
||||
http.HandleFunc("/get_cached_tiles/", getCachedTiles)
|
||||
|
||||
staticFS, err := fs.Sub(staticFiles, "static")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
|
||||
|
||||
// Start the HTTP server on port 8080.
|
||||
addr := fmt.Sprintf(":%d", *port)
|
||||
log.Printf("Starting server on %s", addr)
|
||||
if err := http.ListenAndServe(addr, nil); err != nil {
|
||||
log.Fatalf("Error starting server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// serveHome serves the main HTML page.
|
||||
func serveHome(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write(indexHTML)
|
||||
}
|
||||
|
||||
// getMapSources serves the available map sources as JSON.
|
||||
func getMapSources(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(mapSourcesJSON)
|
||||
}
|
||||
|
||||
// wsHandler handles WebSocket connections.
|
||||
func wsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Upgrade the HTTP connection to a WebSocket connection.
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Loop to read messages from the WebSocket connection.
|
||||
for {
|
||||
messageType, p, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if messageType == websocket.TextMessage {
|
||||
var msg WSMessage
|
||||
if err := json.Unmarshal(p, &msg); err != nil {
|
||||
log.Println("Error unmarshalling message:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle different message types.
|
||||
switch msg.Type {
|
||||
case "start_download":
|
||||
var req DownloadRequest
|
||||
b, _ := json.Marshal(msg.Data)
|
||||
if err := json.Unmarshal(b, &req); err != nil {
|
||||
sendError(conn, "Invalid download request")
|
||||
continue
|
||||
}
|
||||
go handleStartDownload(conn, req)
|
||||
case "start_world_download":
|
||||
var req WorldDownloadRequest
|
||||
b, _ := json.Marshal(msg.Data)
|
||||
if err := json.Unmarshal(b, &req); err != nil {
|
||||
sendError(conn, "Invalid world download request")
|
||||
continue
|
||||
}
|
||||
go handleStartWorldDownload(conn, req)
|
||||
case "cancel_download":
|
||||
handleCancelDownload(conn)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleStartDownload starts a new download process for a defined area.
|
||||
func handleStartDownload(conn *websocket.Conn, req DownloadRequest) {
|
||||
// Lock the mutex to ensure only one download runs at a time.
|
||||
downloadingMutex.Lock()
|
||||
if downloading {
|
||||
sendError(conn, "Another download is already in progress.")
|
||||
downloadingMutex.Unlock()
|
||||
return
|
||||
}
|
||||
downloading = true
|
||||
downloadingMutex.Unlock()
|
||||
|
||||
// Defer setting the downloading flag to false.
|
||||
defer func() {
|
||||
downloadingMutex.Lock()
|
||||
downloading = false
|
||||
downloadingMutex.Unlock()
|
||||
}()
|
||||
|
||||
log.Printf("Starting download for area: %v, zoom: %d-%d, map style: %s", req.Polygons, req.MinZoom, req.MaxZoom, req.MapStyle)
|
||||
|
||||
// Create a new context to allow for cancellation.
|
||||
var ctx context.Context
|
||||
ctx, downloadCancel = context.WithCancel(context.Background())
|
||||
|
||||
// Get the style name and cache directory.
|
||||
styleName := getStyleName(req.MapStyle)
|
||||
styleCacheDir := getStyleCacheDir(styleName)
|
||||
|
||||
// Validate the zoom range.
|
||||
if req.MinZoom < 0 || req.MaxZoom > 19 || req.MinZoom > req.MaxZoom {
|
||||
sendError(conn, "Invalid zoom range (must be 0-19, min <= max)")
|
||||
return
|
||||
}
|
||||
// Validate the polygons.
|
||||
if len(req.Polygons) == 0 {
|
||||
sendError(conn, "No polygons provided")
|
||||
return
|
||||
}
|
||||
|
||||
// Get the list of tiles to download.
|
||||
tilesToDownload := getTilesForPolygons(req.Polygons, req.MinZoom, req.MaxZoom)
|
||||
|
||||
// Start the tile download process.
|
||||
downloadTiles(ctx, conn, tilesToDownload, req.MapStyle, styleCacheDir, req.ConvertTo8Bit)
|
||||
|
||||
// If the download was not cancelled
|
||||
if ctx.Err() == nil {
|
||||
sendMessage(conn, "download_complete", nil)
|
||||
}
|
||||
}
|
||||
|
||||
// handleStartWorldDownload starts a new download process for the entire world.
|
||||
func handleStartWorldDownload(conn *websocket.Conn, req WorldDownloadRequest) {
|
||||
// Lock the mutex to ensure only one download runs at a time.
|
||||
downloadingMutex.Lock()
|
||||
if downloading {
|
||||
sendError(conn, "Another download is already in progress.")
|
||||
downloadingMutex.Unlock()
|
||||
return
|
||||
}
|
||||
downloading = true
|
||||
downloadingMutex.Unlock()
|
||||
|
||||
// Defer setting the downloading flag to false.
|
||||
defer func() {
|
||||
downloadingMutex.Lock()
|
||||
downloading = false
|
||||
downloadingMutex.Unlock()
|
||||
}()
|
||||
|
||||
log.Printf("Starting world download, map style: %s", req.MapStyle)
|
||||
|
||||
// Create a new context to allow for cancellation.
|
||||
var ctx context.Context
|
||||
ctx, downloadCancel = context.WithCancel(context.Background())
|
||||
|
||||
// Get the style name and cache directory.
|
||||
styleName := getStyleName(req.MapStyle)
|
||||
styleCacheDir := getStyleCacheDir(styleName)
|
||||
|
||||
// Get the list of tiles to download for the world.
|
||||
tilesToDownload := getWorldTiles()
|
||||
|
||||
// Start the tile download process.
|
||||
downloadTiles(ctx, conn, tilesToDownload, req.MapStyle, styleCacheDir, req.ConvertTo8Bit)
|
||||
|
||||
// If the download was not cancelled
|
||||
if ctx.Err() == nil {
|
||||
sendMessage(conn, "download_complete", nil)
|
||||
}
|
||||
}
|
||||
|
||||
// handleCancelDownload cancels an ongoing download.
|
||||
func handleCancelDownload(conn *websocket.Conn) {
|
||||
if downloadCancel != nil {
|
||||
downloadCancel()
|
||||
log.Printf("Download cancelled by user")
|
||||
sendMessage(conn, "download_cancelled", nil)
|
||||
}
|
||||
}
|
||||
|
||||
// downloadTiles downloads a list of tiles concurrently.
|
||||
func downloadTiles(ctx context.Context, conn *websocket.Conn, tilesToDownload []Tile, mapStyle, styleCacheDir string, convertTo8Bit bool) {
|
||||
// Create a channel for WebSocket messages.
|
||||
msgChan := make(chan WSMessage)
|
||||
var writerWg sync.WaitGroup
|
||||
writerWg.Add(1)
|
||||
// Start a goroutine to send messages from the channel to the WebSocket connection.
|
||||
go func() {
|
||||
defer writerWg.Done()
|
||||
for msg := range msgChan {
|
||||
if err := conn.WriteJSON(msg); err != nil {
|
||||
log.Println("Error writing JSON to websocket:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Send a message indicating the download has started.
|
||||
msgChan <- WSMessage{Type: "download_started", Data: map[string]int{"total_tiles": len(tilesToDownload)}}
|
||||
|
||||
// Use a WaitGroup to wait for all download goroutines to finish.
|
||||
var downloadWg sync.WaitGroup
|
||||
tileChan := make(chan Tile)
|
||||
|
||||
// Start the download workers.
|
||||
for i := 0; i < *maxWorkers; i++ {
|
||||
downloadWg.Add(1)
|
||||
go func() {
|
||||
defer downloadWg.Done()
|
||||
for tile := range tileChan {
|
||||
select {
|
||||
case <-ctx.Done(): // Check if the download has been cancelled.
|
||||
return
|
||||
default:
|
||||
downloadTile(ctx, msgChan, tile, mapStyle, styleCacheDir, convertTo8Bit, *maxRetries)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Rate limit the download of tiles.
|
||||
ticker := time.NewTicker(time.Second / time.Duration(*rateLimit))
|
||||
defer ticker.Stop()
|
||||
|
||||
for _, tile := range tilesToDownload {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break
|
||||
case <-ticker.C:
|
||||
tileChan <- tile
|
||||
}
|
||||
}
|
||||
close(tileChan)
|
||||
|
||||
// Wait for all downloads to complete.
|
||||
downloadWg.Wait()
|
||||
close(msgChan)
|
||||
writerWg.Wait()
|
||||
|
||||
// If the download was not cancelled, send a completion message.
|
||||
if ctx.Err() == nil {
|
||||
log.Printf("Download finished successfully")
|
||||
sendMessage(conn, "tiles_downloaded", nil)
|
||||
} else {
|
||||
log.Printf("Download failed or was cancelled")
|
||||
}
|
||||
}
|
||||
|
||||
// downloadTile downloads a single map tile.
|
||||
func downloadTile(ctx context.Context, msgChan chan<- WSMessage, tile Tile, mapStyle, styleCacheDir string, convertTo8Bit bool, maxRetries int) {
|
||||
// Construct the path to the tile file.
|
||||
tileDir := filepath.Join(styleCacheDir, fmt.Sprintf("%d/%d", tile.Z, tile.X))
|
||||
tilePath := filepath.Join(tileDir, fmt.Sprintf("%d.png", tile.Y))
|
||||
|
||||
// Check if the tile already exists in the cache.
|
||||
if _, err := os.Stat(tilePath); err == nil {
|
||||
bounds := tileBounds(tile)
|
||||
msgChan <- WSMessage{Type: "tile_skipped", Data: map[string]float64{
|
||||
"west": bounds.West,
|
||||
"south": bounds.South,
|
||||
"east": bounds.East,
|
||||
"north": bounds.North,
|
||||
}}
|
||||
return
|
||||
}
|
||||
|
||||
// Construct the URL for the tile.
|
||||
subdomain := string('a' + rand.Intn(3))
|
||||
url := strings.Replace(mapStyle, "{s}", subdomain, -1)
|
||||
url = strings.Replace(url, "{z}", fmt.Sprintf("%d", tile.Z), -1)
|
||||
url = strings.Replace(url, "{x}", fmt.Sprintf("%d", tile.X), -1)
|
||||
url = strings.Replace(url, "{y}", fmt.Sprintf("%d", tile.Y), -1)
|
||||
|
||||
var err error
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
select {
|
||||
case <-ctx.Done(): // Check for cancellation.
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
var req *http.Request
|
||||
req, err = http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
log.Printf("Error creating request for tile %v: %v. Retrying...", tile, err)
|
||||
time.Sleep(time.Second * time.Duration(math.Pow(2, float64(attempt))))
|
||||
continue
|
||||
}
|
||||
req.Header.Set("User-Agent", "MapTileDownloader/1.0 (Go)")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("Error downloading tile %v: %v. Retrying...", tile, err)
|
||||
time.Sleep(time.Second * time.Duration(math.Pow(2, float64(attempt))))
|
||||
continue
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
resp.Body.Close()
|
||||
log.Printf("Unexpected status code %d for tile %v. Retrying...", resp.StatusCode, tile)
|
||||
time.Sleep(time.Second * time.Duration(math.Pow(2, float64(attempt))))
|
||||
continue
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
log.Printf("Error reading tile body for tile %v: %v. Retrying...", tile, err)
|
||||
time.Sleep(time.Second * time.Duration(math.Pow(2, float64(attempt))))
|
||||
continue
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(tileDir, 0755); err != nil {
|
||||
log.Printf("Error creating tile directory for tile %v: %v", tile, err)
|
||||
return // No point in retrying if we can't create the directory
|
||||
}
|
||||
|
||||
// Convert the image to 8-bit PNG if requested.
|
||||
if convertTo8Bit {
|
||||
img, _, err := image.Decode(bytes.NewReader(body))
|
||||
if err == nil {
|
||||
paletted := image.NewPaletted(img.Bounds(), color.Palette{})
|
||||
draw.Draw(paletted, paletted.Rect, img, img.Bounds().Min, draw.Src)
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, paletted); err == nil {
|
||||
body = buf.Bytes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.WriteFile(tilePath, body, 0644); err != nil {
|
||||
log.Printf("Error writing tile %v: %v", tile, err)
|
||||
return // No point in retrying if we can't write the file
|
||||
}
|
||||
|
||||
bounds := tileBounds(tile)
|
||||
msgChan <- WSMessage{Type: "tile_downloaded", Data: map[string]float64{
|
||||
"west": bounds.West,
|
||||
"south": bounds.South,
|
||||
"east": bounds.East,
|
||||
"north": bounds.North,
|
||||
}}
|
||||
return // Success!
|
||||
}
|
||||
|
||||
// If all retries fail, send a failure message.
|
||||
log.Printf("Failed to download tile %v after %d attempts.", tile, maxRetries)
|
||||
msgChan <- WSMessage{Type: "tile_failed", Data: map[string]string{"tile": fmt.Sprintf("%d/%d/%d", tile.Z, tile.X, tile.Y)}}
|
||||
}
|
||||
|
||||
// getTilesForPolygons calculates the tiles needed to cover the given polygons.
|
||||
func getTilesForPolygons(polygonsData [][]LatLng, minZoom, maxZoom int) []Tile {
|
||||
var allTiles []Tile
|
||||
tileMap := make(map[Tile]bool)
|
||||
|
||||
for _, polyData := range polygonsData {
|
||||
if len(polyData) < 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
minLat, minLon := 90.0, 180.0
|
||||
maxLat, maxLon := -90.0, -180.0
|
||||
for _, p := range polyData {
|
||||
if p.Lat < minLat {
|
||||
minLat = p.Lat
|
||||
}
|
||||
if p.Lat > maxLat {
|
||||
maxLat = p.Lat
|
||||
}
|
||||
if p.Lng < minLon {
|
||||
minLon = p.Lng
|
||||
}
|
||||
if p.Lng > maxLon {
|
||||
maxLon = p.Lng
|
||||
}
|
||||
}
|
||||
|
||||
for z := minZoom; z <= maxZoom; z++ {
|
||||
tlx, tly := latLonToTile(maxLat, minLon, uint32(z))
|
||||
brx, bry := latLonToTile(minLat, maxLon, uint32(z))
|
||||
|
||||
for x := tlx; x <= brx; x++ {
|
||||
for y := tly; y <= bry; y++ {
|
||||
tile := Tile{X: x, Y: y, Z: uint32(z)}
|
||||
if _, exists := tileMap[tile]; exists {
|
||||
continue
|
||||
}
|
||||
|
||||
bounds := tileBounds(tile)
|
||||
|
||||
// Check if the tile is completely inside the polygon
|
||||
if polygonContains(polyData, LatLng{Lat: bounds.North, Lng: bounds.West}) &&
|
||||
polygonContains(polyData, LatLng{Lat: bounds.North, Lng: bounds.East}) &&
|
||||
polygonContains(polyData, LatLng{Lat: bounds.South, Lng: bounds.West}) &&
|
||||
polygonContains(polyData, LatLng{Lat: bounds.South, Lng: bounds.East}) {
|
||||
allTiles = append(allTiles, tile)
|
||||
tileMap[tile] = true
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if the polygon is completely inside the tile
|
||||
polyInTile := true
|
||||
for _, p := range polyData {
|
||||
if !tileContains(bounds, p) {
|
||||
polyInTile = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if polyInTile {
|
||||
allTiles = append(allTiles, tile)
|
||||
tileMap[tile] = true
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for intersection
|
||||
if polygonIntersects(polyData, bounds) {
|
||||
allTiles = append(allTiles, tile)
|
||||
tileMap[tile] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allTiles
|
||||
}
|
||||
|
||||
// tileContains checks if a tile contains a point.
|
||||
func tileContains(bounds BoundingBox, point LatLng) bool {
|
||||
return point.Lat <= bounds.North && point.Lat >= bounds.South && point.Lng >= bounds.West && point.Lng <= bounds.East
|
||||
}
|
||||
|
||||
// polygonIntersects checks if a polygon intersects with a tile.
|
||||
func polygonIntersects(poly []LatLng, bounds BoundingBox) bool {
|
||||
// Check if any of the polygon's vertices are inside the tile
|
||||
for _, p := range poly {
|
||||
if tileContains(bounds, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any of the tile's corners are inside the polygon
|
||||
if polygonContains(poly, LatLng{Lat: bounds.North, Lng: bounds.West}) ||
|
||||
polygonContains(poly, LatLng{Lat: bounds.North, Lng: bounds.East}) ||
|
||||
polygonContains(poly, LatLng{Lat: bounds.South, Lng: bounds.West}) ||
|
||||
polygonContains(poly, LatLng{Lat: bounds.South, Lng: bounds.East}) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if any of the polygon's edges intersect with the tile's edges
|
||||
for i := 0; i < len(poly); i++ {
|
||||
p1 := poly[i]
|
||||
p2 := poly[(i+1)%len(poly)]
|
||||
|
||||
if lineIntersects(p1, p2, LatLng{Lat: bounds.North, Lng: bounds.West}, LatLng{Lat: bounds.North, Lng: bounds.East}) ||
|
||||
lineIntersects(p1, p2, LatLng{Lat: bounds.North, Lng: bounds.East}, LatLng{Lat: bounds.South, Lng: bounds.East}) ||
|
||||
lineIntersects(p1, p2, LatLng{Lat: bounds.South, Lng: bounds.East}, LatLng{Lat: bounds.South, Lng: bounds.West}) ||
|
||||
lineIntersects(p1, p2, LatLng{Lat: bounds.South, Lng: bounds.West}, LatLng{Lat: bounds.North, Lng: bounds.West}) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// lineIntersects checks if two line segments intersect.
|
||||
func lineIntersects(p1, q1, p2, q2 LatLng) bool {
|
||||
o1 := orientation(p1, q1, p2)
|
||||
o2 := orientation(p1, q1, q2)
|
||||
o3 := orientation(p2, q2, p1)
|
||||
o4 := orientation(p2, q2, q1)
|
||||
|
||||
if o1 != o2 && o3 != o4 {
|
||||
return true
|
||||
}
|
||||
|
||||
// Special Cases for colinear points
|
||||
if o1 == 0 && onSegment(p1, p2, q1) {
|
||||
return true
|
||||
}
|
||||
if o2 == 0 && onSegment(p1, q2, q1) {
|
||||
return true
|
||||
}
|
||||
if o3 == 0 && onSegment(p2, p1, q2) {
|
||||
return true
|
||||
}
|
||||
if o4 == 0 && onSegment(p2, q1, q2) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// orientation finds the orientation of the ordered triplet (p, q, r).
|
||||
func orientation(p, q, r LatLng) int {
|
||||
val := (q.Lng-p.Lng)*(r.Lat-q.Lat) - (q.Lat-p.Lat)*(r.Lng-q.Lng)
|
||||
if val == 0 {
|
||||
return 0 // Collinear
|
||||
}
|
||||
if val > 0 {
|
||||
return 1 // Clockwise
|
||||
}
|
||||
return 2 // Counterclockwise
|
||||
}
|
||||
|
||||
// onSegment checks if point q lies on segment pr.
|
||||
func onSegment(p, q, r LatLng) bool {
|
||||
if q.Lat <= math.Max(p.Lat, r.Lat) && q.Lat >= math.Min(p.Lat, r.Lat) &&
|
||||
q.Lng <= math.Max(p.Lng, r.Lng) && q.Lng >= math.Min(p.Lng, r.Lng) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getWorldTiles returns a list of all tiles for the world up to zoom level 7.
|
||||
func getWorldTiles() []Tile {
|
||||
var worldTiles []Tile
|
||||
for z := 0; z <= 7; z++ {
|
||||
max := 1 << z
|
||||
for x := 0; x < max; x++ {
|
||||
for y := 0; y < max; y++ {
|
||||
worldTiles = append(worldTiles, Tile{X: uint32(x), Y: uint32(y), Z: uint32(z)})
|
||||
}
|
||||
}
|
||||
}
|
||||
return worldTiles
|
||||
}
|
||||
|
||||
// serveTile serves a single cached tile.
|
||||
func serveTile(w http.ResponseWriter, r *http.Request) {
|
||||
parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/tiles/"), "/")
|
||||
if len(parts) != 4 {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
styleName := parts[0]
|
||||
z := parts[1]
|
||||
x := parts[2]
|
||||
y := strings.TrimSuffix(parts[3], ".png")
|
||||
|
||||
tilePath := filepath.Join(*cacheDir, sanitizeStyleName(styleName), z, x, y+".png")
|
||||
http.ServeFile(w, r, tilePath)
|
||||
}
|
||||
|
||||
// getCachedTiles returns a list of cached tiles for a specific map style.
|
||||
func getCachedTiles(w http.ResponseWriter, r *http.Request) {
|
||||
styleName := strings.TrimPrefix(r.URL.Path, "/get_cached_tiles/")
|
||||
styleCacheDir := getStyleCacheDir(styleName)
|
||||
|
||||
var cachedTiles [][3]uint32
|
||||
err := filepath.Walk(styleCacheDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() && strings.HasSuffix(info.Name(), ".png") {
|
||||
parts := strings.Split(strings.TrimSuffix(path, ".png"), string(filepath.Separator))
|
||||
if len(parts) >= 4 {
|
||||
z, zErr := strToUint32(parts[len(parts)-3])
|
||||
x, xErr := strToUint32(parts[len(parts)-2])
|
||||
y, yErr := strToUint32(parts[len(parts)-1])
|
||||
if zErr == nil && xErr == nil && yErr == nil {
|
||||
cachedTiles = append(cachedTiles, [3]uint32{z, x, y})
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Error reading cache: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(cachedTiles)
|
||||
}
|
||||
|
||||
// getStyleName returns the name of the map style for a given URL.
|
||||
func getStyleName(mapStyleURL string) string {
|
||||
for name, url := range mapSources {
|
||||
if url == mapStyleURL {
|
||||
return name
|
||||
}
|
||||
}
|
||||
return "default"
|
||||
}
|
||||
|
||||
// getStyleCacheDir returns the cache directory for a given style name.
|
||||
func getStyleCacheDir(styleName string) string {
|
||||
return filepath.Join(*cacheDir, sanitizeStyleName(styleName))
|
||||
}
|
||||
|
||||
// nonAlphanumeric is a regular expression to match any character that is not a letter, number, hyphen, or underscore.
|
||||
var nonAlphanumeric = regexp.MustCompile(`[^a-zA-Z0-9-_]+`)
|
||||
|
||||
// sanitizeStyleName sanitizes the style name to be used as a directory name.
|
||||
func sanitizeStyleName(styleName string) string {
|
||||
return nonAlphanumeric.ReplaceAllString(strings.ReplaceAll(styleName, " ", "-"), "")
|
||||
}
|
||||
|
||||
// sendMessage sends a WebSocket message.
|
||||
func sendMessage(conn *websocket.Conn, msgType string, data interface{}) {
|
||||
msg := WSMessage{Type: msgType, Data: data}
|
||||
if err := conn.WriteJSON(msg); err != nil {
|
||||
log.Println("Error sending message:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// sendError sends an error message over the WebSocket connection.
|
||||
func sendError(conn *websocket.Conn, message string) {
|
||||
sendMessage(conn, "error", map[string]string{"message": message})
|
||||
}
|
||||
|
||||
// strToUint32 converts a string to a uint32.
|
||||
func strToUint32(s string) (uint32, error) {
|
||||
var i uint32
|
||||
_, err := fmt.Sscanf(s, "%d", &i)
|
||||
return i, err
|
||||
}
|
||||
|
||||
// latLonToTile converts latitude and longitude to tile coordinates.
|
||||
func latLonToTile(lat, lon float64, zoom uint32) (x, y uint32) {
|
||||
latRad := lat * math.Pi / 180
|
||||
n := math.Pow(2, float64(zoom))
|
||||
x = uint32(n * ((lon + 180) / 360))
|
||||
y = uint32(n * (1 - (math.Log(math.Tan(latRad)+1/math.Cos(latRad)) / math.Pi)) / 2)
|
||||
return
|
||||
}
|
||||
|
||||
// tileBounds calculates the geographical bounding box of a tile.
|
||||
func tileBounds(tile Tile) BoundingBox {
|
||||
n := math.Pow(2.0, float64(tile.Z))
|
||||
lonDeg := float64(tile.X)/n*360.0 - 180.0
|
||||
latRad := math.Atan(math.Sinh(math.Pi * (1 - 2*float64(tile.Y)/n)))
|
||||
latDeg := latRad * 180.0 / math.Pi
|
||||
|
||||
lon2Deg := float64(tile.X+1)/n*360.0 - 180.0
|
||||
lat2Rad := math.Atan(math.Sinh(math.Pi * (1 - 2*float64(tile.Y+1)/n)))
|
||||
lat2Deg := lat2Rad * 180.0 / math.Pi
|
||||
|
||||
return BoundingBox{
|
||||
North: latDeg,
|
||||
South: lat2Deg,
|
||||
East: lon2Deg,
|
||||
West: lonDeg,
|
||||
}
|
||||
}
|
||||
|
||||
// polygonContains checks if a point is inside a polygon using the ray casting algorithm.
|
||||
func polygonContains(poly []LatLng, point LatLng) bool {
|
||||
in := false
|
||||
for i, j := 0, len(poly)-1; i < len(poly); j, i = i, i+1 {
|
||||
if (poly[i].Lat > point.Lat) != (poly[j].Lat > point.Lat) &&
|
||||
(point.Lng < (poly[j].Lng-poly[i].Lng)*(point.Lat-poly[i].Lat)/(poly[j].Lat-poly[i].Lat)+poly[i].Lng) {
|
||||
in = !in
|
||||
}
|
||||
}
|
||||
return in
|
||||
}
|
||||
Reference in New Issue
Block a user