Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions include/tile57.h
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,21 @@ typedef struct {
tile57_status tile57_chart_query(tile57_chart *chart, double lon, double lat, double zoom,
const tile57_query_cb *cb, tile57_error *err);

/* The decoded pick report for one queried feature. Compose it from the class,
* the cell name, and the attribute JSON of a query callback. *out is JSON
* (free with tile57_free):
* {"title","subtitle","chip","notes":[...],
* "rows":[{"label","value","depth","file","picture"}...],
* "footnote","empty":"none"|"source"}
* The title is the object's key fact (a light's characteristic, a depth
* area's range). The rows are decoded through the S-57 and S-101 catalogues.
* A value that no catalogue contains passes through unchanged. The "empty"
* field appears only when there is nothing to read. */
tile57_status tile57_s57_report(const char *cls, size_t cls_len,
const char *cell, size_t cell_len,
const char *attrs, size_t attrs_len,
uint8_t **out, size_t *out_len, tile57_error *err);

/* ---- mariner settings ------------------------------------------------------
*
* The mariner's S-52 display options: shared by the view renderers below
Expand Down
12 changes: 12 additions & 0 deletions src/capi.zig
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,18 @@ export fn tile57_chart_tile(handle: ?*Chart, z: u8, x: u32, y: u32, out: ?*?[*]u
return exportOut(err, o, n, bytes);
}

/// The decoded pick report for one queried feature, as JSON: title, rows,
/// notes and provenance, composed from the class, the cell name and the
/// attribute payload of a query callback. See tile57.h.
export fn tile57_s57_report(cls: ?[*]const u8, cls_len: usize, cell: ?[*]const u8, cell_len: usize, attrs: ?[*]const u8, attrs_len: usize, out: ?*?[*]u8, out_len: ?*usize, err: ?*CError) callconv(.c) c_int {
const o, const n = bytesOut(out, out_len) catch return failWith(err, .badarg, bad_out);
const c = cls orelse return failWith(err, .badarg, "cls must not be null");
const ch = cell orelse return failWith(err, .badarg, "cell must not be null");
const s = attrs orelse return failWith(err, .badarg, "attrs must not be null");
const bytes = s57.decode.report(gpa, c[0..cls_len], ch[0..cell_len], s[0..attrs_len]) catch |e| return fail(err, e);
return exportOut(err, o, n, bytes);
}

const CQueryCb = @import("render").query.QueryCb;

/// Cursor object-query at (lon,lat) for the view `zoom` (web-mercator): invokes
Expand Down
1,360 changes: 1,360 additions & 0 deletions src/s57/catalog.zig

Large diffs are not rendered by default.

1,600 changes: 1,600 additions & 0 deletions src/s57/catalog_s101.zig

Large diffs are not rendered by default.

667 changes: 667 additions & 0 deletions src/s57/decode.zig

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions src/s57/s57.zig
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ const Allocator = std.mem.Allocator;
/// that want the raw records. It is its own module (`@import("iso8211")`), so a
/// caller can depend on the 8211 layer without pulling in S-57 semantics.
pub const iso8211 = @import("iso8211");
/// The attribute catalogues as data, and the pick-report decode over
/// them. Every shell reads the same decode.
pub const catalog = @import("catalog.zig");
pub const catalog_s101 = @import("catalog_s101.zig");
pub const decode = @import("decode.zig");
const iso = iso8211;

// Geographic coordinate stored in S-57's native integer ×1e7 units (lon ±1.8e9,
Expand Down Expand Up @@ -2473,4 +2478,5 @@ test "pointInRings: inside, outside, and inside a hole" {

test {
_ = iso8211;
_ = decode;
}
236 changes: 236 additions & 0 deletions tools/fcattrs.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
const std = @import("std");

// Emit src/s57/catalog_s101.zig from the vendored S-101 Feature Catalogue
// XML: every simple attribute's label and, for enumeration types, its
// listed values, keyed by the S-101 code (beaconShape). The tool does not
// emit enum values under the S-57 aliases. S-101 changed attributes that
// it took from S-57 (CATTRK became a boolean; BUISHP keeps five shapes),
// so an S-57 code must not read the S-101 tables. catalog.zig stays the
// authority for S-57 acronyms. The scan uses the FC's fixed element order
// instead of an XML parser. A malformed block is skipped whole.
pub fn run(io: std.Io, a: std.mem.Allocator, args: []const [:0]const u8) !void {
if (args.len < 3) {
std.debug.print("usage: tile57 fcattrs <FeatureCatalogue.xml> [> catalog.zig]\n", .{});
return;
}
const xml = try std.Io.Dir.cwd().readFileAlloc(io, args[2], a, .unlimited);

var out = std.ArrayList(u8).empty;
try out.appendSlice(a,
\\//! The S-101 Feature Catalogue's simple attributes, as data.
\\//!
\\//! GENERATED by `tile57 fcattrs` from the vendored Feature Catalogue —
\\//! do not edit by hand; regenerate instead. Keys are S-101 attribute
\\//! codes; the S-57 catalogue is its own authority in catalog.zig. A
\\//! code the catalogue does not list stays undecoded and passes
\\//! through a report raw.
\\
\\const catalog = @import("catalog.zig");
\\
\\/// S-101 attribute code -> the label a mariner reads.
\\pub const attribute_names = [_]catalog.AttrName{
\\
);

var enums = std.ArrayList(u8).empty;
var n_attrs: usize = 0;
var n_vals: usize = 0;

var rest: []const u8 = xml;
while (indexAfter(rest, "<S100FC:S100_FC_SimpleAttribute>")) |start| {
const end = std.mem.indexOf(u8, rest[start..], "</S100FC:S100_FC_SimpleAttribute>") orelse break;
const block = rest[start .. start + end];
rest = rest[start + end ..];

// The attribute's own name/code/alias come before its listed values.
const head = block[0 .. std.mem.indexOf(u8, block, "<S100FC:listedValues>") orelse block.len];
const name = element(head, "S100FC:name") orelse continue;
const code = element(head, "S100FC:code") orelse continue;
const alias = element(head, "S100FC:alias");

_ = alias;
try emitName(a, &out, code, name);
n_attrs += 1;

// Every listed value: label, then its own code. definitionReference
// carries an unrelated sourceIdentifier, so the label anchors the
// scan and the code is taken from between the two.
var vals: []const u8 = block[head.len..];
while (indexAfter(vals, "<S100FC:listedValue>")) |vs| {
const ve = std.mem.indexOf(u8, vals[vs..], "</S100FC:listedValue>") orelse break;
const vblock = vals[vs .. vs + ve];
vals = vals[vs + ve ..];
const label = element(vblock, "S100FC:label") orelse continue;
const vcode_text = element(vblock, "S100FC:code") orelse continue;
const vcode = std.fmt.parseInt(u16, vcode_text, 10) catch continue;
try emitValue(a, &enums, code, vcode, label);
n_vals += 1;
}
}

try out.appendSlice(a,
\\};
\\
\\/// Every enumerated S-101 attribute's listed values, one per code.
\\pub const enum_values = [_]catalog.EnumValue{
\\
);
try out.appendSlice(a, enums.items);
try out.appendSlice(a, "};\n");

// Feature types: the object-class names. Emit the S-101 code always.
// Emit the S-57 alias only when exactly one feature type claims it and
// it has the shape of an S-57 acronym. S-101 splits some S-57 classes
// into several features (CTNARE becomes Caution Area and Discoloured
// Water), and an alias with several claims does not name one class.
try out.appendSlice(a,
\\
\\/// Object class (S-57 acronym or S-101 code) -> the chart's name.
\\pub const class_names = [_]catalog.AttrName{
\\
);
var n_classes: usize = 0;
// First pass: count the claims on each alias.
var claims = std.StringHashMap(usize).init(a);
var crest: []const u8 = xml;
while (indexAfter(crest, "<S100FC:S100_FC_FeatureType ")) |start| {
const end = std.mem.indexOf(u8, crest[start..], "</S100FC:S100_FC_FeatureType>") orelse break;
const head = crest[start .. start + (std.mem.indexOf(u8, crest[start .. start + end], "<S100FC:attributeBinding") orelse end)];
crest = crest[start + end ..];
if (element(head, "S100FC:alias")) |al| {
const g = try claims.getOrPut(al);
if (!g.found_existing) g.value_ptr.* = 0;
g.value_ptr.* += 1;
}
}
var frest: []const u8 = xml;
while (indexAfter(frest, "<S100FC:S100_FC_FeatureType ")) |start| {
const end = std.mem.indexOf(u8, frest[start..], "</S100FC:S100_FC_FeatureType>") orelse break;
const block = frest[start .. start + end];
frest = frest[start + end ..];
const head = block[0 .. std.mem.indexOf(u8, block, "<S100FC:attributeBinding") orelse block.len];
const name = element(head, "S100FC:name") orelse continue;
const code = element(head, "S100FC:code") orelse continue;
try emitName(a, &out, code, name);
n_classes += 1;
if (element(head, "S100FC:alias")) |al| {
if ((claims.get(al) orelse 0) == 1 and isAcronym(al)) {
try emitName(a, &out, al, name);
n_classes += 1;
}
}
}
try out.appendSlice(a, "};\n");
std.debug.print("fcattrs: {d} class names\n", .{n_classes});

std.debug.print("fcattrs: {d} attribute names, {d} enum values\n", .{ n_attrs, n_vals });
std.Io.File.stdout().writeStreamingAll(io, out.items) catch {};
}

/// True for the shape of an S-57 acronym: six characters, upper case,
/// digits, or underscore.
fn isAcronym(s: []const u8) bool {
if (s.len != 6) return false;
for (s) |c| {
if (!std.ascii.isUpper(c) and !std.ascii.isDigit(c) and c != '_') return false;
}
return true;
}

fn indexAfter(hay: []const u8, needle: []const u8) ?usize {
const at = std.mem.indexOf(u8, hay, needle) orelse return null;
return at + needle.len;
}

/// The text of `<tag>text</tag>`, trimmed, or null.
fn element(block: []const u8, comptime tag: []const u8) ?[]const u8 {
const open = "<" ++ tag ++ ">";
const close = "</" ++ tag ++ ">";
const at = indexAfter(block, open) orelse return null;
const end = std.mem.indexOf(u8, block[at..], close) orelse return null;
return std.mem.trim(u8, block[at .. at + end], " \t\r\n");
}

fn emitName(a: std.mem.Allocator, out: *std.ArrayList(u8), key: []const u8, name: []const u8) !void {
try out.appendSlice(a, " .{ .acronym = \"");
try appendEscaped(a, out, key);
try out.appendSlice(a, "\", .name = \"");
try appendEscaped(a, out, name);
try out.appendSlice(a, "\" },\n");
}

fn emitValue(a: std.mem.Allocator, out: *std.ArrayList(u8), key: []const u8, code: u16, label: []const u8) !void {
var buf: [16]u8 = undefined;
try out.appendSlice(a, " .{ .acronym = \"");
try appendEscaped(a, out, key);
try out.appendSlice(a, "\", .code = ");
try out.appendSlice(a, std.fmt.bufPrint(&buf, "{d}", .{code}) catch unreachable);
try out.appendSlice(a, ", .value = \"");
try appendLoweredEscaped(a, out, label);
try out.appendSlice(a, "\" },\n");
}

/// XML entities unescaped, string quoting escaped.
fn appendEscaped(a: std.mem.Allocator, out: *std.ArrayList(u8), s: []const u8) !void {
var i: usize = 0;
while (i < s.len) : (i += 1) {
if (try appendEntity(a, out, s, &i)) continue;
switch (s[i]) {
'"' => try out.appendSlice(a, "\\\""),
'\\' => try out.appendSlice(a, "\\\\"),
else => try out.append(a, s[i]),
}
}
}

/// Lowercase the label. A word in ALL CAPS is an initialism (IALA, ODAS,
/// TSS) and keeps its case.
fn appendLoweredEscaped(a: std.mem.Allocator, out: *std.ArrayList(u8), s: []const u8) !void {
var i: usize = 0;
while (i < s.len) {
// find the current word's extent
var j = i;
while (j < s.len and s[j] != ' ') : (j += 1) {}
const word = s[i..j];
var caps: usize = 0;
var letters: usize = 0;
for (word) |c| {
if (std.ascii.isAlphabetic(c)) {
letters += 1;
if (std.ascii.isUpper(c)) caps += 1;
}
}
const initialism = letters >= 2 and caps == letters;
var k: usize = i;
while (k < j) : (k += 1) {
var ch = s[k];
if (!initialism) ch = std.ascii.toLower(ch);
if (try appendEntity(a, out, s, &k)) continue;
switch (ch) {
'"' => try out.appendSlice(a, "\\\""),
'\\' => try out.appendSlice(a, "\\\\"),
else => try out.append(a, ch),
}
}
if (j < s.len) try out.append(a, ' ');
i = j + 1;
}
}

/// Consume an XML entity at s[i.*] if present, appending its character.
fn appendEntity(a: std.mem.Allocator, out: *std.ArrayList(u8), s: []const u8, i: *usize) !bool {
const rest = s[i.*..];
const map = [_]struct { ent: []const u8, ch: []const u8 }{
.{ .ent = "&amp;", .ch = "&" }, .{ .ent = "&lt;", .ch = "<" },
.{ .ent = "&gt;", .ch = ">" }, .{ .ent = "&quot;", .ch = "\\\"" },
.{ .ent = "&apos;", .ch = "'" },
};
for (map) |m| {
if (std.mem.startsWith(u8, rest, m.ent)) {
try out.appendSlice(a, m.ch);
i.* += m.ent.len - 1;
return true;
}
}
return false;
}
5 changes: 5 additions & 0 deletions tools/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const ascii = @import("ascii.zig");
const explore = @import("explore.zig");
const cells = @import("cells.zig");
const catalog = @import("catalog.zig");
const fcattrs = @import("fcattrs.zig");
const features = @import("features.zig");
const inspect = @import("inspect.zig");
const zoomsizes = @import("zoomsizes.zig");
Expand Down Expand Up @@ -116,6 +117,10 @@ pub fn main(init: std.process.Init) !void {
return cells.run(io, arena, args);
}

if (std.mem.eql(u8, sub, "fcattrs")) {
return fcattrs.run(io, arena, args);
}

if (std.mem.eql(u8, sub, "catalog")) {
return catalog.run(io, arena, args);
}
Expand Down
Loading