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:
Binary file not shown.
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user