Places map: city base map, cluster gallery, zoom + render polish

Base map / tiles:
- Switch the base map to the mapchart city-label world map; re-derive its
  Web-Mercator georeferencing (MAP_LON0/1, MAP_IMG_MY0/1) by least-squares
  against the pixel-identical graticule export.
- Add a 1.5x LANCZOS supersample LOD and bump tile quality 90->94 so city
  labels stay crisp when zoomed in (tools/png_to_tiles.py).
- Fill the map view background with the ocean colour instead of black.

Navigation / clustering:
- Snap the cluster grid to power-of-two zoom octaves so the bubbles stop
  jittering during a continuous zoom.
- Replace the loose zoom clamps with named MAP_SCALE_MIN/MAX (whole map fills
  the screen .. country/region level).
- Add a toggleable computed lat/lon graticule overlay (TRIANGLE).

Cluster gallery:
- Selecting a bubble now opens all its photos on the same justified grid as
  the timeline/search (a g_gallery_active filter). Members not already paged
  in are appended as hidden g_asset entries (kept out of the main timeline);
  a background metadata worker resolves each one's date/aspect so the grid
  reflows into month sections + true aspect ratios.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011uKJQMYDE9Nrq9ECsU3MxH
This commit is contained in:
Sads
2026-06-18 22:26:59 +02:00
co-authored by Claude Opus 4.8
parent 02f428a76d
commit 5a0eb06d3e
8 changed files with 408 additions and 59 deletions
Binary file not shown.
+99
View File
@@ -89,6 +89,7 @@ static int parse_assets(const char *js, size_t jslen, char *err, size_t errlen)
g_asset_ratio[cur] = 0.0f;
g_asset_rot[cur] = 0;
g_asset_is_video[cur] = 0;
g_asset_hidden[cur] = 0;
continue;
}
if (cur < 0 || tok[i].type != JSMN_STRING || tok[i].size != 1 ||
@@ -279,4 +280,102 @@ static int check_new_assets(void)
return fresh;
}
/* parse a single /api/assets/{id} object for the fields the grid needs to lay
* an item out: capture date, display aspect (from exif), orientation, video
* flag. returns 1 if the date was found. (parse_assets handles arrays; this is
* the one-object form used by the gallery's lazy metadata resolver.) */
static int parse_one_asset(const char *js, size_t len, char *date, size_t datelen,
float *ratio, unsigned char *rot, int *is_video)
{
jsmn_parser p;
jsmn_init(&p);
int n = jsmn_parse(&p, js, len, NULL, 0);
if (n <= 0)
return 0;
jsmntok_t *t = malloc(sizeof(*t) * n);
if (!t)
return 0;
jsmn_init(&p);
n = jsmn_parse(&p, js, len, t, n);
if (n <= 0 || t[0].type != JSMN_OBJECT) {
free(t);
return 0;
}
date[0] = '\0';
*ratio = 0.0f;
*rot = 0;
*is_video = 0;
int exif_obj = -1, exw = 0, exh = 0, exori = 0, got = 0;
for (int i = 1; i + 1 < n; i++) {
if (t[i].type != JSMN_STRING || t[i].size != 1)
continue;
int kl = t[i].end - t[i].start;
const char *k = js + t[i].start;
jsmntok_t *v = &t[i + 1];
int vl = v->end - v->start;
if (t[i].parent == 0 && kl == 8 && !strncmp(k, "exifInfo", 8) &&
v->type == JSMN_OBJECT) {
exif_obj = i + 1;
continue;
}
if (t[i].parent == exif_obj) {
if (kl == 14 && !strncmp(k, "exifImageWidth", 14))
exw = atoi(js + v->start);
else if (kl == 15 && !strncmp(k, "exifImageHeight", 15))
exh = atoi(js + v->start);
else if (kl == 11 && !strncmp(k, "orientation", 11))
exori = atoi(js + v->start);
continue;
}
if (t[i].parent != 0)
continue;
if (kl == 13 && !strncmp(k, "fileCreatedAt", 13) && vl >= 10) {
int cl = vl < (int)datelen - 1 ? vl : (int)datelen - 1;
memcpy(date, js + v->start, cl);
date[cl] = '\0';
got = 1;
} else if (kl == 4 && !strncmp(k, "type", 4)) {
*is_video = (vl >= 5 && !strncmp(js + v->start, "VIDEO", 5));
}
}
if (exw > 0 && exh > 0)
*ratio = (exori >= 5 && exori <= 8) ?
(float)exh / exw : (float)exw / exh;
*rot = (unsigned char)exori;
free(t);
return got;
}
/* background metadata resolver thread: fetches one asset's date/aspect at a
* time (the gallery's lazy fill). One-slot handoff like the thumb worker —
* main writes g_meta_id + flips to PENDING, we fetch+parse and flip to DONE. */
static int meta_worker(SceSize args, void *argp)
{
(void)args;
(void)argp;
for (;;) {
if (g_meta_state != META_PENDING) {
sceKernelDelayThread(4000);
continue;
}
char url[600];
snprintf(url, sizeof(url), "%s/api/assets/%s", g_server, g_meta_id);
membuf buf;
long code;
CURLcode r = http_request(url, NULL, &buf, &code, NULL, 0);
g_meta_date[0] = '\0';
g_meta_ratio = 0.0f;
g_meta_rot = 0;
g_meta_is_video = 0;
if (r == CURLE_OK && code >= 200 && code < 300 && buf.data)
parse_one_asset(buf.data, buf.size, g_meta_date,
sizeof(g_meta_date), &g_meta_ratio,
&g_meta_rot, &g_meta_is_video);
free(buf.data);
__sync_synchronize();
g_meta_state = META_DONE;
}
return 0;
}
+50 -7
View File
@@ -112,6 +112,8 @@ static void reset_for_account_change(void)
g_next_page = 1;
g_search_active = 0;
g_search_count = 0;
g_gallery_active = 0;
g_gallery_count = 0;
g_srv_state = 0; /* re-fetch the cloud page's server info */
g_srv_fetching = 0;
g_map_state = 0; /* re-fetch the new account's map markers */
@@ -272,6 +274,11 @@ int main(void)
if (syncw >= 0)
sceKernelStartThread(syncw, 0, NULL);
SceUID metaw = sceKernelCreateThread("meta_worker", meta_worker,
0x10000100, 128 * 1024, 0, 0, NULL);
if (metaw >= 0)
sceKernelStartThread(metaw, 0, NULL);
load_autobackup();
int mode = MODE_GRID;
@@ -482,7 +489,8 @@ int main(void)
* be fetched yet — pull pages until a new month
* shows up (or the library ends). search results
* aren't paginated, so skip the pull. */
if (dir > 0 && !g_search_active && can_pull) {
if (dir > 0 && !g_search_active &&
!g_gallery_active && can_pull) {
int guard = 0;
while (g_next_page > 0 && guard++ < 10 &&
!strncmp(disp_date(month_jump(sel, +1)),
@@ -540,7 +548,7 @@ int main(void)
* screen: the grid stays up, the HUD's "+" already says
* more is coming, and the fetch only blocks briefly */
if (!scrolling_fast && rl_idle >= 12 && !g_search_active &&
g_next_page > 0 &&
!g_gallery_active && g_next_page > 0 &&
sel >= g_disp_count - COLS * 4) {
if (fetch_page(0) > 0)
rebuild_keep_view(&sel, &scroll,
@@ -553,6 +561,7 @@ int main(void)
* across the relayout */
uint64_t pnow = sceKernelGetProcessTimeWide();
if (!scrolling_fast && !touch_active && !g_search_active &&
!g_gallery_active &&
pnow - last_poll > 30ULL * 1000 * 1000) {
last_poll = pnow;
if (check_new_assets() > 0)
@@ -664,6 +673,21 @@ int main(void)
}
}
/* in a cluster gallery, O returns to the map; any
* bar action (search/cloud/map) drops the filter */
if (g_gallery_active && (pressed & SCE_CTRL_CIRCLE)) {
gallery_close();
mode = MODE_MAP;
sel = 0;
scroll = target = 0;
grid_last_sel = -1;
touch_active = touch_on_bar = 0;
continue;
}
if (g_gallery_active &&
(do_open_cloud || do_open_map || do_search_open))
gallery_close();
if (do_open_cloud) {
mode = MODE_CLOUD;
continue;
@@ -825,6 +849,12 @@ int main(void)
last_vis = g_disp_count - 1;
}
/* cluster gallery: resolve members' date/aspect in the
* background so the grid sorts into months + true aspects */
if (g_gallery_active)
gallery_meta_pump(first_vis, last_vis,
&sel, &scroll, &target);
/* collect finished download, then hand the worker
* the next most useful thumbnail */
if (g_req_state == REQ_DONE) {
@@ -1070,7 +1100,8 @@ int main(void)
}
if (pressed & SCE_CTRL_CIRCLE) {
/* back to wherever we came from (the map if the
* photo was opened from a marker, else the grid) */
* photo was opened from a marker, else the grid —
* which may be showing a cluster gallery) */
mode = detail_from_map ? MODE_MAP : MODE_GRID;
detail_from_map = 0;
detail_idx = -1;
@@ -1831,6 +1862,7 @@ pf_skip:
if (pad.buttons & SCE_CTRL_DOWN) g_map_cy += pstep;
if (pad.buttons & SCE_CTRL_RTRIGGER) g_map_scale *= 1.03;
if (pad.buttons & SCE_CTRL_LTRIGGER) g_map_scale /= 1.03;
if (pressed & SCE_CTRL_TRIANGLE) g_map_grid = !g_map_grid;
map_clamp_view();
/* touch: one finger drags the map, two fingers pinch-zoom,
@@ -1904,7 +1936,8 @@ pf_skip:
else if (tapped_cluster >= 0)
act = tapped_cluster;
if (act >= 0 && act < g_cl_count) {
if (map_should_open(act)) {
if (g_cl_n[act] <= 1) {
/* a lone marker: open the photo directly */
int s;
if (map_open_marker(g_cl_rep[act], &s)) {
sel = s;
@@ -1918,15 +1951,25 @@ pf_skip:
grid_last_sel = -1;
continue;
}
map_zoom_into(act); /* couldn't open: just zoom */
} else {
map_zoom_into(act);
/* a bubble: show all its photos on the grid */
g_search_active = 0;
gallery_open(act);
sel = 0;
scroll = target = 0.0f;
grid_last_sel = -1;
mode = MODE_GRID;
continue;
}
}
vita2d_start_drawing();
vita2d_clear_screen();
vita2d_draw_rectangle(0, 0, SCREEN_W, SCREEN_H,
MAP_OCEAN_BG); /* sea, not black */
map_draw_tiles();
if (g_map_grid)
map_draw_graticule();
map_draw_clusters(nc);
/* centre crosshair: shows what X will act on */
vita2d_draw_rectangle(SCREEN_W / 2.0f - 8, SCREEN_H / 2.0f,
@@ -1940,7 +1983,7 @@ pf_skip:
draw_text(12, 28, RGBA8(235, 235, 240, 255), 1.0f, mh);
draw_text(12, SCREEN_H - 16,
RGBA8(150, 150, 158, 255), 0.85f,
"d-pad pan L/R zoom X open/zoom O back");
"d-pad pan L/R zoom X open /\\ grid O back");
}
vita2d_end_drawing();
vita2d_swap_buffers();
+20
View File
@@ -176,6 +176,24 @@ static void layout_grid(void)
* server asset is shown once, as the server asset (green badge). */
static void rebuild_display(void)
{
/* map cluster gallery: show exactly the selected bubble's members (server
* assets), date-grouped like the timeline. Members whose metadata hasn't
* resolved yet have an empty date and sort to the end until it arrives. */
if (g_gallery_active) {
if (!grow_disp(g_gallery_count > 0 ? g_gallery_count : 1))
return;
int n = 0;
for (int k = 0; k < g_gallery_count; k++) {
g_disp[n].src = SRC_SERVER;
g_disp[n].idx = g_gallery_idx[k];
n++;
}
g_disp_count = n;
qsort(g_disp, n, sizeof(g_disp[0]), disp_cmp);
layout_grid();
return;
}
/* search mode: the merged timeline is replaced by the result set
* (server assets only, date-grouped); local media isn't folded in */
if (g_search_active) {
@@ -201,6 +219,8 @@ static void rebuild_display(void)
int n = 0;
for (int i = 0; i < g_asset_count; i++) {
if (g_asset_hidden[i])
continue; /* gallery-only asset: not in the timeline */
g_disp[n].src = SRC_SERVER;
g_disp[n].idx = i;
n++;
+3
View File
@@ -128,6 +128,9 @@ static unsigned char *g_asset_is_video;
/* set during display rebuild: a backed-up local file matches this server
* asset, so its grid cell shows the green "backed up" badge */
static unsigned char *g_asset_local_backed;
/* 1 = appended only to back a map-cluster gallery (its id/thumb are needed but
* it isn't part of the paged library): shown in the gallery, hidden elsewhere */
static unsigned char *g_asset_hidden;
static int g_asset_count;
static int g_asset_cap;
static int g_next_page = 1; /* 0 = no more pages */
+188 -43
View File
@@ -32,17 +32,33 @@ static volatile int g_map_fetching; /* a worker thread is fetching now */
* on-screen pixel size (256 << zoom). g_map_view_init is cleared so the
* first time the page opens (and after an account switch) it fits to the
* markers' bounding box instead of showing a stale view. */
/* zoom limits — g_map_scale is the whole world's on-screen width in px.
* MIN stops zoom-out once the map covers the screen with no black bars; the map
* is taller-aspect than the screen, so the poles and a little longitude crop off
* (pan to reach them) rather than leaving margins. MAX is about country/region
* level — ~4x the deepest tile LOD (10496px), so the raster base map stays
* legible rather than blurring into mush as the old deep caps did. */
#define MAP_SCALE_MIN 1200.0
#define MAP_SCALE_MAX 4.0e4
/* the base map's ocean colour — fills the view behind/around the tiles so the
* margins (and any gap while tiles stream in) read as sea, not black */
#define MAP_OCEAN_BG RGBA8(217, 187, 149, 255)
static double g_map_cx = 0.5, g_map_cy = 0.5;
static double g_map_scale = 512.0;
static double g_map_scale = MAP_SCALE_MIN;
static int g_map_view_init;
static int g_map_grid = 0; /* overlay the computed graticule (toggle: TRIANGLE) */
#define MAP_PI 3.14159265358979323846
/* The source map (MapChart_Map (4)) is a clean conformal full-longitude Web
* Mercator: lon -180..180 across its width, latitude band MAP_IMG_MY0..MY1.
* Verified by isolated-island centroids (span 360.0°, x/y scales match). */
#define MAP_LON0 (-180.0) /* longitude at the image's left edge */
#define MAP_LON1 (180.0) /* longitude at the image's right edge */
/* The source map (a mapchart.net world map, city-label variant) is a
* near-conformal full-longitude Web Mercator: lon spans the image width,
* latitude band MAP_IMG_MY0..MY1. These bounds were fit by least-squares against
* the pixel-identical graticule export of the same map (meridian residual
* <0.5px, parallel <2.3px over 7000px) — that sibling shares this one's exact
* crop (verified: 0px phase-correlation shift), so the fit transfers directly.
* The image runs ~0.45° past ±180 each side. */
#define MAP_LON0 (-180.40) /* longitude at the image's left edge */
#define MAP_LON1 (180.50) /* longitude at the image's right edge */
/* lon/lat -> normalised image coords [0,1]. y is standard Web-Mercator (lat
* clamped to the valid band); the image's merc-y bounds are MAP_IMG_MY0/MY1. */
@@ -168,8 +184,8 @@ static void fetch_map_markers(void)
/* keep the centre on the map and the zoom within sane bounds */
static void map_clamp_view(void)
{
if (g_map_scale < 360.0) g_map_scale = 360.0; /* ~whole world */
if (g_map_scale > 2.0e8) g_map_scale = 2.0e8; /* street level */
if (g_map_scale < MAP_SCALE_MIN) g_map_scale = MAP_SCALE_MIN; /* whole world */
if (g_map_scale > MAP_SCALE_MAX) g_map_scale = MAP_SCALE_MAX; /* neighbourhood */
if (g_map_cx < 0.0) g_map_cx = 0.0;
if (g_map_cx > 1.0) g_map_cx = 1.0;
if (g_map_cy < 0.0) g_map_cy = 0.0;
@@ -183,7 +199,7 @@ static void map_fit_view(void)
if (g_map_count <= 0) {
g_map_cx = 0.5;
g_map_cy = 0.5;
g_map_scale = 512.0;
g_map_scale = MAP_SCALE_MIN;
map_clamp_view();
return;
}
@@ -202,9 +218,9 @@ static void map_fit_view(void)
if (h < 1e-9) h = 1e-9;
double sx = SCREEN_W * 0.82 / w, sy = SCREEN_H * 0.82 / h;
g_map_scale = sx < sy ? sx : sy;
/* a single point fits at "infinite" zoom — pull back to a city view */
if (g_map_scale > 4.0e6)
g_map_scale = 4.0e6;
/* a single point fits at "infinite" zoom — pull back to the max zoom */
if (g_map_scale > MAP_SCALE_MAX)
g_map_scale = MAP_SCALE_MAX;
map_clamp_view();
}
@@ -226,13 +242,19 @@ static int g_cl_count;
/* Group markers into bubbles. The grid cells are anchored in MAP (Web-Mercator)
* space, not screen space, so panning leaves each marker in the same cell and
* the bubbles simply translate with the map instead of reshuffling/jumping.
* Cell size tracks zoom (~MAP_CELL px wide at the current scale). */
*
* The cell size is keyed off a scale SNAPPED to the nearest power-of-two octave,
* not the live scale: the live scale changes every frame while zooming, which
* drifts the cell boundaries so markers hop cells and the bubbles jitter. With
* the snap, the grid is fixed across a ~1.4x zoom band and only re-forms once
* per octave, so during a zoom the bubbles just glide/scale with the map. */
static void map_cluster(void)
{
g_cl_count = 0;
if (g_map_scale <= 0.0)
return;
double cellsz = (double)MAP_CELL / g_map_scale; /* merc units per cell */
double qscale = pow(2.0, round(log2(g_map_scale))); /* octave-snapped */
double cellsz = (double)MAP_CELL / qscale; /* merc units per cell */
for (int i = 0; i < g_map_count; i++) {
double mx = merc_x(g_map_lon[i]), my = merc_y(g_map_lat[i]);
float sx, sy;
@@ -314,30 +336,6 @@ static int map_cluster_at(float x, float y)
return -1;
}
/* a tap/X on cluster c should OPEN its photo (rather than zoom in) when it's a
* lone marker, or when its members are effectively on the same spot so zooming
* would never split them (common with photos shot at one place). */
static int map_should_open(int c)
{
if (c < 0 || c >= g_cl_count)
return 0;
if (g_cl_n[c] <= 1)
return 1;
return (g_cl_x1[c] - g_cl_x0[c] < 6.0f &&
g_cl_y1[c] - g_cl_y0[c] < 6.0f);
}
/* recentre on a cluster and zoom in a notch (splits dense bubbles apart) */
static void map_zoom_into(int c)
{
if (c < 0 || c >= g_cl_count)
return;
g_map_cx += (g_cl_x[c] - SCREEN_W / 2.0f) / g_map_scale;
g_map_cy += (g_cl_y[c] - SCREEN_H / 2.0f) / g_map_scale;
g_map_scale *= 2.6;
map_clamp_view();
}
/* resolve a marker's asset to a display slot, opening it in the detail view.
* the map lists the whole server, so the asset is often not among the pages
* fetched for the grid — in that case fetch its metadata, append it to the
@@ -379,6 +377,7 @@ static int map_open_marker(int marker_idx, int *out_sel)
g_asset_rot[idx] = 0;
g_asset_is_video[idx] = !strncmp(tval, "VIDEO", 5);
g_asset_local_backed[idx] = 0;
g_asset_hidden[idx] = 0;
g_thumb[idx] = NULL;
g_thumb_failed[idx] = 0;
g_tex_err[idx][0] = '\0';
@@ -397,11 +396,11 @@ static int map_open_marker(int marker_idx, int *out_sel)
}
/* latitude band of the source image, in normalised Web-Mercator. NOT simply
* image-height/width: MapChart's vertical scale differs slightly from its
* horizontal, so these were fit against the real marker coordinates (so the
* photos land on land, not offset north into the water). ~76.6°N .. -59.9°S. */
#define MAP_IMG_MY0 0.03993 /* top edge (~83.5°N) */
#define MAP_IMG_MY1 0.95395 /* bottom edge (~-83.5°S) */
* image-height/width: the source's vertical scale differs ~0.4% from its
* horizontal, so these were fit against the image's baked-in graticule (the
* equator sits at y=3213 of 6352, ~37px below image centre). ~83.7°N .. -83.3°S. */
#define MAP_IMG_MY0 0.03814 /* top edge (~83.7°N) */
#define MAP_IMG_MY1 0.95125 /* bottom edge (~-83.3°S) */
/* ------------------------------------------------------------------ */
/* raster tile pyramid (app0:maptiles.pak) — the LOD base map. */
@@ -641,6 +640,41 @@ static void map_draw_grid(void)
}
}
/* Lat/lon graticule, projected with merc_x/merc_y + MAP_LON0/1 + MAP_IMG_MY0/1
* (the same maths the markers use), drawn bright over the tiled base map at 10°
* spacing. The current city-label base map has no printed grid, so this is the
* only graticule; against the graticule export it coincided to ~1px, which is
* how the georeferencing was validated. Prime meridian and equator are brighter
* to anchor orientation. Toggle with TRIANGLE. */
static void map_draw_graticule(void)
{
uint32_t reg = RGBA8(225, 45, 45, 140); /* translucent red */
uint32_t axis = RGBA8(255, 90, 90, 230); /* lon 0 / equator */
/* clip the overlay to the base-map rectangle (clamped to the screen) so
* each line spans only where the map is, not the black margins around it */
float lx, ty, rx, by;
map_project(0.0, MAP_IMG_MY0, &lx, &ty); /* image top-left */
map_project(1.0, MAP_IMG_MY1, &rx, &by); /* image bot-right */
if (lx < 0) lx = 0;
if (ty < 0) ty = 0;
if (rx > SCREEN_W) rx = SCREEN_W;
if (by > SCREEN_H) by = SCREEN_H;
for (int lon = -180; lon <= 180 && by > ty; lon += 10) {
float sx, sy;
map_project(merc_x(lon), 0.0, &sx, &sy);
if (sx >= 0 && sx <= SCREEN_W)
vita2d_draw_rectangle(sx, ty, 1, by - ty,
lon == 0 ? axis : reg);
}
for (int lat = -80; lat <= 80 && rx > lx; lat += 10) {
float sx, sy;
map_project(0.0, merc_y(lat), &sx, &sy);
if (sy >= 0 && sy <= SCREEN_H)
vita2d_draw_rectangle(lx, sy, rx - lx, 1,
lat == 0 ? axis : reg);
}
}
/* draw every cluster bubble; `sel` gets a white selection ring */
static void map_draw_clusters(int sel)
{
@@ -664,3 +698,114 @@ static void map_draw_clusters(int sel)
draw_text(x - w / 2.0f, y + 6.0f, white, 0.8f, b);
}
}
/* ------------------------------------------------------------------ */
/* cluster gallery */
/* */
/* Selecting a bubble shows every photo it groups on the SAME grid as */
/* the timeline/search — a filtered view (g_gallery_*). Members already */
/* paged into the library are reused; the rest are appended as hidden */
/* g_asset entries (id + thumb only) whose date/aspect the metadata */
/* worker fills in lazily, so the justified grid sorts into month */
/* sections and true aspect ratios as the data arrives. */
/* ------------------------------------------------------------------ */
/* g_asset index for an id: an existing entry, or a freshly appended hidden one
* (lets the gallery show a marker not yet paged into the library). -1 on OOM. */
static int asset_for_id(const char *id)
{
for (int i = 0; i < g_asset_count; i++)
if (!strcmp(g_asset_ids[i], id))
return i;
if (!grow_assets(g_asset_count + 1))
return -1;
int idx = g_asset_count++;
snprintf(g_asset_ids[idx], sizeof(g_asset_ids[0]), "%s", id);
g_asset_dates[idx][0] = '\0'; /* "" = unresolved; meta_worker fills it */
g_asset_ratio[idx] = 0.0f;
g_asset_rot[idx] = 0;
g_asset_is_video[idx] = 0;
g_asset_local_backed[idx] = 0;
g_asset_hidden[idx] = 1; /* gallery-only: kept out of the timeline */
g_thumb[idx] = NULL;
g_thumb_failed[idx] = 0;
g_tex_err[idx][0] = '\0';
return idx;
}
/* open the gallery for cluster c: collect its members (same merc cell as the
* bubble) into g_gallery_idx, then rebuild the grid to show them */
static void gallery_open(int c)
{
g_gallery_count = 0;
if (c < 0 || c >= g_cl_count || g_map_scale <= 0.0)
return;
double qscale = pow(2.0, round(log2(g_map_scale)));
double cellsz = (double)MAP_CELL / qscale;
int tgx = g_cl_gx[c], tgy = g_cl_gy[c];
for (int i = 0; i < g_map_count; i++) {
double mx = merc_x(g_map_lon[i]), my = merc_y(g_map_lat[i]);
float sx, sy;
map_project(mx, my, &sx, &sy);
if (sx < -90 || sx > SCREEN_W + 90 ||
sy < -90 || sy > SCREEN_H + 90)
continue;
if ((int)(mx / cellsz) != tgx || (int)(my / cellsz) != tgy)
continue;
int idx = asset_for_id(g_map_id[i]);
if (idx < 0 || !grow_gallery(g_gallery_count + 1))
break;
g_gallery_idx[g_gallery_count++] = idx;
}
g_gallery_active = 1;
rebuild_display();
}
static void gallery_close(void)
{
g_gallery_active = 0;
rebuild_display();
}
/* lazy fill: apply a finished metadata result (re-laying out the grid so the
* item drops into its month/aspect), then request the next missing one —
* visible slots first. g_meta_target is a g_asset index, so it stays valid
* across the rebuilds that reorder g_disp. */
static void gallery_meta_pump(int first_vis, int last_vis,
int *sel, float *scroll, float *target)
{
if (g_meta_state == META_DONE) {
int t = g_meta_target;
if (t >= 0 && t < g_asset_count && g_meta_date[0]) {
snprintf(g_asset_dates[t], sizeof(g_asset_dates[0]), "%s",
g_meta_date);
g_asset_ratio[t] = g_meta_ratio;
g_asset_rot[t] = g_meta_rot;
g_asset_is_video[t] = (unsigned char)g_meta_is_video;
} else if (t >= 0 && t < g_asset_count) {
/* fetch failed: stamp a placeholder date so we don't
* spin on it forever (it just won't group by month) */
snprintf(g_asset_dates[t], sizeof(g_asset_dates[0]),
"0000-00-00");
}
g_meta_state = META_IDLE;
rebuild_keep_view(sel, scroll, target);
}
if (g_meta_state != META_IDLE)
return;
int pick = -1;
for (int d = first_vis; pick < 0 && d >= 0 && d <= last_vis &&
d < g_disp_count; d++)
if (g_disp[d].src == SRC_SERVER &&
g_asset_dates[g_disp[d].idx][0] == '\0')
pick = g_disp[d].idx;
for (int k = 0; pick < 0 && k < g_gallery_count; k++)
if (g_asset_dates[g_gallery_idx[k]][0] == '\0')
pick = g_gallery_idx[k];
if (pick < 0)
return;
g_meta_target = pick;
snprintf(g_meta_id, sizeof(g_meta_id), "%s", g_asset_ids[pick]);
__sync_synchronize();
g_meta_state = META_PENDING;
}
+35
View File
@@ -63,6 +63,29 @@ static int g_sect_count;
static int g_sect_cap;
static float g_content_h; /* total scrollable height incl. headers */
/* map cluster gallery: the photos of one map bubble, shown on the SAME grid as
* the timeline/search — a filtered view like g_search_*. g_gallery_idx holds
* indices into the g_asset_* arrays for the bubble's members; members not
* already paged into the library are appended as g_asset entries flagged
* g_asset_hidden (so they appear only here, never in the main timeline). Their
* date/aspect are resolved lazily by the metadata worker below, so the grid
* reflows into real month sections + true aspect ratios as they arrive. */
static int g_gallery_active;
static int *g_gallery_idx;
static int g_gallery_count;
static int g_gallery_cap;
/* background metadata resolver: a one-slot handoff (mirroring the thumb worker)
* that fetches one asset's date/aspect so the justified grid can place it. */
enum { META_IDLE, META_PENDING, META_DONE };
static volatile int g_meta_state = META_IDLE;
static char g_meta_id[40]; /* asset id to resolve (main -> worker) */
static int g_meta_target; /* g_asset index to fill (main-thread owned) */
static char g_meta_date[DATELEN]; /* results (worker -> main) */
static float g_meta_ratio;
static unsigned char g_meta_rot;
static int g_meta_is_video;
/* free-text smart (CLIP) search. when active the grid shows only the matching
* server assets — still date-grouped under month headers like the timeline —
* instead of the full merged library; TRIANGLE opens the keyboard, CIRCLE (or
@@ -159,6 +182,7 @@ static int grow_assets(int need)
!GROW(g_asset_rot, c, need) ||
!GROW(g_asset_is_video, c, need) ||
!GROW(g_asset_local_backed, c, need) ||
!GROW(g_asset_hidden, c, need) ||
!GROW(g_thumb, c, need) ||
!GROW(g_thumb_failed, c, need) ||
!(g_asset_cap = GROW(g_tex_err, c, need)))
@@ -214,6 +238,17 @@ static int grow_sect(int need)
return 1;
}
static int grow_gallery(int need)
{
if (need <= g_gallery_cap)
return 1;
int cap = GROW(g_gallery_idx, g_gallery_cap, need);
if (!cap)
return 0;
g_gallery_cap = cap;
return 1;
}
static void log_line(const char *fmt, ...)
{
FILE *f = fopen(LOG_PATH, "a");
+13 -9
View File
@@ -1,9 +1,10 @@
#!/usr/bin/env python3
"""Generate a Web-Mercator tile pyramid (LODs) from a MapChart world PNG and
"""Generate a Web-Mercator tile pyramid (LODs) from a world map image and
pack it into one file for the Vita map view.
The source PNG covers the same cropped-Mercator region as before: longitude
-180..180 across its full width, latitude band [MAP_IMG_MY0, MAP_IMG_MY1].
The source image covers a cropped-Mercator region: longitude spans its full
width, latitude band [MAP_IMG_MY0, MAP_IMG_MY1]. The default source carries a
baked-in 10° graticule, against which those bounds were fit (see map.c).
Each LOD level is the source scaled to a width, padded to whole 256px tiles,
and cut up; tiles are JPEG (decoded into pooled textures on device, the safe
path). Everything goes into assets/maptiles.pak:
@@ -18,12 +19,15 @@ offsets are from the start of the JPEG blob.
import sys, struct, io
from PIL import Image
SRC = sys.argv[1] if len(sys.argv) > 1 else '/home/sads/Desktop/MapChart_Map (3).png'
SRC = sys.argv[1] if len(sys.argv) > 1 else '/home/sads/Desktop/world map cities.jpeg'
OUT = sys.argv[2] if len(sys.argv) > 2 else '/home/sads/Code/vitaImmich/assets/maptiles.pak'
TILE = 256
QUALITY = 90
# level widths (px) for the full map; last is ~native. heights follow aspect.
WIDTHS = [512, 1024, 2048, 4096, 7000]
QUALITY = 94
# level widths (px). The last entry (10496 ~= 1.5x the 7000px source) is a
# LANCZOS supersample: it adds no true detail beyond native, but lets a zoomed-in
# view sample crisp pre-upscaled tiles instead of the GPU bilinear-magnifying the
# native level, so city labels read sharper. heights follow aspect.
WIDTHS = [512, 1024, 2048, 4096, 7000, 10496]
src = Image.open(SRC).convert('RGB')
SW, SH = src.size
@@ -31,8 +35,8 @@ print('source', SW, SH, 'aspect', round(SW/SH, 4))
levels = [] # (imgW,imgH,cols,rows,Image-padded)
for w in WIDTHS:
if w > SW:
w = SW
if w > 2 * SW: # allow a modest supersample, but cap runaway upscales
w = 2 * SW
h = round(w * SH / SW)
img = src.resize((w, h), Image.LANCZOS)
cols = (w + TILE - 1) // TILE