AI: Add IATA resolution
This commit is contained in:
parent
a1815e88f9
commit
3a7d65eeb7
6 changed files with 181 additions and 8 deletions
|
|
@ -46,10 +46,6 @@ This directory contains comprehensive documentation for rewriting wttr.in in Zig
|
|||
- Based on location coordinates and timezone
|
||||
- Display in custom format output
|
||||
|
||||
7. **Airport Database**
|
||||
- IATA airport code to coordinates mapping
|
||||
- Use dedicated airport data instead of geocoding service
|
||||
|
||||
## Documentation Files
|
||||
|
||||
### [TARGET_ARCHITECTURE.md](TARGET_ARCHITECTURE.md) ⭐ NEW
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@
|
|||
.url = "https://github.com/maxmind/libmaxminddb/archive/refs/tags/1.11.0.tar.gz",
|
||||
.hash = "N-V-__8AAAYyBQCd9x7qVVFKQIxi01UZ1K8ZFZFfTzj99CvX",
|
||||
},
|
||||
.openflights = .{
|
||||
.url = "https://github.com/jpatokal/openflights/archive/refs/heads/master.tar.gz",
|
||||
.hash = "N-V-__8AAKQtFgNwqtlYfjcAZQB5M1Vqc6ZPqjHkEaMHsJoT",
|
||||
},
|
||||
},
|
||||
.fingerprint = 0x710c2b57e81aa678,
|
||||
.minimum_zig_version = "0.15.0",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ pub const Config = struct {
|
|||
cache_dir: []const u8,
|
||||
geolite_path: []const u8,
|
||||
geocache_file: ?[]const u8,
|
||||
airports_dat_path: ?[]const u8,
|
||||
|
||||
pub fn load(allocator: std.mem.Allocator) !Config {
|
||||
return Config{
|
||||
|
|
@ -24,6 +25,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"),
|
||||
.geocache_file = std.process.getEnvVarOwned(allocator, "WTTR_GEOCACHE_FILE") catch null,
|
||||
.airports_dat_path = std.process.getEnvVarOwned(allocator, "WTTR_AIRPORTS_DAT") catch null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -32,6 +34,7 @@ pub const Config = struct {
|
|||
allocator.free(self.cache_dir);
|
||||
allocator.free(self.geolite_path);
|
||||
if (self.geocache_file) |f| allocator.free(f);
|
||||
if (self.airports_dat_path) |f| allocator.free(f);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -45,4 +48,5 @@ test "config loads defaults" {
|
|||
try std.testing.expectEqual(@as(usize, 10_000), cfg.cache_size);
|
||||
try std.testing.expectEqualStrings("./GeoLite2-City.mmdb", cfg.geolite_path);
|
||||
try std.testing.expect(cfg.geocache_file == null);
|
||||
try std.testing.expect(cfg.airports_dat_path == null);
|
||||
}
|
||||
|
|
|
|||
131
zig/src/location/airports.zig
Normal file
131
zig/src/location/airports.zig
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
const std = @import("std");
|
||||
|
||||
pub const Airport = struct {
|
||||
iata: []const u8,
|
||||
name: []const u8,
|
||||
latitude: f64,
|
||||
longitude: f64,
|
||||
};
|
||||
|
||||
pub const AirportDB = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
airports: std.StringHashMap(Airport),
|
||||
|
||||
pub fn initFromFile(allocator: std.mem.Allocator, file_path: []const u8) !AirportDB {
|
||||
const file = try std.fs.cwd().openFile(file_path, .{});
|
||||
defer file.close();
|
||||
|
||||
const csv_data = try file.readToEndAlloc(allocator, 10 * 1024 * 1024); // 10MB max
|
||||
defer allocator.free(csv_data);
|
||||
|
||||
return try init(allocator, csv_data);
|
||||
}
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, csv_data: []const u8) !AirportDB {
|
||||
var airports = std.StringHashMap(Airport).init(allocator);
|
||||
|
||||
var lines = std.mem.splitScalar(u8, csv_data, '\n');
|
||||
while (lines.next()) |line| {
|
||||
if (line.len == 0) continue;
|
||||
|
||||
const airport = parseAirportLine(allocator, line) catch continue;
|
||||
if (airport.iata.len == 3) {
|
||||
try airports.put(airport.iata, airport);
|
||||
}
|
||||
}
|
||||
|
||||
return AirportDB{
|
||||
.allocator = allocator,
|
||||
.airports = airports,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *AirportDB) void {
|
||||
var it = self.airports.iterator();
|
||||
while (it.next()) |entry| {
|
||||
self.allocator.free(entry.key_ptr.*);
|
||||
self.allocator.free(entry.value_ptr.name);
|
||||
}
|
||||
self.airports.deinit();
|
||||
}
|
||||
|
||||
pub fn lookup(self: *AirportDB, iata_code: []const u8) ?Airport {
|
||||
return self.airports.get(iata_code);
|
||||
}
|
||||
|
||||
fn parseAirportLine(allocator: std.mem.Allocator, line: []const u8) !Airport {
|
||||
// CSV format: ID,Name,City,Country,IATA,ICAO,Lat,Lon,...
|
||||
var fields = std.mem.splitScalar(u8, line, ',');
|
||||
|
||||
_ = fields.next() orelse return error.InvalidFormat; // ID
|
||||
const name_quoted = fields.next() orelse return error.InvalidFormat; // Name
|
||||
_ = fields.next() orelse return error.InvalidFormat; // City
|
||||
_ = fields.next() orelse return error.InvalidFormat; // Country
|
||||
const iata_quoted = fields.next() orelse return error.InvalidFormat; // IATA
|
||||
_ = fields.next() orelse return error.InvalidFormat; // ICAO
|
||||
const lat_str = fields.next() orelse return error.InvalidFormat; // Lat
|
||||
const lon_str = fields.next() orelse return error.InvalidFormat; // Lon
|
||||
|
||||
// Remove quotes from fields
|
||||
const name = try unquote(allocator, name_quoted);
|
||||
const iata = try unquote(allocator, iata_quoted);
|
||||
|
||||
// Skip if IATA is "\\N" (null)
|
||||
if (std.mem.eql(u8, iata, "\\N")) {
|
||||
allocator.free(name);
|
||||
allocator.free(iata);
|
||||
return error.NoIATA;
|
||||
}
|
||||
|
||||
const lat = try std.fmt.parseFloat(f64, lat_str);
|
||||
const lon = try std.fmt.parseFloat(f64, lon_str);
|
||||
|
||||
return Airport{
|
||||
.iata = iata,
|
||||
.name = name,
|
||||
.latitude = lat,
|
||||
.longitude = lon,
|
||||
};
|
||||
}
|
||||
|
||||
fn unquote(allocator: std.mem.Allocator, quoted: []const u8) ![]const u8 {
|
||||
if (quoted.len >= 2 and quoted[0] == '"' and quoted[quoted.len - 1] == '"') {
|
||||
return allocator.dupe(u8, quoted[1 .. quoted.len - 1]);
|
||||
}
|
||||
return allocator.dupe(u8, quoted);
|
||||
}
|
||||
};
|
||||
|
||||
test "parseAirportLine valid" {
|
||||
const allocator = std.testing.allocator;
|
||||
const line = "1,\"Goroka Airport\",\"Goroka\",\"Papua New Guinea\",\"GKA\",\"AYGA\",-6.081689834590001,145.391998291,5282,10,\"U\",\"Pacific/Port_Moresby\",\"airport\",\"OurAirports\"";
|
||||
|
||||
const airport = try AirportDB.parseAirportLine(allocator, line);
|
||||
defer allocator.free(airport.iata);
|
||||
defer allocator.free(airport.name);
|
||||
|
||||
try std.testing.expectEqualStrings("GKA", airport.iata);
|
||||
try std.testing.expectEqualStrings("Goroka Airport", airport.name);
|
||||
try std.testing.expectApproxEqAbs(@as(f64, -6.081689834590001), airport.latitude, 0.0001);
|
||||
try std.testing.expectApproxEqAbs(@as(f64, 145.391998291), airport.longitude, 0.0001);
|
||||
}
|
||||
|
||||
test "parseAirportLine with null IATA" {
|
||||
const allocator = std.testing.allocator;
|
||||
const line = "1,\"Test Airport\",\"City\",\"Country\",\"\\N\",\"ICAO\",0.0,0.0";
|
||||
|
||||
try std.testing.expectError(error.NoIATA, AirportDB.parseAirportLine(allocator, line));
|
||||
}
|
||||
|
||||
test "AirportDB lookup" {
|
||||
const allocator = std.testing.allocator;
|
||||
const csv = "1,\"Munich Airport\",\"Munich\",\"Germany\",\"MUC\",\"EDDM\",48.353802,11.7861,1487,1,\"E\",\"Europe/Berlin\",\"airport\",\"OurAirports\"";
|
||||
|
||||
var db = try AirportDB.init(allocator, csv);
|
||||
defer db.deinit();
|
||||
|
||||
const result = db.lookup("MUC");
|
||||
try std.testing.expect(result != null);
|
||||
try std.testing.expectEqualStrings("Munich Airport", result.?.name);
|
||||
try std.testing.expectApproxEqAbs(@as(f64, 48.353802), result.?.latitude, 0.0001);
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
const std = @import("std");
|
||||
const GeoIP = @import("geoip.zig").GeoIP;
|
||||
const GeoCache = @import("geocache.zig").GeoCache;
|
||||
const AirportDB = @import("airports.zig").AirportDB;
|
||||
|
||||
pub const Location = struct {
|
||||
name: []const u8,
|
||||
|
|
@ -20,12 +21,14 @@ pub const Resolver = struct {
|
|||
allocator: std.mem.Allocator,
|
||||
geoip: ?*GeoIP,
|
||||
geocache: *GeoCache,
|
||||
airports: ?*AirportDB,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, geoip: ?*GeoIP, geocache: *GeoCache) Resolver {
|
||||
pub fn init(allocator: std.mem.Allocator, geoip: ?*GeoIP, geocache: *GeoCache, airports: ?*AirportDB) Resolver {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.geoip = geoip,
|
||||
.geocache = geocache,
|
||||
.airports = airports,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -154,7 +157,24 @@ pub const Resolver = struct {
|
|||
}
|
||||
|
||||
fn resolveAirport(self: *Resolver, code: []const u8) !Location {
|
||||
// For now, treat as geocoded location
|
||||
// Try airport database first
|
||||
if (self.airports) |airports| {
|
||||
// Convert to uppercase for lookup
|
||||
var upper_code: [3]u8 = undefined;
|
||||
for (code, 0..) |c, i| {
|
||||
upper_code[i] = std.ascii.toUpper(c);
|
||||
}
|
||||
|
||||
if (airports.lookup(&upper_code)) |airport| {
|
||||
return Location{
|
||||
.name = try self.allocator.dupe(u8, airport.name),
|
||||
.latitude = airport.latitude,
|
||||
.longitude = airport.longitude,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to geocoding
|
||||
return self.resolveGeocoded(code);
|
||||
}
|
||||
|
||||
|
|
@ -196,6 +216,7 @@ test "resolver init" {
|
|||
const allocator = std.testing.allocator;
|
||||
var geocache = try GeoCache.init(allocator, null);
|
||||
defer geocache.deinit();
|
||||
const resolver = Resolver.init(allocator, null, &geocache);
|
||||
const resolver = Resolver.init(allocator, null, &geocache, null);
|
||||
try std.testing.expect(resolver.geoip == null);
|
||||
try std.testing.expect(resolver.airports == null);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ 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 AirportDB = @import("location/airports.zig").AirportDB;
|
||||
const Resolver = @import("location/resolver.zig").Resolver;
|
||||
|
||||
pub const std_options: std.Options = .{
|
||||
|
|
@ -34,6 +35,9 @@ pub fn main() !void {
|
|||
} else {
|
||||
try stdout.print("Geocache: in-memory only\n", .{});
|
||||
}
|
||||
if (cfg.airports_dat_path) |f| {
|
||||
try stdout.print("Airports database: {s}\n", .{f});
|
||||
}
|
||||
try stdout.flush();
|
||||
|
||||
// Initialize GeoIP database
|
||||
|
|
@ -48,8 +52,20 @@ pub fn main() !void {
|
|||
var geocache = try GeoCache.init(allocator, cfg.geocache_file);
|
||||
defer geocache.deinit();
|
||||
|
||||
// Initialize airports database
|
||||
var airports_db: ?AirportDB = null;
|
||||
if (cfg.airports_dat_path) |path| {
|
||||
airports_db = AirportDB.initFromFile(allocator, path) catch |err| blk: {
|
||||
std.log.warn("Failed to load airports database: {}", .{err});
|
||||
break :blk null;
|
||||
};
|
||||
}
|
||||
if (airports_db) |*db| {
|
||||
defer db.deinit();
|
||||
}
|
||||
|
||||
// Initialize location resolver
|
||||
var resolver = Resolver.init(allocator, &geoip, &geocache);
|
||||
var resolver = Resolver.init(allocator, &geoip, &geocache, if (airports_db) |*db| db else null);
|
||||
|
||||
var cache = try Cache.init(allocator, .{
|
||||
.max_entries = cfg.cache_size,
|
||||
|
|
@ -90,5 +106,6 @@ test {
|
|||
_ = @import("render/custom.zig");
|
||||
_ = @import("location/geoip.zig");
|
||||
_ = @import("location/geocache.zig");
|
||||
_ = @import("location/airports.zig");
|
||||
_ = @import("location/resolver.zig");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue