summaryrefslogtreecommitdiff
path: root/osmpass/fetch.go
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2026-08-03 08:25:07 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2026-08-03 08:25:07 +0200
commit9707f9ad09db935af7d06b5de08bc52de13ba6a7 (patch)
tree34bcd01e5eda302a8730bf0817d5dd7e82186359 /osmpass/fetch.go
parentf06a10982ab9d5aaa7d31cea31964431fa75f253 (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.go98
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
+}