diff --git a/assets/maptiles.pak b/assets/maptiles.pak index 1ee3b9f..842d6b1 100644 Binary files a/assets/maptiles.pak and b/assets/maptiles.pak differ diff --git a/src/api.c b/src/api.c index 0d40481..200aed7 100644 --- a/src/api.c +++ b/src/api.c @@ -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; +} + diff --git a/src/app.c b/src/app.c index 96cc879..015fa09 100644 --- a/src/app.c +++ b/src/app.c @@ -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(); diff --git a/src/display.c b/src/display.c index ebacec2..a447560 100644 --- a/src/display.c +++ b/src/display.c @@ -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++; diff --git a/src/main.c b/src/main.c index ff3449c..3e20c4b 100644 --- a/src/main.c +++ b/src/main.c @@ -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 */ diff --git a/src/map.c b/src/map.c index f5e4000..d8854af 100644 --- a/src/map.c +++ b/src/map.c @@ -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; +} diff --git a/src/state.c b/src/state.c index 0ca8a02..101ef24 100644 --- a/src/state.c +++ b/src/state.c @@ -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"); diff --git a/tools/png_to_tiles.py b/tools/png_to_tiles.py index 253f0be..740c00f 100644 --- a/tools/png_to_tiles.py +++ b/tools/png_to_tiles.py @@ -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