storage.go

  1package routing
  2
  3import (
  4	"crypto/sha256"
  5	"encoding/hex"
  6	"encoding/json"
  7	"fmt"
  8	"log/slog"
  9	"os"
 10	"path/filepath"
 11)
 12
 13// RouteStorage handles persistence of route data to disk
 14type RouteStorage struct {
 15	baseDir string
 16}
 17
 18// NewRouteStorage creates a new RouteStorage with the given base directory
 19func NewRouteStorage(baseDir string) *RouteStorage {
 20	return &RouteStorage{baseDir: baseDir}
 21}
 22
 23// SaveRoute persists route GeoJSON to content-addressed storage.
 24// The routeID is the SHA256 hash of the JSON-encoded points.
 25// Files are stored at: {baseDir}/{hash[0:2]}/{hash[2:]}.geojson
 26func (rs *RouteStorage) SaveRoute(routeID string, points [][]float64, distance, routeTime float64, bbox []float64) error {
 27	slog.Debug("SaveRoute: starting",
 28		"routeID", routeID,
 29		"numPoints", len(points),
 30		"distance", distance,
 31		"time", routeTime,
 32		"bbox", bbox,
 33		"baseDir", rs.baseDir)
 34
 35	if len(routeID) < 3 {
 36		slog.Error("SaveRoute: invalid route ID",
 37			"routeIDLen", len(routeID))
 38		return fmt.Errorf("invalid route ID: must be at least 3 characters, got %d", len(routeID))
 39	}
 40
 41	dir := filepath.Join(rs.baseDir, routeID[0:2])
 42	slog.Debug("SaveRoute: creating directory",
 43		"dir", dir)
 44
 45	if err := os.MkdirAll(dir, 0755); err != nil {
 46		slog.Error("SaveRoute: failed to create directory",
 47			"dir", dir,
 48			"error", err)
 49		return fmt.Errorf("failed to create directory %s: %w", dir, err)
 50	}
 51
 52	geojsonPath := filepath.Join(dir, routeID[2:]+".geojson")
 53	slog.Debug("SaveRoute: preparing to write file",
 54		"path", geojsonPath)
 55
 56	feature := map[string]interface{}{
 57		"type": "Feature",
 58		"geometry": map[string]interface{}{
 59			"type":        "LineString",
 60			"coordinates": points,
 61		},
 62		"properties": map[string]interface{}{
 63			"id":       routeID,
 64			"distance": distance,
 65			"time":     routeTime,
 66			"bbox":     bbox,
 67		},
 68	}
 69
 70	geojsonData, err := json.Marshal(feature)
 71	if err != nil {
 72		slog.Error("SaveRoute: failed to marshal GeoJSON",
 73			"error", err)
 74		return fmt.Errorf("failed to marshal GeoJSON: %w", err)
 75	}
 76
 77	slog.Debug("SaveRoute: writing file",
 78		"path", geojsonPath,
 79		"dataSize", len(geojsonData))
 80
 81	if err := os.WriteFile(geojsonPath, geojsonData, 0644); err != nil {
 82		slog.Error("SaveRoute: failed to write file",
 83			"path", geojsonPath,
 84			"error", err)
 85		return fmt.Errorf("failed to write GeoJSON file: %w", err)
 86	}
 87
 88	slog.Debug("SaveRoute: completed successfully",
 89		"path", geojsonPath,
 90		"routeID", routeID)
 91
 92	return nil
 93}
 94
 95// SaveElevation persists elevation data to content-addressed storage.
 96// Elevation is stored as a JSON array at: {baseDir}/{hash[0:2]}/{hash[2:]}.elevation.json
 97func (rs *RouteStorage) SaveElevation(routeID string, elevation []float64) error {
 98	slog.Debug("SaveElevation: starting",
 99		"routeID", routeID,
100		"numElevationPoints", len(elevation))
101
102	if len(routeID) < 3 {
103		slog.Error("SaveElevation: invalid route ID",
104			"routeIDLen", len(routeID))
105		return fmt.Errorf("invalid route ID: must be at least 3 characters, got %d", len(routeID))
106	}
107
108	dir := filepath.Join(rs.baseDir, routeID[0:2])
109	slog.Debug("SaveElevation: creating directory",
110		"dir", dir)
111
112	if err := os.MkdirAll(dir, 0755); err != nil {
113		slog.Error("SaveElevation: failed to create directory",
114			"dir", dir,
115			"error", err)
116		return fmt.Errorf("failed to create directory %s: %w", dir, err)
117	}
118
119	elevationPath := filepath.Join(dir, routeID[2:]+".elevation.json")
120	slog.Debug("SaveElevation: preparing to write file",
121		"path", elevationPath)
122
123	elevationData, err := json.Marshal(elevation)
124	if err != nil {
125		slog.Error("SaveElevation: failed to marshal elevation data",
126			"error", err)
127		return fmt.Errorf("failed to marshal elevation data: %w", err)
128	}
129
130	slog.Debug("SaveElevation: writing file",
131		"path", elevationPath,
132		"dataSize", len(elevationData))
133
134	if err := os.WriteFile(elevationPath, elevationData, 0644); err != nil {
135		slog.Error("SaveElevation: failed to write file",
136			"path", elevationPath,
137			"error", err)
138		return fmt.Errorf("failed to write elevation file: %w", err)
139	}
140
141	slog.Debug("SaveElevation: completed successfully",
142		"path", elevationPath,
143		"routeID", routeID)
144
145	return nil
146}
147
148// HashPoints computes the SHA256 hash of JSON-encoded points and returns hex string
149func HashPoints(points [][]float64) string {
150	data, _ := json.Marshal(points)
151	hash := sha256.Sum256(data)
152	return hex.EncodeToString(hash[:])
153}