const std = @import("std"); const Coordinates = @import("../Coordinates.zig"); const GeoCache = @This(); allocator: std.mem.Allocator, cache: std.StringHashMap(CachedLocation), cache_file: ?[]const u8, dirty: bool, last_save: i64, pub const CachedLocation = struct { name: []const u8, coords: Coordinates, }; pub fn init(allocator: std.mem.Allocator, cache_file: ?[]const u8) !GeoCache { var cache = std.StringHashMap(CachedLocation).init(allocator); // Load from file if specified if (cache_file) |file_path| { loadFromFile(allocator, &cache, file_path) catch |err| { std.log.warn("Failed to load geocoding cache from {s}: {}", .{ file_path, err }); }; } return GeoCache{ .allocator = allocator, .cache = cache, .cache_file = if (cache_file) |f| try allocator.dupe(u8, f) else null, .dirty = false, .last_save = std.time.milliTimestamp(), }; } pub fn deinit(self: *GeoCache) void { // Save to file if specified if (self.cache_file) |file_path| { self.saveToFile(file_path) catch |err| { std.log.warn("Failed to save geocoding cache to {s}: {}", .{ file_path, err }); }; } var it = self.cache.iterator(); while (it.next()) |entry| { self.allocator.free(entry.key_ptr.*); self.allocator.free(entry.value_ptr.name); } self.cache.deinit(); if (self.cache_file) |f| self.allocator.free(f); } pub fn get(self: *GeoCache, query: []const u8) ?CachedLocation { self.saveIfNeeded(); return self.cache.get(query); } pub fn put(self: *GeoCache, query: []const u8, location: CachedLocation) !void { const key = try self.allocator.dupe(u8, query); const value = CachedLocation{ .name = try self.allocator.dupe(u8, location.name), .coords = location.coords, }; try self.cache.put(key, value); self.dirty = true; } /// Save cache to disk if dirty and enough time has passed (15 minutes) pub fn saveIfNeeded(self: *GeoCache) void { if (!self.dirty) return; const cache_file = self.cache_file orelse return; const now = std.time.milliTimestamp(); const elapsed_ms = now - self.last_save; const fifteen_minutes_ms = 15 * std.time.ms_per_min; if (elapsed_ms < fifteen_minutes_ms) return; self.saveToFile(cache_file) catch |err| { std.log.warn("Failed to save geocoding cache to {s}: {}", .{ cache_file, err }); return; }; self.dirty = false; self.last_save = now; } fn loadFromFile(allocator: std.mem.Allocator, cache: *std.StringHashMap(CachedLocation), file_path: []const u8) !void { const file = try std.fs.cwd().openFile(file_path, .{}); defer file.close(); const content = try file.readToEndAlloc(allocator, 10 * 1024 * 1024); // 10MB max defer allocator.free(content); const parsed = try std.json.parseFromSlice( std.json.Value, allocator, content, .{}, ); defer parsed.deinit(); var it = parsed.value.object.iterator(); while (it.next()) |entry| { const obj = entry.value_ptr.object; const key = try allocator.dupe(u8, entry.key_ptr.*); const value = CachedLocation{ .name = try allocator.dupe(u8, obj.get("name").?.string), .coords = .{ .latitude = obj.get("latitude").?.float, .longitude = obj.get("longitude").?.float, }, }; try cache.put(key, value); } } fn saveToFile(self: *GeoCache, file_path: []const u8) !void { const file = try std.fs.cwd().createFile(file_path, .{}); defer file.close(); var buffer: [4096]u8 = undefined; var file_writer = file.writer(&buffer); const writer = &file_writer.interface; try writer.writeAll("{\n"); var it = self.cache.iterator(); var first = true; while (it.next()) |entry| { if (!first) try writer.writeAll(",\n"); first = false; try writer.print(" {any}: {any}", .{ std.json.fmt(entry.key_ptr.*, .{}), std.json.fmt(.{ .name = entry.value_ptr.name, .latitude = entry.value_ptr.coords.latitude, .longitude = entry.value_ptr.coords.longitude, }, .{}), }); } try writer.writeAll("\n}\n"); try writer.flush(); } test "GeoCache basic operations" { const allocator = std.testing.allocator; var cache = try GeoCache.init(allocator, null); defer cache.deinit(); // Test put and get try cache.put("London", .{ .name = "London, UK", .coords = .{ .latitude = 51.5074, .longitude = -0.1278, }, }); const result = cache.get("London"); try std.testing.expect(result != null); try std.testing.expectApproxEqAbs(@as(f64, 51.5074), result.?.coords.latitude, 0.0001); try std.testing.expectApproxEqAbs(@as(f64, -0.1278), result.?.coords.longitude, 0.0001); } test "GeoCache miss returns null" { const allocator = std.testing.allocator; var cache = try GeoCache.init(allocator, null); defer cache.deinit(); const result = cache.get("NonExistent"); try std.testing.expect(result == null); }