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
|
- Based on location coordinates and timezone
|
||||||
- Display in custom format output
|
- Display in custom format output
|
||||||
|
|
||||||
7. **Airport Database**
|
|
||||||
- IATA airport code to coordinates mapping
|
|
||||||
- Use dedicated airport data instead of geocoding service
|
|
||||||
|
|
||||||
## Documentation Files
|
## Documentation Files
|
||||||
|
|
||||||
### [TARGET_ARCHITECTURE.md](TARGET_ARCHITECTURE.md) ⭐ NEW
|
### [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",
|
.url = "https://github.com/maxmind/libmaxminddb/archive/refs/tags/1.11.0.tar.gz",
|
||||||
.hash = "N-V-__8AAAYyBQCd9x7qVVFKQIxi01UZ1K8ZFZFfTzj99CvX",
|
.hash = "N-V-__8AAAYyBQCd9x7qVVFKQIxi01UZ1K8ZFZFfTzj99CvX",
|
||||||
},
|
},
|
||||||
|
.openflights = .{
|
||||||
|
.url = "https://github.com/jpatokal/openflights/archive/refs/heads/master.tar.gz",
|
||||||
|
.hash = "N-V-__8AAKQtFgNwqtlYfjcAZQB5M1Vqc6ZPqjHkEaMHsJoT",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
.fingerprint = 0x710c2b57e81aa678,
|
.fingerprint = 0x710c2b57e81aa678,
|
||||||
.minimum_zig_version = "0.15.0",
|
.minimum_zig_version = "0.15.0",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ pub const Config = struct {
|
||||||
cache_dir: []const u8,
|
cache_dir: []const u8,
|
||||||
geolite_path: []const u8,
|
geolite_path: []const u8,
|
||||||
geocache_file: ?[]const u8,
|
geocache_file: ?[]const u8,
|
||||||
|
airports_dat_path: ?[]const u8,
|
||||||
|
|
||||||
pub fn load(allocator: std.mem.Allocator) !Config {
|
pub fn load(allocator: std.mem.Allocator) !Config {
|
||||||
return 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"),
|
.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"),
|
.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,
|
.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.cache_dir);
|
||||||
allocator.free(self.geolite_path);
|
allocator.free(self.geolite_path);
|
||||||
if (self.geocache_file) |f| allocator.free(f);
|
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.expectEqual(@as(usize, 10_000), cfg.cache_size);
|
||||||
try std.testing.expectEqualStrings("./GeoLite2-City.mmdb", cfg.geolite_path);
|
try std.testing.expectEqualStrings("./GeoLite2-City.mmdb", cfg.geolite_path);
|
||||||
try std.testing.expect(cfg.geocache_file == null);
|
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 std = @import("std");
|
||||||
const GeoIP = @import("geoip.zig").GeoIP;
|
const GeoIP = @import("geoip.zig").GeoIP;
|
||||||
const GeoCache = @import("geocache.zig").GeoCache;
|
const GeoCache = @import("geocache.zig").GeoCache;
|
||||||
|
const AirportDB = @import("airports.zig").AirportDB;
|
||||||
|
|
||||||
pub const Location = struct {
|
pub const Location = struct {
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
|
|
@ -20,12 +21,14 @@ pub const Resolver = struct {
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
geoip: ?*GeoIP,
|
geoip: ?*GeoIP,
|
||||||
geocache: *GeoCache,
|
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 .{
|
return .{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.geoip = geoip,
|
.geoip = geoip,
|
||||||
.geocache = geocache,
|
.geocache = geocache,
|
||||||
|
.airports = airports,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,7 +157,24 @@ pub const Resolver = struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolveAirport(self: *Resolver, code: []const u8) !Location {
|
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);
|
return self.resolveGeocoded(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,6 +216,7 @@ test "resolver init" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
var geocache = try GeoCache.init(allocator, null);
|
var geocache = try GeoCache.init(allocator, null);
|
||||||
defer geocache.deinit();
|
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.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 RateLimiter = @import("http/rate_limiter.zig").RateLimiter;
|
||||||
const GeoIP = @import("location/geoip.zig").GeoIP;
|
const GeoIP = @import("location/geoip.zig").GeoIP;
|
||||||
const GeoCache = @import("location/geocache.zig").GeoCache;
|
const GeoCache = @import("location/geocache.zig").GeoCache;
|
||||||
|
const AirportDB = @import("location/airports.zig").AirportDB;
|
||||||
const Resolver = @import("location/resolver.zig").Resolver;
|
const Resolver = @import("location/resolver.zig").Resolver;
|
||||||
|
|
||||||
pub const std_options: std.Options = .{
|
pub const std_options: std.Options = .{
|
||||||
|
|
@ -34,6 +35,9 @@ pub fn main() !void {
|
||||||
} else {
|
} else {
|
||||||
try stdout.print("Geocache: in-memory only\n", .{});
|
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();
|
try stdout.flush();
|
||||||
|
|
||||||
// Initialize GeoIP database
|
// Initialize GeoIP database
|
||||||
|
|
@ -48,8 +52,20 @@ pub fn main() !void {
|
||||||
var geocache = try GeoCache.init(allocator, cfg.geocache_file);
|
var geocache = try GeoCache.init(allocator, cfg.geocache_file);
|
||||||
defer geocache.deinit();
|
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
|
// 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, .{
|
var cache = try Cache.init(allocator, .{
|
||||||
.max_entries = cfg.cache_size,
|
.max_entries = cfg.cache_size,
|
||||||
|
|
@ -90,5 +106,6 @@ test {
|
||||||
_ = @import("render/custom.zig");
|
_ = @import("render/custom.zig");
|
||||||
_ = @import("location/geoip.zig");
|
_ = @import("location/geoip.zig");
|
||||||
_ = @import("location/geocache.zig");
|
_ = @import("location/geocache.zig");
|
||||||
|
_ = @import("location/airports.zig");
|
||||||
_ = @import("location/resolver.zig");
|
_ = @import("location/resolver.zig");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue