diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-03 08:25:07 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-03 08:25:07 +0200 |
| commit | 9707f9ad09db935af7d06b5de08bc52de13ba6a7 (patch) | |
| tree | 34bcd01e5eda302a8730bf0817d5dd7e82186359 /osmpass/fetch.go | |
| parent | f06a10982ab9d5aaa7d31cea31964431fa75f253 (diff) | |
feat: Detect mountain pass crossings, name climbs after passes, render ride charts
- osmpass: extract mountain_pass=yes nodes from an OSM PBF, enrich centcols passes with OSM coordinates, resumable -fetch-osm download
- mountain_pass: DetectCrossings for a ride, MatchClimb to name a climb after the pass it tops
- ride: expose Points(), add Climb.Top() and Climb.Name
- chart: -chart renders elevation profile with climb bands and pass markers
- commands: -fetch-osm, -extract-osm, -import-cached, -enrich
Diffstat (limited to 'osmpass/fetch.go')
| -rw-r--r-- | osmpass/fetch.go | 98 |
1 files changed, 98 insertions, 0 deletions
diff --git a/osmpass/fetch.go b/osmpass/fetch.go new file mode 100644 index 0000000..6abd136 --- /dev/null +++ b/osmpass/fetch.go @@ -0,0 +1,98 @@ +package osmpass + +import ( + "errors" + "fmt" + "io" + "log/slog" + "net/http" + "os" + + "github.com/schollz/progressbar/v3" +) + +const francePBFRUL = "https://download.geofabrik.de/europe/france-latest.osm.pbf" + +// FetchFrancePBF downloads the full France OSM PBF to destPath, resuming a +// partial download when the file already exists and skipping the download +// entirely when it is already complete. +func FetchFrancePBF(destPath string) error { + client := &http.Client{} + + head, err := client.Head(francePBFRUL) + if err != nil { + return fmt.Errorf("failed to check remote size: %w", err) + } + head.Body.Close() + total := head.ContentLength + if total <= 0 { + return fmt.Errorf("unexpected remote content length: %d", total) + } + + info, statErr := os.Stat(destPath) + var offset int64 + if statErr == nil { + offset = info.Size() + if offset >= total { + slog.Info("Already downloaded", "file", destPath, "bytes", offset) + return nil + } + if offset > 0 { + slog.Info("Resuming download", "file", destPath, "bytes", offset, "remaining", total-offset) + } + } else if !errors.Is(statErr, os.ErrNotExist) { + return fmt.Errorf("failed to stat %s: %w", destPath, statErr) + } + + req, err := http.NewRequest(http.MethodGet, francePBFRUL, nil) + if err != nil { + return err + } + req.Header.Set("User-Agent", "biking_home") + if offset > 0 { + req.Header.Set("Range", fmt.Sprintf("bytes=%d-", offset)) + } + + res, err := client.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + + resuming := offset > 0 && res.StatusCode == http.StatusPartialContent + if !resuming { + if res.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status code: %d", res.StatusCode) + } + if offset > 0 { + slog.Warn("Server ignored Range header, restarting download", "file", destPath) + } + offset = 0 + } + + mode := os.O_CREATE | os.O_WRONLY | os.O_TRUNC + if resuming { + mode = os.O_CREATE | os.O_WRONLY + } + file, err := os.OpenFile(destPath, mode, 0o644) + if err != nil { + return err + } + defer file.Close() + + bar := progressbar.DefaultBytes(res.ContentLength, "Downloading france-latest.osm.pbf") + if resuming { + // Re-append to the partial file when the server honored the range request. + if _, err := file.Seek(offset, io.SeekStart); err != nil { + return err + } + } + _, err = io.Copy(io.MultiWriter(file, bar), res.Body) + if err != nil { + return fmt.Errorf("failed to download %s: %w", francePBFRUL, err) + } + if size, err := file.Stat(); err == nil && size.Size() != total { + return fmt.Errorf("download incomplete: got %d bytes, want %d", size.Size(), total) + } + return nil +} |