wttr/src/location/GeoCache.zig

176 lines
5.1 KiB
Zig

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);
}