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}