aboutsummaryrefslogtreecommitdiff
path: root/mountain_pass/extract.go
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2026-08-21 10:30:27 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2026-08-21 10:30:27 +0200
commit44f7333d9deef81d3468e20b378c63692a9406ec (patch)
treeaeacb99247dba4e48ee61e2792f5476a3770795a /mountain_pass/extract.go
parent6667ee479c06c14bb182f36a0a9ee704fca6218d (diff)
Use OSM as the single source of mountain passes
Replace the centcols CSV download/enrichment pipeline with OSM PBF extraction as the primary (only) source: merge osmpass into mountain_pass, drop the centcols table and model fields, filter to named passes with elevation, and remove the centcols CLI flags.
Diffstat (limited to 'mountain_pass/extract.go')
-rw-r--r--mountain_pass/extract.go98
1 files changed, 98 insertions, 0 deletions
diff --git a/mountain_pass/extract.go b/mountain_pass/extract.go
new file mode 100644
index 0000000..81bed68
--- /dev/null
+++ b/mountain_pass/extract.go
@@ -0,0 +1,98 @@
+package mountain_pass
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "io"
+ "log/slog"
+ "os"
+ "strconv"
+ "strings"
+
+ "github.com/paulmach/osm"
+ "github.com/paulmach/osm/osmpbf"
+)
+
+const insertStatement = `
+ INSERT INTO mountain_passes (osm_id, name, elevation, latitude, longitude)
+ VALUES (?, ?, ?, ?, ?)
+ ON CONFLICT(osm_id) DO UPDATE SET
+ name = excluded.name,
+ elevation = excluded.elevation,
+ latitude = excluded.latitude,
+ longitude = excluded.longitude
+`
+
+func ExtractMountainPasses(ctx context.Context, pbfPath string, db *sql.DB) (int, error) {
+ file, err := os.Open(pbfPath)
+ if err != nil {
+ return 0, fmt.Errorf("failed to open pbf file: %w", err)
+ }
+ defer file.Close()
+
+ scanner := osmpbf.New(ctx, file, 4)
+ scanner.SkipWays = true
+ scanner.SkipRelations = true
+ scanner.FilterNode = isMountainPass
+
+ tx, err := db.BeginTx(ctx, nil)
+ if err != nil {
+ return 0, fmt.Errorf("failed to begin transaction: %w", err)
+ }
+ defer tx.Rollback()
+
+ stmt, err := tx.PrepareContext(ctx, insertStatement)
+ if err != nil {
+ return 0, fmt.Errorf("failed to prepare insert: %w", err)
+ }
+ defer stmt.Close()
+
+ count := 0
+ for scanner.Scan() {
+ node, ok := scanner.Object().(*osm.Node)
+ if !ok {
+ continue
+ }
+ elevation, hasElevation := parseElevation(node.Tags.Find("ele"))
+ var elevationValue any
+ if hasElevation {
+ elevationValue = elevation
+ }
+ if _, err := stmt.ExecContext(ctx, node.ID, node.Tags.Find("name"), elevationValue, node.Lat, node.Lon); err != nil {
+ return count, fmt.Errorf("failed to insert osm pass %d: %w", node.ID, err)
+ }
+ count++
+ }
+ if err := scanner.Err(); err != nil && err != io.EOF {
+ return count, fmt.Errorf("failed to scan pbf: %w", err)
+ }
+ if err := tx.Commit(); err != nil {
+ return count, fmt.Errorf("failed to commit transaction: %w", err)
+ }
+ slog.Info("Extracted mountain passes", "count", count, "source", pbfPath)
+ return count, nil
+}
+
+func isMountainPass(node *osm.Node) bool {
+ if node.Tags.Find("mountain_pass") == "yes" {
+ return true
+ }
+ return node.Tags.Find("natural") == "mountain_pass"
+}
+
+func parseElevation(ele string) (int, bool) {
+ ele = strings.TrimSpace(ele)
+ if ele == "" {
+ return 0, false
+ }
+ for _, candidate := range strings.Fields(ele) {
+ if value, err := strconv.Atoi(candidate); err == nil {
+ return value, true
+ }
+ if value, err := strconv.ParseFloat(candidate, 64); err == nil {
+ return int(value), true
+ }
+ }
+ return 0, false
+}