AI: Nominatim Geocoding
This commit is contained in:
parent
20ed4151ff
commit
a1815e88f9
4 changed files with 238 additions and 16 deletions
|
|
@ -6,7 +6,7 @@ pub const Config = struct {
|
|||
cache_size: usize,
|
||||
cache_dir: []const u8,
|
||||
geolite_path: []const u8,
|
||||
geolocator_url: []const u8,
|
||||
geocache_file: ?[]const u8,
|
||||
|
||||
pub fn load(allocator: std.mem.Allocator) !Config {
|
||||
return Config{
|
||||
|
|
@ -23,7 +23,7 @@ pub const Config = struct {
|
|||
},
|
||||
.cache_dir = std.process.getEnvVarOwned(allocator, "WTTR_CACHE_DIR") catch try allocator.dupe(u8, "/tmp/wttr-cache"),
|
||||
.geolite_path = std.process.getEnvVarOwned(allocator, "WTTR_GEOLITE_PATH") catch try allocator.dupe(u8, "./GeoLite2-City.mmdb"),
|
||||
.geolocator_url = std.process.getEnvVarOwned(allocator, "WTTR_GEOLOCATOR_URL") catch try allocator.dupe(u8, "http://localhost:8004"),
|
||||
.geocache_file = std.process.getEnvVarOwned(allocator, "WTTR_GEOCACHE_FILE") catch null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ pub const Config = struct {
|
|||
allocator.free(self.listen_host);
|
||||
allocator.free(self.cache_dir);
|
||||
allocator.free(self.geolite_path);
|
||||
allocator.free(self.geolocator_url);
|
||||
if (self.geocache_file) |f| allocator.free(f);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -44,5 +44,5 @@ test "config loads defaults" {
|
|||
try std.testing.expectEqual(@as(u16, 8002), cfg.listen_port);
|
||||
try std.testing.expectEqual(@as(usize, 10_000), cfg.cache_size);
|
||||
try std.testing.expectEqualStrings("./GeoLite2-City.mmdb", cfg.geolite_path);
|
||||
try std.testing.expectEqualStrings("http://localhost:8004", cfg.geolocator_url);
|
||||
try std.testing.expect(cfg.geocache_file == null);
|
||||
}
|
||||
|
|
|
|||
146
zig/src/location/geocache.zig
Normal file
146
zig/src/location/geocache.zig
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
const std = @import("std");
|
||||
|
||||
pub const GeoCache = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
cache: std.StringHashMap(CachedLocation),
|
||||
cache_file: ?[]const u8,
|
||||
|
||||
pub const CachedLocation = struct {
|
||||
name: []const u8,
|
||||
latitude: f64,
|
||||
longitude: f64,
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
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 {
|
||||
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),
|
||||
.latitude = location.latitude,
|
||||
.longitude = location.longitude,
|
||||
};
|
||||
try self.cache.put(key, value);
|
||||
}
|
||||
|
||||
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),
|
||||
.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.latitude,
|
||||
.longitude = entry.value_ptr.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",
|
||||
.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.?.latitude, 0.0001);
|
||||
try std.testing.expectApproxEqAbs(@as(f64, -0.1278), result.?.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);
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
const std = @import("std");
|
||||
const GeoIP = @import("geoip.zig").GeoIP;
|
||||
const GeoCache = @import("geocache.zig").GeoCache;
|
||||
|
||||
pub const Location = struct {
|
||||
name: []const u8,
|
||||
|
|
@ -18,13 +19,13 @@ pub const LocationType = enum {
|
|||
pub const Resolver = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
geoip: ?*GeoIP,
|
||||
geolocator_url: []const u8,
|
||||
geocache: *GeoCache,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, geoip: ?*GeoIP, geolocator_url: []const u8) Resolver {
|
||||
pub fn init(allocator: std.mem.Allocator, geoip: ?*GeoIP, geocache: *GeoCache) Resolver {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.geoip = geoip,
|
||||
.geolocator_url = geolocator_url,
|
||||
.geocache = geocache,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -81,11 +82,75 @@ pub const Resolver = struct {
|
|||
}
|
||||
|
||||
fn resolveGeocoded(self: *Resolver, name: []const u8) !Location {
|
||||
// TODO: Call external geocoding service
|
||||
// For now, return a placeholder error
|
||||
_ = self;
|
||||
_ = name;
|
||||
return error.GeocodingNotImplemented;
|
||||
// Check cache first
|
||||
if (self.geocache.get(name)) |cached| {
|
||||
return Location{
|
||||
.name = try self.allocator.dupe(u8, cached.name),
|
||||
.latitude = cached.latitude,
|
||||
.longitude = cached.longitude,
|
||||
};
|
||||
}
|
||||
|
||||
// Call Nominatim API
|
||||
const url = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"https://nominatim.openstreetmap.org/search?q={s}&format=json&limit=1",
|
||||
.{name},
|
||||
);
|
||||
defer self.allocator.free(url);
|
||||
|
||||
var client = std.http.Client{ .allocator = self.allocator };
|
||||
defer client.deinit();
|
||||
|
||||
const uri = try std.Uri.parse(url);
|
||||
|
||||
var response_buf: [1024 * 1024]u8 = undefined;
|
||||
var writer = std.Io.Writer.fixed(&response_buf);
|
||||
const result = try client.fetch(.{
|
||||
.location = .{ .uri = uri },
|
||||
.method = .GET,
|
||||
.response_writer = &writer,
|
||||
.extra_headers = &.{
|
||||
.{ .name = "User-Agent", .value = "wttr.in-zig/1.0" },
|
||||
},
|
||||
});
|
||||
|
||||
if (result.status != .ok) {
|
||||
return error.GeocodingFailed;
|
||||
}
|
||||
|
||||
const response_body = response_buf[0..writer.end];
|
||||
|
||||
// Parse JSON response
|
||||
const parsed = try std.json.parseFromSlice(
|
||||
std.json.Value,
|
||||
self.allocator,
|
||||
response_body,
|
||||
.{},
|
||||
);
|
||||
defer parsed.deinit();
|
||||
|
||||
if (parsed.value.array.items.len == 0) {
|
||||
return error.LocationNotFound;
|
||||
}
|
||||
|
||||
const first = parsed.value.array.items[0];
|
||||
const display_name = first.object.get("display_name").?.string;
|
||||
const lat = try std.fmt.parseFloat(f64, first.object.get("lat").?.string);
|
||||
const lon = try std.fmt.parseFloat(f64, first.object.get("lon").?.string);
|
||||
|
||||
// Cache the result
|
||||
try self.geocache.put(name, .{
|
||||
.name = display_name,
|
||||
.latitude = lat,
|
||||
.longitude = lon,
|
||||
});
|
||||
|
||||
return Location{
|
||||
.name = try self.allocator.dupe(u8, display_name),
|
||||
.latitude = lat,
|
||||
.longitude = lon,
|
||||
};
|
||||
}
|
||||
|
||||
fn resolveAirport(self: *Resolver, code: []const u8) !Location {
|
||||
|
|
@ -129,7 +194,8 @@ test "detect location type" {
|
|||
|
||||
test "resolver init" {
|
||||
const allocator = std.testing.allocator;
|
||||
const resolver = Resolver.init(allocator, null, "http://localhost:8004");
|
||||
var geocache = try GeoCache.init(allocator, null);
|
||||
defer geocache.deinit();
|
||||
const resolver = Resolver.init(allocator, null, &geocache);
|
||||
try std.testing.expect(resolver.geoip == null);
|
||||
try std.testing.expectEqualStrings("http://localhost:8004", resolver.geolocator_url);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const types = @import("weather/types.zig");
|
|||
const Server = @import("http/server.zig").Server;
|
||||
const RateLimiter = @import("http/rate_limiter.zig").RateLimiter;
|
||||
const GeoIP = @import("location/geoip.zig").GeoIP;
|
||||
const GeoCache = @import("location/geocache.zig").GeoCache;
|
||||
const Resolver = @import("location/resolver.zig").Resolver;
|
||||
|
||||
pub const std_options: std.Options = .{
|
||||
|
|
@ -28,7 +29,11 @@ pub fn main() !void {
|
|||
try stdout.print("Cache size: {d}\n", .{cfg.cache_size});
|
||||
try stdout.print("Cache dir: {s}\n", .{cfg.cache_dir});
|
||||
try stdout.print("GeoLite2 path: {s}\n", .{cfg.geolite_path});
|
||||
try stdout.print("Geolocator URL: {s}\n", .{cfg.geolocator_url});
|
||||
if (cfg.geocache_file) |f| {
|
||||
try stdout.print("Geocache file: {s}\n", .{f});
|
||||
} else {
|
||||
try stdout.print("Geocache: in-memory only\n", .{});
|
||||
}
|
||||
try stdout.flush();
|
||||
|
||||
// Initialize GeoIP database
|
||||
|
|
@ -39,8 +44,12 @@ pub fn main() !void {
|
|||
};
|
||||
defer geoip.deinit();
|
||||
|
||||
// Initialize geocoding cache
|
||||
var geocache = try GeoCache.init(allocator, cfg.geocache_file);
|
||||
defer geocache.deinit();
|
||||
|
||||
// Initialize location resolver
|
||||
var resolver = Resolver.init(allocator, &geoip, cfg.geolocator_url);
|
||||
var resolver = Resolver.init(allocator, &geoip, &geocache);
|
||||
|
||||
var cache = try Cache.init(allocator, .{
|
||||
.max_entries = cfg.cache_size,
|
||||
|
|
@ -80,5 +89,6 @@ test {
|
|||
_ = @import("render/v2.zig");
|
||||
_ = @import("render/custom.zig");
|
||||
_ = @import("location/geoip.zig");
|
||||
_ = @import("location/geocache.zig");
|
||||
_ = @import("location/resolver.zig");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue