Skip to content
Open
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
10 changes: 10 additions & 0 deletions docs/docs/api/render.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ void tile57_chart_get_info(tile57_chart *chart, tile57_info *out);
tile57_status tile57_chart_scamin(tile57_chart *chart, int32_t **out, size_t *out_len,
tile57_error *err);

/* The label languages the chart states besides English, as ISO 639-2 codes
* (ascending); NULL/0 when none. A host offers the mariner these: a
* preferred_language outside the set draws the portrayed name, except on an
* S-57 chart, whose one national name is coded "und" and answers any language.
* One allocation holds the pointer array and the codes; free *out with
* tile57_free. The compositor borrows its charts, so a host reading a quilt
* unions the sets from the charts it opened, the way it does for SCAMIN. */
tile57_status tile57_chart_languages(tile57_chart *chart, const char *const **out,
size_t *out_len, tile57_error *err);

/* The chart's M_COVR data-coverage polygons, from the coverage the bake embedded:
* ring() is called once per polygon with its exterior ring as npts interleaved
* lon,lat doubles (valid only during the call). OK with no calls when the archive
Expand Down
10 changes: 10 additions & 0 deletions include/tile57.h
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,16 @@ void tile57_chart_get_info(tile57_chart *chart, tile57_info *out);
tile57_status tile57_chart_scamin(tile57_chart *chart, int32_t **out, size_t *out_len,
tile57_error *err);

/* The label languages the chart states besides English, as ISO 639-2 codes,
* ascending. A host offers the mariner these and nothing else: a
* tile57_mariner.preferred_language outside the set draws the portrayed name,
* except on an S-57 chart, whose single national name is coded "und" and
* answers any language. TILE57_OK with *out pointing at *out_len
* NUL-terminated codes, or NULL/0 when the chart states none. One allocation
* holds the pointer array and the codes; free *out with tile57_free. */
tile57_status tile57_chart_languages(tile57_chart *chart, const char *const **out,
size_t *out_len, tile57_error *err);

