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
12 changes: 12 additions & 0 deletions bindings/go/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,18 @@ func (c *ComposeSource) Meta() ComposeMeta {
}
}

// Skipped returns the charts handed to the open that embed no usable coverage.
// They own no ground and are absent from every composed tile, so this is what
// tells a complete quilt from one with holes in it.
func (c *ComposeSource) Skipped() uint32 {
c.mu.Lock()
defer c.mu.Unlock()
if c.ptr == nil {
return 0
}
return uint32(C.tile57_compose_skipped(c.ptr))
}


// Close releases the compositor, then any charts [OpenCompose] opened for it.
// Borrowed charts (from [OpenComposeCharts]) stay open. Idempotent.
Expand Down
5 changes: 5 additions & 0 deletions docs/docs/api/compose.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ tile57_status tile57_compose_gpu_scene(tile57_compose *c, double lon, double lat
/* Fill *out with the compositor's zoom range + union coverage bounds. */
void tile57_compose_get_meta(tile57_compose *c, tile57_compose_meta *out);

/* Charts handed to the open that embed no usable coverage: they own no ground
* and are absent from every composed tile, so a host reads this to tell a
* complete quilt from one with holes in it. 0 for a NULL handle. */
uint32_t tile57_compose_skipped(tile57_compose *c);

/* Serialize the ownership partition to `path` (a sidecar a later
* tile57_compose_open loads to skip the build). */
tile57_status tile57_compose_save_partition(tile57_compose *c, const char *path,
Expand Down
17 changes: 17 additions & 0 deletions include/tile57.h
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,14 @@ typedef struct {
uint8_t tile_type; /* tile57_tile_type */
int32_t native_scale;
bool is_raster; /* tiles are images, not vector tiles */
uint32_t skipped_cells; /* cells handed to the open that produced no chart.
* The open succeeds while one parses, so this tells a
* chart set with a gap in it from a complete one.
* Set on a chart opened from ENC source. The opens
* this header exports are archive-backed, where it
* stays 0; the engine names each dropped cell on
* stderr as it goes. Appended for ABI-append-safety; a
* zeroed struct reads 0. */
} tile57_info;
void tile57_chart_get_info(tile57_chart *chart, tile57_info *out);

Expand Down Expand Up @@ -1635,6 +1643,15 @@ tile57_status tile57_compose_query(tile57_compose *c, double lon, double lat, do
/* Fill *out with the compositor's zoom range + union coverage bounds. */
void tile57_compose_get_meta(tile57_compose *c, tile57_compose_meta *out);

/* Charts handed to the open that embed no usable coverage: they own no ground
* and are absent from every composed tile, so a host reads this to tell a
* complete quilt from one with holes in it. 0 for a NULL handle.
*
* A call rather than a tile57_compose_meta field: that struct ends on a double
* with no padding left, so any field added to it moves the bounds and grows the
* struct a host already allocates. */
uint32_t tile57_compose_skipped(tile57_compose *c);

/* The deepest zoom the chart covering (lon,lat) can serve (its native window +
* overscale fill-up). A host caps its per-view zoom-in here so it never magnifies
* past that chart into nodata — unlike compose_meta.max_zoom, which is the
Expand Down
16 changes: 16 additions & 0 deletions src/capi.zig
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,10 @@ const CInfo = extern struct {
tile_type: u8, // the archive's stored encoding (TILE57_TILE_TYPE_*)
native_scale: i32, // embedded compilation scale (1:N); 0 = derive from zoom band
is_raster: bool, // the archive stores pictures, not vector tiles
// Cells handed to the open that produced no chart. The open succeeds while
// one parses, so this is what tells a host the set has a gap. Appended for
// ABI-append-safety; a zeroed struct reads 0.
skipped_cells: u32,
};

// tile57_tile_type values (keep in sync with tile57.h).
Expand All @@ -909,6 +913,7 @@ export fn tile57_chart_get_info(src: ?*Chart, out: ?*CInfo) callconv(.c) void {
// nothing above tells it apart from a vector chart. Its tiles are images.
// tile_type cannot carry this: TILE57_TILE_TYPE_MLT is 2, and 2 is PNG in
// the PMTiles header.
o.skipped_cells = s.skipped_cells;
o.is_raster = switch (s.tileType()) {
.png, .jpeg, .webp, .avif => true,
else => false,
Expand Down Expand Up @@ -1627,6 +1632,10 @@ export fn tile57_compose_open(
error.MixedChartKinds => failWith(err, .unsupported, mixed_kinds),
else => fail(err, e),
}) orelse return failWith(err, .unsupported, "no chart carries per-cell coverage");
// What the open kept, subtracted from what it was handed. Counting the
// charts with no decoded coverage missed the ones openBorrowed drops later
// for an empty coverage ring, so charts + skipped came to less than n.
src.skipped = @intCast(n - src.readers.len);
sidecar.refresh(io, src);
o.* = src;
return OK;
Expand Down Expand Up @@ -1725,6 +1734,7 @@ export fn tile57_compose_rasters(
error.MixedChartKinds => failWith(err, .unsupported, mixed_kinds),
else => fail(err, e),
}) orelse return failWith(err, .unsupported, "no raster chart carries a compilation scale and coverage");
src.skipped = @intCast(n - src.readers.len);
o.* = src;
return OK;
}
Expand Down Expand Up @@ -1958,6 +1968,12 @@ export fn tile57_compose_get_meta(handle: ?*compose.ComposeSource, out: ?*CCompo
};
}

/// Charts handed to the open that embed no usable coverage. See tile57.h.
export fn tile57_compose_skipped(handle: ?*compose.ComposeSource) callconv(.c) u32 {
const src = handle orelse return 0;
return src.skipped;
}

/// The deepest zoom the chart covering (lon,lat) can serve — the host caps its
/// per-view zoom-in here so it never magnifies past that chart into nodata. Falls
/// back to the library max where the point covers no cell. See tile57.h.
Expand Down
63 changes: 52 additions & 11 deletions src/chart.zig
Original file line number Diff line number Diff line change
Expand Up @@ -388,13 +388,23 @@ fn cdup(bytes: []const u8) ?[*]u8 {
// Peek `relpath`'s bbox+scale; on success append an index-aligned meta + a gpa-owned
// copy of the path. Cells that don't read / have no coverage bbox are skipped (both
// lists), keeping meta[i] and paths[i] aligned with the streaming cell index.
fn addPathCell(io: std.Io, dir: std.Io.Dir, relpath: []const u8, metas: *std.ArrayList(ChartMeta), paths: *std.ArrayList([]u8)) !void {
const bytes = dir.readFileAlloc(io, relpath, gpa, .limited(MAX_CELL_BYTES)) catch return;
fn addPathCell(io: std.Io, dir: std.Io.Dir, relpath: []const u8, metas: *std.ArrayList(ChartMeta), paths: *std.ArrayList([]u8)) !bool {
const bytes = dir.readFileAlloc(io, relpath, gpa, .limited(MAX_CELL_BYTES)) catch {
std.debug.print("CHART LOST {s}: cell did not read\n", .{relpath});
return false;
};
defer gpa.free(bytes);
const m = peekAnyMeta(bytes) orelse return;
const bb = m.bounds orelse return;
const m = peekAnyMeta(bytes) orelse {
std.debug.print("CHART LOST {s}: cell did not parse\n", .{relpath});
return false;
};
const bb = m.bounds orelse {
std.debug.print("CHART LOST {s}: cell has no extent\n", .{relpath});
return false;
};
try metas.append(gpa, .{ .west = bb[0], .south = bb[1], .east = bb[2], .north = bb[3], .cscl = m.cscl });
try paths.append(gpa, try gpa.dupe(u8, relpath));
return true;
}

// Internal ChartReadFn for a path-backed chart: read cell `index`'s base .000 + its
Expand Down Expand Up @@ -1155,7 +1165,13 @@ fn bakeFileWorker(ctx: *BakeFileCtx) void {
const i = ctx.next.fetchAdd(1, .monotonic);
if (i >= ctx.in_paths.len) return;
bakeOneToFile(ctx, i);
if (ctx.label) |lb| lb(ctx.progress_ctx, @intCast(i)); // name the chart just finished
// The label names a chart that was written. It fired for every cell
// before, so a host printed a finished chart for one that failed.
if (ctx.ok[i]) {
if (ctx.label) |lb| lb(ctx.progress_ctx, @intCast(i));
} else {
std.debug.print("CHART LOST {s}: bake produced no archive\n", .{ctx.in_paths[i]});
}
const d = ctx.done.fetchAdd(1, .monotonic) + 1; // attempted count (smooth progress)
if (ctx.progress) |cb| {
if (!cb(ctx.progress_ctx, d, @intCast(ctx.in_paths.len))) {
Expand Down Expand Up @@ -2389,6 +2405,10 @@ pub const Chart = struct {
/// partition sidecar the bake wrote next to the archives, so a host never
/// has to know that file exists.
source_path: ?[]u8 = null,
/// Cells handed to the open that produced no chart. The open succeeds while
/// one cell parses, so a host reads this to tell a chart set with a gap in
/// it from a complete one.
skipped_cells: u32 = 0,
cache: std.AutoHashMap(u64, []u8), // tile key -> MVT bytes (owned)
cache_max: usize = 8192,
// Emit the per-feature pick-report attrs (s57/cell) on live-generated tiles.
Expand Down Expand Up @@ -2459,8 +2479,12 @@ pub const Chart = struct {
bake_enc.parallelFor(gpa, cells_in.len, &ow, OpenWork.run);

var valid: usize = 0;
for (ok) |k| {
if (k) valid += 1;
for (ok, cells_in) |k, in| {
if (k) {
valid += 1;
} else {
std.debug.print("CHART LOST {s}: cell did not parse\n", .{in.name});
}
}
if (valid == 0) return error.InvalidCell; // cells provided, but none parsed

Expand All @@ -2481,6 +2505,7 @@ pub const Chart = struct {
};
src.* = .{
.backend = .{ .cells = .{ .cells = cells, .rules_dir = dir_copy } },
.skipped_cells = @intCast(cells_in.len - valid),
.cache = std.AutoHashMap(u64, []u8).init(gpa),
.pick_attrs = pick_attrs,
};
Expand Down Expand Up @@ -2546,16 +2571,21 @@ pub const Chart = struct {
for (paths.items) |p| gpa.free(p);
paths.deinit(gpa);
}
// Cells the walk found and could not use. The open succeeds on the
// rest, so this is what tells a host the set has a gap.
var skipped: u32 = 0;

if (single_file) {
try addPathCell(io, dir, std.fs.path.basename(path), &metas, &paths);
if (!try addPathCell(io, dir, std.fs.path.basename(path), &metas, &paths)) skipped += 1;
} else if (dir.readFileAlloc(io, "CATALOG.031", gpa, .limited(MAX_CELL_BYTES))) |cbytes| {
defer gpa.free(cbytes);
var carena = std.heap.ArenaAllocator.init(gpa);
defer carena.deinit();
if (s57.parseCatalog(carena.allocator(), cbytes)) |entries| {
for (entries) |e| {
if (e.is_cell) try addPathCell(io, dir, e.path, &metas, &paths);
if (e.is_cell) {
if (!try addPathCell(io, dir, e.path, &metas, &paths)) skipped += 1;
}
}
}
} else |_| {
Expand All @@ -2564,7 +2594,7 @@ pub const Chart = struct {
while (try walker.next(io)) |entry| {
if (entry.kind != .file) continue;
if (!std.mem.endsWith(u8, entry.path, ".000")) continue;
try addPathCell(io, dir, entry.path, &metas, &paths);
if (!try addPathCell(io, dir, entry.path, &metas, &paths)) skipped += 1;
}
}
if (metas.items.len == 0) return error.OpenFailed;
Expand All @@ -2573,6 +2603,7 @@ pub const Chart = struct {
// Chart owns the PathCtx (Io + Dir + paths) via ls.path_ctx, freed in deinit.
const src = try openChartsStreaming(metas.items, pathRead, null, rules_dir, pick_attrs);
errdefer src.deinit();
src.skipped_cells = skipped;
const ctx = try gpa.create(PathCtx);
errdefer gpa.destroy(ctx);
ctx.* = .{ .threaded = threaded, .io = io, .dir = dir, .paths = try paths.toOwnedSlice(gpa) };
Expand Down Expand Up @@ -4184,7 +4215,17 @@ pub fn bakeArchive(
@memset(pas, null);
var bw = BakeWork{ .sources = sources.items, .outs = outs, .arenas = pas, .rules_dir = dir, .build_geo = bake_enc.cacheGeoForBand(band) };
bake_enc.parallelFor(gpa, sources.items.len, &bw, BakeWork.run);
loaded += idxs.len;
// Count the cells that produced a backend. `idxs.len` counted the ones
// attempted, so progress reached the total whether they loaded or not.
for (outs, idxs) |o, ci| {
if (o != null) {
loaded += 1;
} else {
// The bake keeps going. This line is the report: a cell absent
// from the archive with no word for it reads as empty ocean.
std.debug.print("CHART LOST {s}: parse produced no cell\n", .{cells_in[ci].name});
}
}
if (progress) |cb| if (has_work) cb(user, 0, loaded, cells_in.len, band_ord - 1, band_count, @tagName(band).ptr);

var backs = std.ArrayList(bake_enc.Backend).empty;
Expand Down
4 changes: 4 additions & 0 deletions src/compose/compose.zig
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,10 @@ pub const ComposeSource = struct {
/// is common, and everything above it — clip a feature or stack a picture,
/// portray or not — is not. Set at open and never changed.
kind: Kind = .vector,
/// Charts handed to the open that embed no usable coverage. They own no
/// ground, so they are absent from every composed tile, and the open
/// succeeds without them.
skipped: u32 = 0,
// A files-open owns its readers + mmaps (deinit closes them); a charts-open
// borrows them from the charts, which must outlive this source.
owns_archives: bool = true,
Expand Down
Loading