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 ` // ExtractMountainPasses scans pbfPath for mountain pass nodes and upserts them // into the database. An optional reporter is invoked as scanning progresses // with bytes scanned and the total file size, so callers (e.g. the CLI) can // render progress. func ExtractMountainPasses(ctx context.Context, pbfPath string, db *sql.DB, reporters ...func(current, total int64)) (int, error) { file, err := os.Open(pbfPath) if err != nil { return 0, fmt.Errorf("failed to open pbf file: %w", err) } defer file.Close() info, err := file.Stat() if err != nil { return 0, fmt.Errorf("failed to stat pbf file: %w", err) } total := info.Size() report := func(int64, int64) {} if len(reporters) > 0 { report = reporters[0] } reader := &countingReader{r: file} scanner := osmpbf.New(ctx, reader, 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() { report(reader.n, total) 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 } // countingReader tracks the number of bytes read from an underlying reader, // used to report scan progress against the PBF file size. type countingReader struct { r io.Reader n int64 } func (c *countingReader) Read(p []byte) (int, error) { n, err := c.r.Read(p) c.n += int64(n) return n, err } 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.FieldsSeq(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 }