/* The chart's M_COVR(CATCOV=1) data-coverage polygons, from the coverage the
* bake embedded in the archive metadata — the real coverage a host reports so
* a quilt fills gaps to coarser charts (vs. the bounding box). ring() is called
Expand Down
34 changes: 34 additions & 0 deletions src/capi.zig
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,40 @@ export fn tile57_chart_scamin(handle: ?*Chart, out: ?*?[*]i32, out_len: ?*usize,
return OK;
}

/// The label languages the chart states besides English, as NUL-terminated
/// ISO 639-2 codes. A host offers the mariner these, because a
/// `preferred_language` outside the set draws the portrayed name. One
/// allocation holds the pointer array and the codes, so `tile57_free` on
/// `*out` releases all of it.
export fn tile57_chart_languages(handle: ?*Chart, out: ?*?[*]const [*:0]const u8, out_len: ?*usize, err: ?*CError) callconv(.c) c_int {
const o = out orelse return failWith(err, .badarg, bad_out);
const n = out_len orelse return failWith(err, .badarg, bad_out);
o.* = null;
n.* = 0;
const s = handle orelse return failWith(err, .badarg, "chart must not be null");
const codes = s.languages() catch |e| return fail(err, e);
defer {
for (codes) |c| gpa.free(c);
gpa.free(codes);
}
if (codes.len == 0) return OK;

var bytes: usize = codes.len * @sizeOf([*:0]const u8);
for (codes) |c| bytes += c.len + 1;
const p = exportAlloc(bytes) orelse return failWith(err, .nomem, "out of memory");
const table: [*][*:0]const u8 = @ptrCast(@alignCast(p));
var at: usize = codes.len * @sizeOf([*:0]const u8);
for (codes, 0..) |c, i| {
@memcpy(p[at .. at + c.len], c);
p[at + c.len] = 0;
table[i] = @ptrCast(p + at);
at += c.len + 1;
}
o.* = table;
n.* = codes.len;
return OK;
}

const CCoverageCb = extern struct {
ctx: ?*anyopaque,
ring: *const fn (?*anyopaque, lonlat: [*]const f64, npts: usize) callconv(.c) void,
Expand Down
118 changes: 117 additions & 1 deletion src/chart.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3518,6 +3518,30 @@ pub const Chart = struct {
std.mem.sort(u32, vals, {}, std.sort.asc(u32));
return vals;
}

/// The label languages the chart states besides English, as ISO 639-2
/// codes, ascending. A host offers the mariner these and nothing else,
/// because `preferred_language` outside the set draws the portrayed name.
/// A baked source reads them from the archive metadata; a cell source
/// takes them from the adapted features. Returns a gpa-owned slice of
/// gpa-owned codes.
pub fn languages(self: *Chart) ![]const []const u8 {
var out = std.ArrayList([]const u8).empty;
switch (self.backend) {
.reader => |*r| languagesFromMetadata(r, &out),
.cell => |*cb| for (cb.portrayal_national) |p| {
const code = gpa.dupe(u8, p.lang) catch continue;
out.append(gpa, code) catch gpa.free(code);
},
.cells => {},
}
std.mem.sort([]const u8, out.items, {}, struct {
fn lt(_: void, x: []const u8, y: []const u8) bool {
return std.mem.lessThan(u8, x, y);
}
}.lt);
return out.toOwnedSlice(gpa);
}
};

/// Render ONE feature's resolved portrayal onto a solid background — the
Expand Down Expand Up @@ -3888,6 +3912,47 @@ fn scanScaminArray(json: []const u8, set: *std.AutoHashMap(u32, void)) void {
}
}

// The archive metadata's `"languages":["…"]` array, the codes the bake spliced
// in. Read the same way scaminFromMetadata reads its ladder: the metadata is
// uncompressed in this engine's archives, and a gzip writer is handled too.
fn languagesFromMetadata(r: *pmtiles.Reader, out: *std.ArrayList([]const u8)) void {
const h = r.header;
if (h.metadata_length == 0) return;
const raw = r.bytes[@intCast(h.metadata_offset)..][0..@intCast(h.metadata_length)];
var owned: ?[]u8 = null;
defer if (owned) |o| gpa.free(o);
const json: []const u8 = switch (h.internal_compression) {
.none => raw,
.gzip => blk: {
owned = gzip.decompress(gpa, raw) catch return;
break :blk owned.?;
},
else => return,
};
scanLanguageArray(json, out);
}

// Minimal extractor for `"languages":["zho","und"]`, tolerant of whitespace.
// Each code is duped into gpa, so it outlives a decompressed metadata buffer.
fn scanLanguageArray(json: []const u8, out: *std.ArrayList([]const u8)) void {
const ki = std.mem.indexOf(u8, json, "\"languages\"") orelse return;
var i = ki + "\"languages\"".len;
while (i < json.len and json[i] != '[' and json[i] != '}') i += 1; // skip ` : `
if (i >= json.len or json[i] != '[') return;
i += 1;
while (i < json.len and json[i] != ']') {
while (i < json.len and json[i] != '"' and json[i] != ']') i += 1;
if (i >= json.len or json[i] == ']') break;
i += 1;
const start = i;
while (i < json.len and json[i] != '"') i += 1;
if (i >= json.len) return;
const code = gpa.dupe(u8, json[start..i]) catch return;
out.append(gpa, code) catch gpa.free(code);
i += 1;
}
}

// ---- ENC_ROOT bake -------------------------------------------------------

const BakeSource = struct { base: []const u8, updates: []const []const u8, name: []const u8 = "" };
Expand Down Expand Up @@ -4029,6 +4094,14 @@ pub fn bakeArchive(
// of truth) while they're alive, before each band frees them.
var scamin_set = std.AutoHashMap(u32, void).init(gpa);
defer scamin_set.deinit();
// The label languages every cell in the bake states, for the archive's
// "languages" key. A host reads them to offer the mariner what the chart
// holds.
var langs = std.ArrayList([]const u8).empty;
defer {
for (langs.items) |l| gpa.free(l);
langs.deinit(gpa);
}

// The coarsest populated band gets .extend_min (fill down to minzoom — the
// live tileRefs coarsest-band fallback); every other populated band defers its
Expand Down Expand Up @@ -4140,6 +4213,15 @@ pub fn bakeArchive(
const q = bake_enc.overscaleGateDenom(be.cscl);
if (q > 0) scamin_set.put(@intCast(q), {}) catch {};
}
for (be.portrayal_national) |p| {
var seen = false;
for (langs.items) |l| {
if (std.mem.eql(u8, l, p.lang)) seen = true;
}
if (seen) continue;
const owned = gpa.dupe(u8, p.lang) catch continue;
langs.append(gpa, owned) catch gpa.free(owned);
}
// Fold the sector-figure reach for the archive's "light_reach" key
// (union bbox, max ground leg) while the backends are alive.
if (be.light_bbox) |lb| {
Expand Down Expand Up @@ -4220,10 +4302,15 @@ pub fn bakeArchive(
while (it.next()) |k| try scamin_vals.append(gpa, k.*);
std.mem.sort(u32, scamin_vals.items, {}, std.sort.asc(u32));
}
std.mem.sort([]const u8, langs.items, {}, struct {
fn lt(_: void, x: []const u8, y: []const u8) bool {
return std.mem.lessThan(u8, x, y);
}
}.lt);
var light_reach_json: ?[]const u8 = null;
defer if (light_reach_json) |lj| gpa.free(lj);
if (lr_union) |u| light_reach_json = scene.coverage.encodeLightReachJson(gpa, .{ .bbox = u, .range_m = lr_range_m }) catch null;
const meta = try scene.metadataJson(gpa, scamin_vals.items, coverage_json, light_reach_json);
const meta = try scene.metadataJson(gpa, scamin_vals.items, langs.items, coverage_json, light_reach_json);
defer gpa.free(meta);
return try sw.finishBytes(.{
.metadata_json = meta,
Expand All @@ -4247,3 +4334,32 @@ fn streamSink(ctx: ?*anyopaque, z: u8, x: u32, y: u32, comp: []const u8) anyerro
const sw: *pmtiles.StreamWriter = @ptrCast(@alignCast(ctx.?));
try sw.addCompressed(z, x, y, comp);
}

test "scanLanguageArray reads the codes the bake spliced in" {
var out = std.ArrayList([]const u8).empty;
defer {
for (out.items) |c| gpa.free(c);
out.deinit(gpa);
}
scanLanguageArray("{\"name\":\"chartplotter\",\"languages\":[\"und\",\"zho\"],\"scamin\":[1000]}", &out);
try std.testing.expectEqual(@as(usize, 2), out.items.len);
try std.testing.expectEqualStrings("und", out.items[0]);
try std.testing.expectEqualStrings("zho", out.items[1]);

// Whitespace, and an archive that states none.
var ws = std.ArrayList([]const u8).empty;
defer {
for (ws.items) |c| gpa.free(c);
ws.deinit(gpa);
}
scanLanguageArray("{ \"languages\" : [ \"fin\" ] }", &ws);
try std.testing.expectEqual(@as(usize, 1), ws.items.len);
try std.testing.expectEqualStrings("fin", ws.items[0]);

var none = std.ArrayList([]const u8).empty;
defer none.deinit(gpa);
scanLanguageArray("{\"name\":\"chartplotter\",\"scamin\":[1000]}", &none);
try std.testing.expectEqual(@as(usize, 0), none.items.len);
scanLanguageArray("{\"languages\":[]}", &none);
try std.testing.expectEqual(@as(usize, 0), none.items.len);
}
30 changes: 28 additions & 2 deletions src/scene/scene.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1082,14 +1082,17 @@ pub const VECTOR_LAYERS = mvt.VECTOR_LAYERS;
/// the TileJSON, plus a "scamin" array of the distinct SCAMIN denominators present
/// (ascending) so the client builds one native-minzoom bucket layer per value at
/// load instead of probing tiles. Mirrors the Go pmtiles.Builder.metadata
/// (vector_layers + scamin splice). `scamin` empty -> omit the field. `coverage_json`,
/// (vector_layers + scamin splice). `scamin` empty -> omit the field.
/// `languages` are the label languages the chart states besides English, as
/// ISO 639-2 codes, spliced under a "languages" key so a host reads what a
/// baked chart offers without decoding a tile. Empty -> omit the field. `coverage_json`,
/// when non-null, is a per-cell coverage object (see `coverage.encodeJson`) spliced
/// under a "coverage" key — a single-cell composite bake carries its own M_COVR there.
/// `light_reach_json` (see `coverage.encodeLightReachJson`), when non-null, is the
/// cell's sector-figure reach summary spliced under a "light_reach" key so the
/// compositor can widen its tile addressing without re-portraying the cell.
/// Caller owns the returned bytes (allocated in `a`).
pub fn metadataJson(a: Allocator, scamin: []const u32, coverage_json: ?[]const u8, light_reach_json: ?[]const u8) ![]const u8 {
pub fn metadataJson(a: Allocator, scamin: []const u32, languages: []const []const u8, coverage_json: ?[]const u8, light_reach_json: ?[]const u8) ![]const u8 {
var b = std.ArrayList(u8).empty;
try b.appendSlice(a, "{\"name\":\"chartplotter\",\"format\":\"pbf\",\"vector_layers\":[");
for (VECTOR_LAYERS, 0..) |name, i| {
Expand All @@ -1108,6 +1111,16 @@ pub fn metadataJson(a: Allocator, scamin: []const u32, coverage_json: ?[]const u
}
try b.append(a, ']');
}
if (languages.len > 0) {
try b.appendSlice(a, ",\"languages\":[");
for (languages, 0..) |lang, i| {
if (i > 0) try b.append(a, ',');
try b.append(a, '"');
try b.appendSlice(a, lang);
try b.append(a, '"');
}
try b.append(a, ']');
}
if (coverage_json) |cj| {
try b.appendSlice(a, ",\"coverage\":");
try b.appendSlice(a, cj);
Expand Down Expand Up @@ -3592,3 +3605,16 @@ test "nationalFor prefers the mariner's language and falls back to und" {
const only_zho = [_]rs.NationalText{.{ .lang = "zho", .text = "\u{4e0a}\u{6d77}" }};
try std.testing.expect(rs.nationalFor(&only_zho, "fin") == null);
}

test "metadataJson states the chart's languages" {
const a = std.testing.allocator;
const with = try metadataJson(a, &.{}, &.{ "und", "zho" }, null, null);
defer a.free(with);
try std.testing.expect(std.mem.indexOf(u8, with, "\"languages\":[\"und\",\"zho\"]") != null);

// A chart naming every feature in English states none, and the key is left
// out rather than written empty.
const without = try metadataJson(a, &.{}, &.{}, null, null);
defer a.free(without);
try std.testing.expect(std.mem.indexOf(u8, without, "languages") == null);
}
10 changes: 5 additions & 5 deletions src/style/mariner.zig
Original file line number Diff line number Diff line change
Expand Up @@ -450,12 +450,12 @@ pub fn contourLabelField(b: B, m: *const Settings) !Value {
});
}

// The label text-field. The bake stores two twins beside the portrayed string:
// text_ft, a dredged area's depth in feet, and text_nat, a feature's
// national-language name. Each setting reads its twin and falls back to `text`,
// so a chart baked before a twin existed still labels.
// The label text-field. The bake stores twins beside the portrayed string:
// text_ft, a dredged area's depth in feet, and one text_<lang> per language the
// chart states. Each setting reads its twin and falls back to `text`, so a
// chart baked before a twin existed still labels.
//
// A feature has at most one of the two twins, so their relative order in the
// A feature has at most one of the two kinds, so their relative order in the
// coalesce has no effect.
pub fn labelTextField(b: B, m: *const Settings) !Value {
var terms: [6]Value = undefined;
Expand Down
Loading