176 lines
5.1 KiB
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);
|
|
}
|