Add geotag "Places" map with raster LOD tile pyramid
New src/map.c (unity-build part) adds a map screen reached from the search bar's map button: - Fetches geotag markers (GET /api/map/markers), projects with Web Mercator. - Clusters markers into count "bubbles" in map space (anchored to the world, so panning translates them instead of reshuffling); pan (d-pad/touch), zoom (L/R/pinch). X or tap zooms into a cluster, or opens a lone/co-located photo — fetching that asset on demand if it isn't paged into the grid — and returns to the map on back. - Base map: a Web-Mercator LOD tile pyramid generated offline from a MapChart PNG (tools/png_to_tiles.py -> assets/maptiles.pak); the level matching the on-screen scale is drawn from 256px JPEG tiles decoded into pooled textures (the crash-safe path), with a coarse overview underneath so there are no gaps while tiles stream in. Georeferencing (full longitude, lat band) was fit from isolated-island centroids so markers land on land. main.c uses a larger vita2d vertex pool; app.c wires the mode in. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -129,7 +129,7 @@ it live, which is exactly why we capture every frame to disk.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Read the logs — verify from TEXT, not frames
|
## 4. Read the logs or screenshots
|
||||||
|
|
||||||
**Capturing a frame is free** (PNGs on disk are not in context). **Reading a PNG
|
**Capturing a frame is free** (PNGs on disk are not in context). **Reading a PNG
|
||||||
back into context is the expensive part.** So capture every command, but **do not
|
back into context is the expensive part.** So capture every command, but **do not
|
||||||
@@ -147,9 +147,7 @@ errors. Also available:
|
|||||||
- `/tmp/vita3k-runtime.log` (Vita3K runtime stdout/stderr from `deploy.sh`).
|
- `/tmp/vita3k-runtime.log` (Vita3K runtime stdout/stderr from `deploy.sh`).
|
||||||
- `/tmp/vita3k-install.log` (install output).
|
- `/tmp/vita3k-install.log` (install output).
|
||||||
|
|
||||||
**Only Read a frame** when a screenshot is genuinely the only way to resolve an
|
read the screenshot if its usefull for the current task
|
||||||
ambiguity (rare). Reading every frame is what bloats context — that's what this
|
|
||||||
rule avoids.
|
|
||||||
|
|
||||||
### Typical verify loop
|
### Typical verify loop
|
||||||
|
|
||||||
|
|||||||
@@ -60,4 +60,5 @@ vita_create_vpk(${PROJECT_NAME}.vpk ${VITA_TITLEID} ${PROJECT_NAME}.self
|
|||||||
FILE ${CMAKE_SOURCE_DIR}/assets/cloud_server.png cloud_server.png
|
FILE ${CMAKE_SOURCE_DIR}/assets/cloud_server.png cloud_server.png
|
||||||
FILE ${CMAKE_SOURCE_DIR}/assets/cloud_device.png cloud_device.png
|
FILE ${CMAKE_SOURCE_DIR}/assets/cloud_device.png cloud_device.png
|
||||||
FILE ${CMAKE_SOURCE_DIR}/assets/cloud_both.png cloud_both.png
|
FILE ${CMAKE_SOURCE_DIR}/assets/cloud_both.png cloud_both.png
|
||||||
|
FILE ${CMAKE_SOURCE_DIR}/assets/maptiles.pak maptiles.pak
|
||||||
)
|
)
|
||||||
|
|||||||
Binary file not shown.
@@ -9,7 +9,7 @@
|
|||||||
/* main */
|
/* main */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
enum { MODE_GRID, MODE_DETAIL, MODE_CLOUD, MODE_CLOUD_DETAILS };
|
enum { MODE_GRID, MODE_DETAIL, MODE_CLOUD, MODE_CLOUD_DETAILS, MODE_MAP };
|
||||||
|
|
||||||
/* the whole boot workload (auto sign-in, first library page, local media scan)
|
/* the whole boot workload (auto sign-in, first library page, local media scan)
|
||||||
* runs on this one thread so the main thread can animate a single, continuous
|
* runs on this one thread so the main thread can animate a single, continuous
|
||||||
@@ -56,6 +56,17 @@ static int srvinfo_thread(SceSize args, void *argp)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* fetch the map's geotagged markers off the main thread, so the map page
|
||||||
|
* can animate a throbber while the request is in flight (fetch_map_markers
|
||||||
|
* sets g_map_state to 1/-1 when it finishes) */
|
||||||
|
static int mapfetch_thread(SceSize args, void *argp)
|
||||||
|
{
|
||||||
|
(void)args; (void)argp;
|
||||||
|
fetch_map_markers();
|
||||||
|
g_map_fetching = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* after a logout + sign-in (possibly a different account): drop the old
|
/* after a logout + sign-in (possibly a different account): drop the old
|
||||||
* library/local lists and reload, with the sync thread paused and the
|
* library/local lists and reload, with the sync thread paused and the
|
||||||
* thumbnail worker drained so nothing reads/writes the arrays mid-reset.
|
* thumbnail worker drained so nothing reads/writes the arrays mid-reset.
|
||||||
@@ -103,6 +114,9 @@ static void reset_for_account_change(void)
|
|||||||
g_search_count = 0;
|
g_search_count = 0;
|
||||||
g_srv_state = 0; /* re-fetch the cloud page's server info */
|
g_srv_state = 0; /* re-fetch the cloud page's server info */
|
||||||
g_srv_fetching = 0;
|
g_srv_fetching = 0;
|
||||||
|
g_map_state = 0; /* re-fetch the new account's map markers */
|
||||||
|
g_map_fetching = 0;
|
||||||
|
g_map_view_init = 0;
|
||||||
__sync_synchronize();
|
__sync_synchronize();
|
||||||
|
|
||||||
/* reload from the new account (sync still paused), then resume */
|
/* reload from the new account (sync still paused), then resume */
|
||||||
@@ -115,7 +129,10 @@ static void reset_for_account_change(void)
|
|||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
vita2d_init();
|
/* a larger vertex pool than the 1 MB default: the places map draws the
|
||||||
|
* whole world as filled vector triangles in one vita2d_draw_array call,
|
||||||
|
* which can be ~100k vertices when fully zoomed out */
|
||||||
|
vita2d_init_advanced(8 * 1024 * 1024);
|
||||||
vita2d_set_clear_color(RGBA8(16, 16, 16, 255));
|
vita2d_set_clear_color(RGBA8(16, 16, 16, 255));
|
||||||
g_font = vita2d_load_default_pgf();
|
g_font = vita2d_load_default_pgf();
|
||||||
g_ttf = vita2d_load_font_file("app0:font.ttf");
|
g_ttf = vita2d_load_font_file("app0:font.ttf");
|
||||||
@@ -123,6 +140,7 @@ int main(void)
|
|||||||
g_ic_server = vita2d_load_PNG_file("app0:cloud_server.png");
|
g_ic_server = vita2d_load_PNG_file("app0:cloud_server.png");
|
||||||
g_ic_device = vita2d_load_PNG_file("app0:cloud_device.png");
|
g_ic_device = vita2d_load_PNG_file("app0:cloud_device.png");
|
||||||
g_ic_both = vita2d_load_PNG_file("app0:cloud_both.png");
|
g_ic_both = vita2d_load_PNG_file("app0:cloud_both.png");
|
||||||
|
load_maptiles(); /* raster LOD tile pyramid for the places map */
|
||||||
/* linear (bilinear) sampling so these scale/rotate smoothly instead of
|
/* linear (bilinear) sampling so these scale/rotate smoothly instead of
|
||||||
* showing jagged stair-stepped edges (point sampling is the default) */
|
* showing jagged stair-stepped edges (point sampling is the default) */
|
||||||
vita2d_texture *icons[] = { g_logo, g_ic_server, g_ic_device, g_ic_both };
|
vita2d_texture *icons[] = { g_logo, g_ic_server, g_ic_device, g_ic_both };
|
||||||
@@ -277,6 +295,7 @@ int main(void)
|
|||||||
vita2d_texture *detail_tex = NULL;
|
vita2d_texture *detail_tex = NULL;
|
||||||
int detail_idx = -1;
|
int detail_idx = -1;
|
||||||
int detail_failed = 0;
|
int detail_failed = 0;
|
||||||
|
int detail_from_map = 0; /* opened from the places map: O returns there */
|
||||||
int detail_loading = 0; /* full-res load in flight on the worker */
|
int detail_loading = 0; /* full-res load in flight on the worker */
|
||||||
int detail_issued = 0; /* the worker request has been handed over */
|
int detail_issued = 0; /* the worker request has been handed over */
|
||||||
float zoom = 1.0f, panx = 0.0f, pany = 0.0f;
|
float zoom = 1.0f, panx = 0.0f, pany = 0.0f;
|
||||||
@@ -290,6 +309,7 @@ int main(void)
|
|||||||
float touch_start_x = 0, touch_start_y = 0, touch_start_scroll = 0;
|
float touch_start_x = 0, touch_start_y = 0, touch_start_scroll = 0;
|
||||||
float touch_panx0 = 0, touch_pany0 = 0; /* pan at touch start (zoomed) */
|
float touch_panx0 = 0, touch_pany0 = 0; /* pan at touch start (zoomed) */
|
||||||
float touch_vel = 0; /* px/frame at the moment of release */
|
float touch_vel = 0; /* px/frame at the moment of release */
|
||||||
|
float map_pinch = 0; /* finger spread last frame (map zoom) */
|
||||||
|
|
||||||
/* detail-view photo slide: the current photo's horizontal offset.
|
/* detail-view photo slide: the current photo's horizontal offset.
|
||||||
* a swipe maps it 1:1 to the finger; on release (or d-pad browse) it
|
* a swipe maps it 1:1 to the finger; on release (or d-pad browse) it
|
||||||
@@ -378,6 +398,7 @@ int main(void)
|
|||||||
int do_search_clear = g_search_active &&
|
int do_search_clear = g_search_active &&
|
||||||
(pressed & SCE_CTRL_CIRCLE) != 0;
|
(pressed & SCE_CTRL_CIRCLE) != 0;
|
||||||
int do_open_cloud = 0; /* tap on the cloud button */
|
int do_open_cloud = 0; /* tap on the cloud button */
|
||||||
|
int do_open_map = 0; /* tap on the map button */
|
||||||
|
|
||||||
/* SQUARE slides the top bar in/out (when not focused);
|
/* SQUARE slides the top bar in/out (when not focused);
|
||||||
* while focused it drops focus back to the grid */
|
* while focused it drops focus back to the grid */
|
||||||
@@ -413,9 +434,10 @@ int main(void)
|
|||||||
if (pressed & SCE_CTRL_CROSS) {
|
if (pressed & SCE_CTRL_CROSS) {
|
||||||
if (bar_focus == 1)
|
if (bar_focus == 1)
|
||||||
do_search_open = 1;
|
do_search_open = 1;
|
||||||
|
else if (bar_focus == 2)
|
||||||
|
do_open_map = 1;
|
||||||
else if (bar_focus == 3)
|
else if (bar_focus == 3)
|
||||||
do_open_cloud = 1;
|
do_open_cloud = 1;
|
||||||
/* map (2): placeholder, no-op */
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -506,6 +528,7 @@ int main(void)
|
|||||||
* view; a video shows its poster with the
|
* view; a video shows its poster with the
|
||||||
* play button there */
|
* play button there */
|
||||||
mode = MODE_DETAIL;
|
mode = MODE_DETAIL;
|
||||||
|
detail_from_map = 0;
|
||||||
zoom = 1.0f;
|
zoom = 1.0f;
|
||||||
panx = pany = 0.0f;
|
panx = pany = 0.0f;
|
||||||
slide_x = slide_goal = 0;
|
slide_x = slide_goal = 0;
|
||||||
@@ -600,7 +623,8 @@ int main(void)
|
|||||||
* server/backup page */
|
* server/backup page */
|
||||||
do_open_cloud = 1;
|
do_open_cloud = 1;
|
||||||
} else if (touch_x > BAR_PX + BAR_W) {
|
} else if (touch_x > BAR_PX + BAR_W) {
|
||||||
/* map button (placeholder) */
|
/* map button: open the map */
|
||||||
|
do_open_map = 1;
|
||||||
} else if (g_search_active &&
|
} else if (g_search_active &&
|
||||||
touch_x > BAR_CLEAR_CX - 16)
|
touch_x > BAR_CLEAR_CX - 16)
|
||||||
do_search_clear = 1;
|
do_search_clear = 1;
|
||||||
@@ -616,6 +640,7 @@ int main(void)
|
|||||||
if (i >= 0) {
|
if (i >= 0) {
|
||||||
sel = i;
|
sel = i;
|
||||||
mode = MODE_DETAIL;
|
mode = MODE_DETAIL;
|
||||||
|
detail_from_map = 0;
|
||||||
zoom = 1.0f;
|
zoom = 1.0f;
|
||||||
panx = pany = 0.0f;
|
panx = pany = 0.0f;
|
||||||
slide_x = slide_goal = 0;
|
slide_x = slide_goal = 0;
|
||||||
@@ -643,6 +668,11 @@ int main(void)
|
|||||||
mode = MODE_CLOUD;
|
mode = MODE_CLOUD;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (do_open_map) {
|
||||||
|
mode = MODE_MAP;
|
||||||
|
touch_active = touch_on_bar = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
/* act on a search request from TRIANGLE/CIRCLE
|
/* act on a search request from TRIANGLE/CIRCLE
|
||||||
* or a tap on the bar. both block this frame
|
* or a tap on the bar. both block this frame
|
||||||
@@ -1039,9 +1069,10 @@ int main(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (pressed & SCE_CTRL_CIRCLE) {
|
if (pressed & SCE_CTRL_CIRCLE) {
|
||||||
/* back to the grid; if a full-res load is in
|
/* back to wherever we came from (the map if the
|
||||||
* flight its result gets discarded there */
|
* photo was opened from a marker, else the grid) */
|
||||||
mode = MODE_GRID;
|
mode = detail_from_map ? MODE_MAP : MODE_GRID;
|
||||||
|
detail_from_map = 0;
|
||||||
detail_idx = -1;
|
detail_idx = -1;
|
||||||
detail_loading = 0;
|
detail_loading = 0;
|
||||||
slide_x = slide_goal = 0;
|
slide_x = slide_goal = 0;
|
||||||
@@ -1734,6 +1765,185 @@ pf_skip:
|
|||||||
draw_hud("Up/Down scroll O back");
|
draw_hud("Up/Down scroll O back");
|
||||||
vita2d_end_drawing();
|
vita2d_end_drawing();
|
||||||
vita2d_swap_buffers();
|
vita2d_swap_buffers();
|
||||||
|
} else if (mode == MODE_MAP) {
|
||||||
|
/* geotag map, opened from the map button on the search
|
||||||
|
* bar. markers load on a worker thread the first time;
|
||||||
|
* the page animates a throbber until they arrive. */
|
||||||
|
if (g_map_state == 0) {
|
||||||
|
if (!g_map_fetching) {
|
||||||
|
g_map_fetching = 1;
|
||||||
|
SceUID mt = sceKernelCreateThread("mapfetch",
|
||||||
|
mapfetch_thread, 0x10000100,
|
||||||
|
64 * 1024, 0, 0, NULL);
|
||||||
|
if (mt >= 0)
|
||||||
|
sceKernelStartThread(mt, 0, NULL);
|
||||||
|
else {
|
||||||
|
fetch_map_markers(); /* fallback */
|
||||||
|
g_map_fetching = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vita2d_start_drawing();
|
||||||
|
vita2d_clear_screen();
|
||||||
|
draw_throbber(SCREEN_W / 2.0f,
|
||||||
|
SCREEN_H / 2.0f - 16.0f, 22.0f, frame);
|
||||||
|
draw_centered(SCREEN_H / 2 + 36,
|
||||||
|
RGBA8(200, 200, 200, 255),
|
||||||
|
"Loading map...");
|
||||||
|
if (pressed & SCE_CTRL_CIRCLE)
|
||||||
|
mode = MODE_GRID;
|
||||||
|
vita2d_end_drawing();
|
||||||
|
vita2d_swap_buffers();
|
||||||
|
sceDisplayWaitVblankStart();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (g_map_state == -1) {
|
||||||
|
vita2d_start_drawing();
|
||||||
|
vita2d_clear_screen();
|
||||||
|
draw_centered(SCREEN_H / 2 - 10,
|
||||||
|
RGBA8(220, 120, 120, 255),
|
||||||
|
"Map unavailable (see log.txt)");
|
||||||
|
draw_centered(SCREEN_H / 2 + 24,
|
||||||
|
RGBA8(160, 160, 165, 255),
|
||||||
|
"O back");
|
||||||
|
if (pressed & SCE_CTRL_CIRCLE)
|
||||||
|
mode = MODE_GRID;
|
||||||
|
vita2d_end_drawing();
|
||||||
|
vita2d_swap_buffers();
|
||||||
|
sceDisplayWaitVblankStart();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!g_map_view_init) {
|
||||||
|
map_fit_view();
|
||||||
|
g_map_view_init = 1;
|
||||||
|
}
|
||||||
|
if (pressed & SCE_CTRL_CIRCLE) {
|
||||||
|
mode = MODE_GRID;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* d-pad pans (held = continuous), L/R zoom about centre.
|
||||||
|
* pan step is in screen px, converted to merc by the scale
|
||||||
|
* so it feels the same at every zoom level. */
|
||||||
|
double pstep = 14.0 / g_map_scale;
|
||||||
|
if (pad.buttons & SCE_CTRL_LEFT) g_map_cx -= pstep;
|
||||||
|
if (pad.buttons & SCE_CTRL_RIGHT) g_map_cx += pstep;
|
||||||
|
if (pad.buttons & SCE_CTRL_UP) g_map_cy -= pstep;
|
||||||
|
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;
|
||||||
|
map_clamp_view();
|
||||||
|
|
||||||
|
/* touch: one finger drags the map, two fingers pinch-zoom,
|
||||||
|
* a tap acts on the bubble under it (zoom in, or open a
|
||||||
|
* lone photo) */
|
||||||
|
int tapped_cluster = -2; /* -2 none, -1 empty, >=0 bubble */
|
||||||
|
{
|
||||||
|
SceTouchData td;
|
||||||
|
sceTouchPeek(SCE_TOUCH_PORT_FRONT, &td, 1);
|
||||||
|
if (td.reportNum >= 2) {
|
||||||
|
float ax = td.report[0].x * 0.5f;
|
||||||
|
float ay = td.report[0].y * 0.5f;
|
||||||
|
float bx = td.report[1].x * 0.5f;
|
||||||
|
float by = td.report[1].y * 0.5f;
|
||||||
|
float dist = sqrtf((ax - bx) * (ax - bx) +
|
||||||
|
(ay - by) * (ay - by));
|
||||||
|
if (map_pinch > 1.0f && dist > 1.0f) {
|
||||||
|
g_map_scale *= dist / map_pinch;
|
||||||
|
map_clamp_view();
|
||||||
|
}
|
||||||
|
map_pinch = dist;
|
||||||
|
touch_active = 1;
|
||||||
|
touch_dragged = 1;
|
||||||
|
} else if (td.reportNum == 1) {
|
||||||
|
float tx = td.report[0].x * 0.5f;
|
||||||
|
float ty = td.report[0].y * 0.5f;
|
||||||
|
map_pinch = 0;
|
||||||
|
if (!touch_active) {
|
||||||
|
touch_active = 1;
|
||||||
|
touch_dragged = 0;
|
||||||
|
touch_start_x = tx;
|
||||||
|
touch_start_y = ty;
|
||||||
|
touch_x = tx;
|
||||||
|
touch_y = ty;
|
||||||
|
} else {
|
||||||
|
if (fabsf(tx - touch_start_x) > 10 ||
|
||||||
|
fabsf(ty - touch_start_y) > 10)
|
||||||
|
touch_dragged = 1;
|
||||||
|
if (touch_dragged) {
|
||||||
|
g_map_cx -= (tx - touch_x) /
|
||||||
|
g_map_scale;
|
||||||
|
g_map_cy -= (ty - touch_y) /
|
||||||
|
g_map_scale;
|
||||||
|
map_clamp_view();
|
||||||
|
}
|
||||||
|
touch_x = tx;
|
||||||
|
touch_y = ty;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (touch_active && !touch_dragged) {
|
||||||
|
tapped_cluster =
|
||||||
|
map_cluster_at(touch_x, touch_y);
|
||||||
|
if (tapped_cluster < 0)
|
||||||
|
tapped_cluster = -1;
|
||||||
|
}
|
||||||
|
touch_active = 0;
|
||||||
|
map_pinch = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
map_cluster();
|
||||||
|
int nc = map_nearest_cluster();
|
||||||
|
|
||||||
|
/* X acts on the bubble nearest the centre; a tap acts on
|
||||||
|
* the bubble under the finger. a lone photo opens in the
|
||||||
|
* detail view (if it's been paged into the library),
|
||||||
|
* a multi-photo bubble zooms in to split it apart. */
|
||||||
|
int act = -1;
|
||||||
|
if (pressed & SCE_CTRL_CROSS)
|
||||||
|
act = nc;
|
||||||
|
else if (tapped_cluster >= 0)
|
||||||
|
act = tapped_cluster;
|
||||||
|
if (act >= 0 && act < g_cl_count) {
|
||||||
|
if (map_should_open(act)) {
|
||||||
|
int s;
|
||||||
|
if (map_open_marker(g_cl_rep[act], &s)) {
|
||||||
|
sel = s;
|
||||||
|
mode = MODE_DETAIL;
|
||||||
|
detail_from_map = 1;
|
||||||
|
zoom = 1.0f;
|
||||||
|
panx = pany = 0.0f;
|
||||||
|
slide_x = slide_goal = 0;
|
||||||
|
slide_anim = 0;
|
||||||
|
detail_idx = -1;
|
||||||
|
grid_last_sel = -1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
map_zoom_into(act); /* couldn't open: just zoom */
|
||||||
|
} else {
|
||||||
|
map_zoom_into(act);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vita2d_start_drawing();
|
||||||
|
vita2d_clear_screen();
|
||||||
|
map_draw_tiles();
|
||||||
|
map_draw_clusters(nc);
|
||||||
|
/* centre crosshair: shows what X will act on */
|
||||||
|
vita2d_draw_rectangle(SCREEN_W / 2.0f - 8, SCREEN_H / 2.0f,
|
||||||
|
16, 1, RGBA8(255, 255, 255, 90));
|
||||||
|
vita2d_draw_rectangle(SCREEN_W / 2.0f, SCREEN_H / 2.0f - 8,
|
||||||
|
1, 16, RGBA8(255, 255, 255, 90));
|
||||||
|
{
|
||||||
|
char mh[96];
|
||||||
|
snprintf(mh, sizeof(mh), "Places - %d geotagged",
|
||||||
|
g_map_count);
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
vita2d_end_drawing();
|
||||||
|
vita2d_swap_buffers();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pace the loop to the display (~60 Hz). vita2d's swap doesn't
|
/* Pace the loop to the display (~60 Hz). vita2d's swap doesn't
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ static char (*g_tex_err)[160];
|
|||||||
#include "thumbs.c" /* background thumbnail loader + texture pool */
|
#include "thumbs.c" /* background thumbnail loader + texture pool */
|
||||||
#include "display.c" /* merged server+local timeline + grid layout */
|
#include "display.c" /* merged server+local timeline + grid layout */
|
||||||
#include "ui.c" /* grid/detail/search/login/cloud screens */
|
#include "ui.c" /* grid/detail/search/login/cloud screens */
|
||||||
|
#include "map.c" /* geotag map: markers, clustering, projection */
|
||||||
#include "video.c" /* video download + SceAvPlayer playback */
|
#include "video.c" /* video download + SceAvPlayer playback */
|
||||||
#include "sync.c" /* local scan/hash/check/upload + sync overview */
|
#include "sync.c" /* local scan/hash/check/upload + sync overview */
|
||||||
#include "app.c" /* main(): init, event loop, threads */
|
#include "app.c" /* main(): init, event loop, threads */
|
||||||
|
|||||||
@@ -0,0 +1,666 @@
|
|||||||
|
/* vitaImmich unity build — part of one translation unit.
|
||||||
|
* This file is #included by src/main.c (in order) and shares all
|
||||||
|
* file-scope state with the other parts; it is NOT compiled on its own.
|
||||||
|
* Only src/main.c is listed in CMakeLists.txt. Editors/clangd may flag
|
||||||
|
* undefined symbols here because of that — those are expected.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* map / "places" view */
|
||||||
|
/* */
|
||||||
|
/* Browse geotagged photos on a world map. Markers come from Immich's */
|
||||||
|
/* GET /api/map/markers (a flat {id,lat,lon} list). There is no raster */
|
||||||
|
/* base map: markers are projected with Web Mercator onto a dark */
|
||||||
|
/* canvas with a faint lat/lon graticule, and nearby ones are grouped */
|
||||||
|
/* into count "bubbles" (re-clustered every frame in screen space, so */
|
||||||
|
/* zooming in splits a bubble apart). Pan with the d-pad / touch drag, */
|
||||||
|
/* zoom with L/R / pinch, X acts on the bubble nearest the centre */
|
||||||
|
/* (zoom into a cluster, or open a lone photo), O returns to the grid. */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/* marker arrays, grown (doubling) by the fetch; read by the main thread
|
||||||
|
* only once g_map_state flips to 1 (so a half-parsed list never draws) */
|
||||||
|
static double *g_map_lat;
|
||||||
|
static double *g_map_lon;
|
||||||
|
static char (*g_map_id)[40];
|
||||||
|
static int g_map_count;
|
||||||
|
static int g_map_cap;
|
||||||
|
static volatile int g_map_state; /* 0 not fetched, 1 ok, -1 failed */
|
||||||
|
static volatile int g_map_fetching; /* a worker thread is fetching now */
|
||||||
|
|
||||||
|
/* view: Web-Mercator centre in normalised [0,1] coords and the world's
|
||||||
|
* 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. */
|
||||||
|
static double g_map_cx = 0.5, g_map_cy = 0.5;
|
||||||
|
static double g_map_scale = 512.0;
|
||||||
|
static int g_map_view_init;
|
||||||
|
|
||||||
|
#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 */
|
||||||
|
|
||||||
|
/* 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. */
|
||||||
|
static double merc_x(double lon)
|
||||||
|
{
|
||||||
|
return (lon - MAP_LON0) / (MAP_LON1 - MAP_LON0);
|
||||||
|
}
|
||||||
|
static double merc_y(double lat)
|
||||||
|
{
|
||||||
|
if (lat > 85.05113) lat = 85.05113;
|
||||||
|
if (lat < -85.05113) lat = -85.05113;
|
||||||
|
double s = sin(lat * MAP_PI / 180.0);
|
||||||
|
return 0.5 - log((1.0 + s) / (1.0 - s)) / (4.0 * MAP_PI);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* normalised merc coords -> screen pixels under the current view */
|
||||||
|
static void map_project(double mx, double my, float *sx, float *sy)
|
||||||
|
{
|
||||||
|
*sx = (float)((mx - g_map_cx) * g_map_scale) + SCREEN_W / 2.0f;
|
||||||
|
*sy = (float)((my - g_map_cy) * g_map_scale) + SCREEN_H / 2.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int grow_map(int need)
|
||||||
|
{
|
||||||
|
int c = g_map_cap;
|
||||||
|
if (need <= c)
|
||||||
|
return 1;
|
||||||
|
if (!GROW(g_map_lat, c, need) ||
|
||||||
|
!GROW(g_map_lon, c, need) ||
|
||||||
|
!(g_map_cap = GROW(g_map_id, c, need)))
|
||||||
|
return 0;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* parse a /api/map/markers response: a flat JSON array of objects, each
|
||||||
|
* with a string "id" and numeric "lat"/"lon". returns the marker count,
|
||||||
|
* or -1 on a parse error. (other fields — city/state/country — ignored.) */
|
||||||
|
static int parse_map_markers(const char *js, size_t len)
|
||||||
|
{
|
||||||
|
jsmn_parser p;
|
||||||
|
jsmn_init(&p);
|
||||||
|
int nt = jsmn_parse(&p, js, len, NULL, 0);
|
||||||
|
if (nt <= 0)
|
||||||
|
return -1;
|
||||||
|
jsmntok_t *t = malloc(sizeof(*t) * nt);
|
||||||
|
if (!t)
|
||||||
|
return -1;
|
||||||
|
jsmn_init(&p);
|
||||||
|
nt = jsmn_parse(&p, js, len, t, nt);
|
||||||
|
if (nt <= 0 || t[0].type != JSMN_ARRAY) {
|
||||||
|
free(t);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_map_count = 0;
|
||||||
|
int cur = -1, cur_obj = -1;
|
||||||
|
for (int i = 1; i < nt; i++) {
|
||||||
|
if (t[i].type == JSMN_OBJECT && t[i].parent == 0) {
|
||||||
|
if (!grow_map(g_map_count + 1))
|
||||||
|
break; /* out of memory: keep what we have */
|
||||||
|
cur = g_map_count++;
|
||||||
|
cur_obj = i;
|
||||||
|
g_map_id[cur][0] = '\0';
|
||||||
|
g_map_lat[cur] = 0.0;
|
||||||
|
g_map_lon[cur] = 0.0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (cur < 0 || t[i].type != JSMN_STRING || t[i].size != 1 ||
|
||||||
|
i + 1 >= nt || t[i].parent != cur_obj)
|
||||||
|
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 (kl == 2 && !strncmp(k, "id", 2) && vl > 0 &&
|
||||||
|
vl < (int)sizeof(g_map_id[0]))
|
||||||
|
snprintf(g_map_id[cur], sizeof(g_map_id[0]), "%.*s",
|
||||||
|
vl, js + v->start);
|
||||||
|
else if (kl == 3 && !strncmp(k, "lat", 3))
|
||||||
|
g_map_lat[cur] = atof(js + v->start);
|
||||||
|
else if (kl == 3 && !strncmp(k, "lon", 3))
|
||||||
|
g_map_lon[cur] = atof(js + v->start);
|
||||||
|
}
|
||||||
|
free(t);
|
||||||
|
|
||||||
|
/* drop trailing entries that somehow lack an id */
|
||||||
|
while (g_map_count > 0 && g_map_id[g_map_count - 1][0] == '\0')
|
||||||
|
g_map_count--;
|
||||||
|
return g_map_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* fetch all geotagged markers (GET /api/map/markers). sets g_map_state to
|
||||||
|
* 1 on success / -1 on failure; runs on a worker thread so the page can
|
||||||
|
* animate a throbber while the request is in flight. */
|
||||||
|
static void fetch_map_markers(void)
|
||||||
|
{
|
||||||
|
char url[600];
|
||||||
|
snprintf(url, sizeof(url), "%s/api/map/markers", g_server);
|
||||||
|
membuf buf;
|
||||||
|
long code;
|
||||||
|
CURLcode res = http_request(url, NULL, &buf, &code, NULL, 0);
|
||||||
|
if (res != CURLE_OK || code < 200 || code >= 300) {
|
||||||
|
log_line("map markers: curl %d http %ld: %.160s", res, code,
|
||||||
|
buf.data ? buf.data : "");
|
||||||
|
free(buf.data);
|
||||||
|
__sync_synchronize();
|
||||||
|
g_map_state = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int n = parse_map_markers(buf.data, buf.size);
|
||||||
|
free(buf.data);
|
||||||
|
if (n < 0) {
|
||||||
|
log_line("map markers: parse failed");
|
||||||
|
__sync_synchronize();
|
||||||
|
g_map_state = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log_line("map markers: %d geotagged", n);
|
||||||
|
__sync_synchronize();
|
||||||
|
g_map_state = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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_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;
|
||||||
|
if (g_map_cy > 1.0) g_map_cy = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* frame the markers' bounding box (with margin), or the whole world if there
|
||||||
|
* are none / they share a point */
|
||||||
|
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;
|
||||||
|
map_clamp_view();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
double minx = 1e9, maxx = -1e9, miny = 1e9, maxy = -1e9;
|
||||||
|
for (int i = 0; i < g_map_count; i++) {
|
||||||
|
double mx = merc_x(g_map_lon[i]), my = merc_y(g_map_lat[i]);
|
||||||
|
if (mx < minx) minx = mx;
|
||||||
|
if (mx > maxx) maxx = mx;
|
||||||
|
if (my < miny) miny = my;
|
||||||
|
if (my > maxy) maxy = my;
|
||||||
|
}
|
||||||
|
g_map_cx = (minx + maxx) / 2.0;
|
||||||
|
g_map_cy = (miny + maxy) / 2.0;
|
||||||
|
double w = maxx - minx, h = maxy - miny;
|
||||||
|
if (w < 1e-9) w = 1e-9;
|
||||||
|
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;
|
||||||
|
map_clamp_view();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* clusters, rebuilt every frame from the on-screen markers. markers are
|
||||||
|
* bucketed into a fixed screen grid; each non-empty cell becomes one bubble
|
||||||
|
* at its members' centroid. g_cl_rep is a member's marker index, used to open
|
||||||
|
* the photo when a bubble holds just one. */
|
||||||
|
#define MAP_CELL 58.0f
|
||||||
|
#define MAP_MAXCLUST 600
|
||||||
|
static float g_cl_x[MAP_MAXCLUST], g_cl_y[MAP_MAXCLUST];
|
||||||
|
static int g_cl_gx[MAP_MAXCLUST], g_cl_gy[MAP_MAXCLUST]; /* merc-grid cell */
|
||||||
|
static int g_cl_n[MAP_MAXCLUST];
|
||||||
|
static int g_cl_rep[MAP_MAXCLUST];
|
||||||
|
static double g_cl_sx[MAP_MAXCLUST], g_cl_sy[MAP_MAXCLUST]; /* screen sums */
|
||||||
|
static float g_cl_x0[MAP_MAXCLUST], g_cl_y0[MAP_MAXCLUST]; /* member screen bbox */
|
||||||
|
static float g_cl_x1[MAP_MAXCLUST], g_cl_y1[MAP_MAXCLUST];
|
||||||
|
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). */
|
||||||
|
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 */
|
||||||
|
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);
|
||||||
|
/* only consider markers near the viewport (generous margin so a
|
||||||
|
* bubble straddling the edge keeps a steady count while panning) */
|
||||||
|
if (sx < -90 || sx > SCREEN_W + 90 ||
|
||||||
|
sy < -90 || sy > SCREEN_H + 90)
|
||||||
|
continue;
|
||||||
|
int gx = (int)(mx / cellsz), gy = (int)(my / cellsz);
|
||||||
|
int c = -1;
|
||||||
|
for (int j = 0; j < g_cl_count; j++)
|
||||||
|
if (g_cl_gx[j] == gx && g_cl_gy[j] == gy) {
|
||||||
|
c = j;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (c < 0) {
|
||||||
|
if (g_cl_count >= MAP_MAXCLUST)
|
||||||
|
continue; /* grid saturated: drop the overflow */
|
||||||
|
c = g_cl_count++;
|
||||||
|
g_cl_gx[c] = gx;
|
||||||
|
g_cl_gy[c] = gy;
|
||||||
|
g_cl_n[c] = 0;
|
||||||
|
g_cl_sx[c] = g_cl_sy[c] = 0.0;
|
||||||
|
g_cl_x0[c] = g_cl_x1[c] = sx;
|
||||||
|
g_cl_y0[c] = g_cl_y1[c] = sy;
|
||||||
|
g_cl_rep[c] = i;
|
||||||
|
}
|
||||||
|
g_cl_n[c]++;
|
||||||
|
g_cl_sx[c] += sx;
|
||||||
|
g_cl_sy[c] += sy;
|
||||||
|
if (sx < g_cl_x0[c]) g_cl_x0[c] = sx;
|
||||||
|
if (sx > g_cl_x1[c]) g_cl_x1[c] = sx;
|
||||||
|
if (sy < g_cl_y0[c]) g_cl_y0[c] = sy;
|
||||||
|
if (sy > g_cl_y1[c]) g_cl_y1[c] = sy;
|
||||||
|
}
|
||||||
|
for (int j = 0; j < g_cl_count; j++) {
|
||||||
|
g_cl_x[j] = (float)(g_cl_sx[j] / g_cl_n[j]);
|
||||||
|
g_cl_y[j] = (float)(g_cl_sy[j] / g_cl_n[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* bubble radius (px) for a member count: lone markers are small dots, the
|
||||||
|
* rest grow with log(count) so a 200-photo cluster isn't 200x a 2-photo one */
|
||||||
|
static float map_bubble_r(int count)
|
||||||
|
{
|
||||||
|
if (count <= 1)
|
||||||
|
return 7.0f;
|
||||||
|
float r = 14.0f + 9.0f * log10f((float)count);
|
||||||
|
return r > 40.0f ? 40.0f : r;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cluster nearest the screen centre (what X acts on), or -1 if none */
|
||||||
|
static int map_nearest_cluster(void)
|
||||||
|
{
|
||||||
|
int best = -1;
|
||||||
|
float bestd = 1e18f;
|
||||||
|
for (int j = 0; j < g_cl_count; j++) {
|
||||||
|
float dx = g_cl_x[j] - SCREEN_W / 2.0f;
|
||||||
|
float dy = g_cl_y[j] - SCREEN_H / 2.0f;
|
||||||
|
float d = dx * dx + dy * dy;
|
||||||
|
if (d < bestd) {
|
||||||
|
bestd = d;
|
||||||
|
best = j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cluster whose bubble contains screen point (x,y), or -1: touch hit-test */
|
||||||
|
static int map_cluster_at(float x, float y)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < g_cl_count; j++) {
|
||||||
|
float r = map_bubble_r(g_cl_n[j]) + 6.0f;
|
||||||
|
float dx = g_cl_x[j] - x, dy = g_cl_y[j] - y;
|
||||||
|
if (dx * dx + dy * dy <= r * r)
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
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
|
||||||
|
* library and rebuild the timeline so it has a slot. main-thread only (same
|
||||||
|
* pattern as fetch_page). returns 1 and fills *out_sel on success. */
|
||||||
|
static int map_open_marker(int marker_idx, int *out_sel)
|
||||||
|
{
|
||||||
|
if (marker_idx < 0 || marker_idx >= g_map_count)
|
||||||
|
return 0;
|
||||||
|
const char *id = g_map_id[marker_idx];
|
||||||
|
int idx = -1;
|
||||||
|
for (int i = 0; i < g_asset_count; i++)
|
||||||
|
if (!strcmp(g_asset_ids[i], id)) {
|
||||||
|
idx = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idx < 0) {
|
||||||
|
char url[600];
|
||||||
|
snprintf(url, sizeof(url), "%s/api/assets/%s", g_server, id);
|
||||||
|
membuf buf;
|
||||||
|
long code;
|
||||||
|
CURLcode r = http_request(url, NULL, &buf, &code, NULL, 0);
|
||||||
|
if (r != CURLE_OK || code < 200 || code >= 300) {
|
||||||
|
log_line("map open %s: curl %d http %ld", id, r, code);
|
||||||
|
free(buf.data);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
char dval[DATELEN] = "", tval[12] = "";
|
||||||
|
json_get(buf.data, buf.size, "fileCreatedAt", dval, sizeof(dval));
|
||||||
|
json_get(buf.data, buf.size, "type", tval, sizeof(tval));
|
||||||
|
free(buf.data);
|
||||||
|
if (!grow_assets(g_asset_count + 1))
|
||||||
|
return 0;
|
||||||
|
idx = g_asset_count++;
|
||||||
|
snprintf(g_asset_ids[idx], sizeof(g_asset_ids[0]), "%s", id);
|
||||||
|
snprintf(g_asset_dates[idx], sizeof(g_asset_dates[0]), "%s", dval);
|
||||||
|
g_asset_ratio[idx] = 0.0f; /* unknown: detail view fits anyway */
|
||||||
|
g_asset_rot[idx] = 0;
|
||||||
|
g_asset_is_video[idx] = !strncmp(tval, "VIDEO", 5);
|
||||||
|
g_asset_local_backed[idx] = 0;
|
||||||
|
g_thumb[idx] = NULL;
|
||||||
|
g_thumb_failed[idx] = 0;
|
||||||
|
g_tex_err[idx][0] = '\0';
|
||||||
|
/* opening from the map shouldn't be hijacked by an active search */
|
||||||
|
g_search_active = 0;
|
||||||
|
rebuild_display();
|
||||||
|
log_line("map open: fetched asset %s (%s)", id,
|
||||||
|
g_asset_is_video[idx] ? "video" : "photo");
|
||||||
|
}
|
||||||
|
int s = find_disp(SRC_SERVER, idx);
|
||||||
|
if (s >= 0) {
|
||||||
|
*out_sel = s;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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) */
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* raster tile pyramid (app0:maptiles.pak) — the LOD base map. */
|
||||||
|
/* Generated from the MapChart PNG by tools/png_to_tiles.py: several */
|
||||||
|
/* zoom levels, each cut into 256px JPEG tiles. We pick the level whose */
|
||||||
|
/* native width matches the on-screen map width (crisp), and draw only */
|
||||||
|
/* the visible tiles, decoding them into pooled textures (the safe path */
|
||||||
|
/* — freeing textures mid-run crashes Vita3K's texture cache). Tiles */
|
||||||
|
/* span mx[0,1] x my[MAP_IMG_MY0,MAP_IMG_MY1], same georeferencing as */
|
||||||
|
/* the markers, so everything lines up. */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
#define TILE_PX 256
|
||||||
|
#define MAP_MAXLEV 8
|
||||||
|
static void map_draw_grid(void); /* fallback, defined below */
|
||||||
|
static FILE *g_pak;
|
||||||
|
static long g_pak_blob; /* file offset where JPEG data begins */
|
||||||
|
static int g_pak_nlev;
|
||||||
|
static int g_lev_w[MAP_MAXLEV], g_lev_h[MAP_MAXLEV]; /* content px */
|
||||||
|
static int g_lev_cols[MAP_MAXLEV], g_lev_rows[MAP_MAXLEV];
|
||||||
|
static int g_lev_base[MAP_MAXLEV]; /* index of this level's first tile */
|
||||||
|
static unsigned int *g_ti_off, *g_ti_size;
|
||||||
|
static int g_ti_n;
|
||||||
|
|
||||||
|
#define TCACHE 80
|
||||||
|
static struct tile_slot {
|
||||||
|
int lev, col, row;
|
||||||
|
vita2d_texture *tex;
|
||||||
|
unsigned use;
|
||||||
|
} g_tc[TCACHE];
|
||||||
|
static unsigned g_tc_clock;
|
||||||
|
static int g_tile_budget; /* new tile decodes allowed this frame */
|
||||||
|
|
||||||
|
static void load_maptiles(void)
|
||||||
|
{
|
||||||
|
FILE *f = fopen("app0:maptiles.pak", "rb");
|
||||||
|
if (!f)
|
||||||
|
return;
|
||||||
|
char magic[4];
|
||||||
|
unsigned int ver, tile, nlev;
|
||||||
|
if (fread(magic, 1, 4, f) != 4 || memcmp(magic, "MTIL", 4) ||
|
||||||
|
fread(&ver, 4, 1, f) != 1 || fread(&tile, 4, 1, f) != 1 ||
|
||||||
|
fread(&nlev, 4, 1, f) != 1 ||
|
||||||
|
tile != TILE_PX || nlev == 0 || nlev > MAP_MAXLEV) {
|
||||||
|
fclose(f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_pak_nlev = (int)nlev;
|
||||||
|
for (int L = 0; L < g_pak_nlev; L++) {
|
||||||
|
unsigned int w, h, c, r;
|
||||||
|
if (fread(&w, 4, 1, f) != 1 || fread(&h, 4, 1, f) != 1 ||
|
||||||
|
fread(&c, 4, 1, f) != 1 || fread(&r, 4, 1, f) != 1) {
|
||||||
|
fclose(f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_lev_w[L] = w; g_lev_h[L] = h;
|
||||||
|
g_lev_cols[L] = c; g_lev_rows[L] = r;
|
||||||
|
}
|
||||||
|
unsigned int nt;
|
||||||
|
if (fread(&nt, 4, 1, f) != 1 || nt == 0 || nt > 200000) {
|
||||||
|
fclose(f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_ti_off = malloc(nt * sizeof(*g_ti_off));
|
||||||
|
g_ti_size = malloc(nt * sizeof(*g_ti_size));
|
||||||
|
if (!g_ti_off || !g_ti_size) {
|
||||||
|
free(g_ti_off); free(g_ti_size);
|
||||||
|
g_ti_off = g_ti_size = NULL;
|
||||||
|
fclose(f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int curlev = -1;
|
||||||
|
unsigned int i;
|
||||||
|
for (i = 0; i < nt; i++) {
|
||||||
|
unsigned short lev, col, row;
|
||||||
|
unsigned int off, sz;
|
||||||
|
if (fread(&lev, 2, 1, f) != 1 || fread(&col, 2, 1, f) != 1 ||
|
||||||
|
fread(&row, 2, 1, f) != 1 || fread(&off, 4, 1, f) != 1 ||
|
||||||
|
fread(&sz, 4, 1, f) != 1)
|
||||||
|
break;
|
||||||
|
g_ti_off[i] = off;
|
||||||
|
g_ti_size[i] = sz;
|
||||||
|
if ((int)lev != curlev && lev < g_pak_nlev) {
|
||||||
|
curlev = lev;
|
||||||
|
g_lev_base[curlev] = (int)i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_pak_blob = ftell(f); /* index fully read: blob starts here */
|
||||||
|
g_ti_n = (int)i;
|
||||||
|
g_pak = f; /* kept open for on-demand tile reads */
|
||||||
|
log_line("maptiles: %d levels, %d tiles", g_pak_nlev, g_ti_n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* read + decode one tile JPEG into a pooled texture (NULL on any failure) */
|
||||||
|
static vita2d_texture *tile_decode(int L, int col, int row)
|
||||||
|
{
|
||||||
|
if (!g_pak || L < 0 || L >= g_pak_nlev ||
|
||||||
|
col < 0 || col >= g_lev_cols[L] || row < 0 || row >= g_lev_rows[L])
|
||||||
|
return NULL;
|
||||||
|
int ti = g_lev_base[L] + row * g_lev_cols[L] + col;
|
||||||
|
if (ti < 0 || ti >= g_ti_n)
|
||||||
|
return NULL;
|
||||||
|
unsigned int off = g_ti_off[ti], sz = g_ti_size[ti];
|
||||||
|
if (sz == 0 || sz > 4u * 1024 * 1024)
|
||||||
|
return NULL;
|
||||||
|
char *buf = malloc(sz);
|
||||||
|
if (!buf)
|
||||||
|
return NULL;
|
||||||
|
if (fseek(g_pak, g_pak_blob + off, SEEK_SET) != 0 ||
|
||||||
|
fread(buf, 1, sz, g_pak) != sz) {
|
||||||
|
free(buf);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
int w, h, comps;
|
||||||
|
char err[64];
|
||||||
|
unsigned char *pix = decode_jpeg_buf(buf, sz, TILE_PX, &w, &h, &comps,
|
||||||
|
err, sizeof(err));
|
||||||
|
free(buf);
|
||||||
|
if (!pix) {
|
||||||
|
log_line("tile %d/%d/%d: %s", L, col, row, err);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
SceGxmTextureFormat fmt = (comps == 1) ?
|
||||||
|
SCE_GXM_TEXTURE_FORMAT_U8_R111 :
|
||||||
|
SCE_GXM_TEXTURE_FORMAT_U8U8U8_BGR;
|
||||||
|
vita2d_texture *t = tex_acquire(w, h, fmt);
|
||||||
|
if (t) {
|
||||||
|
unsigned char *dst = vita2d_texture_get_datap(t);
|
||||||
|
unsigned int stride = vita2d_texture_get_stride(t);
|
||||||
|
unsigned int rb = w * comps;
|
||||||
|
for (int y = 0; y < h; y++)
|
||||||
|
memcpy(dst + y * stride, pix + y * rb, rb);
|
||||||
|
vita2d_texture_set_filters(t, SCE_GXM_TEXTURE_FILTER_LINEAR,
|
||||||
|
SCE_GXM_TEXTURE_FILTER_LINEAR);
|
||||||
|
}
|
||||||
|
free(pix);
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* fetch a tile from the cache, optionally loading (budget-limited) on a miss */
|
||||||
|
static vita2d_texture *tile_get(int L, int col, int row, int may_load)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < TCACHE; i++)
|
||||||
|
if (g_tc[i].tex && g_tc[i].lev == L &&
|
||||||
|
g_tc[i].col == col && g_tc[i].row == row) {
|
||||||
|
g_tc[i].use = ++g_tc_clock;
|
||||||
|
return g_tc[i].tex;
|
||||||
|
}
|
||||||
|
if (!may_load || g_tile_budget <= 0)
|
||||||
|
return NULL;
|
||||||
|
vita2d_texture *t = tile_decode(L, col, row);
|
||||||
|
if (!t)
|
||||||
|
return NULL;
|
||||||
|
g_tile_budget--;
|
||||||
|
int slot = -1;
|
||||||
|
unsigned oldest = 0xffffffffu;
|
||||||
|
for (int i = 0; i < TCACHE; i++) {
|
||||||
|
if (!g_tc[i].tex) { slot = i; break; }
|
||||||
|
if (g_tc[i].use < oldest) { oldest = g_tc[i].use; slot = i; }
|
||||||
|
}
|
||||||
|
if (g_tc[slot].tex)
|
||||||
|
tex_release(g_tc[slot].tex); /* recycle, never free */
|
||||||
|
g_tc[slot].tex = t;
|
||||||
|
g_tc[slot].lev = L;
|
||||||
|
g_tc[slot].col = col;
|
||||||
|
g_tc[slot].row = row;
|
||||||
|
g_tc[slot].use = ++g_tc_clock;
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* the LOD whose native width best matches the on-screen map width */
|
||||||
|
static int tile_pick_level(void)
|
||||||
|
{
|
||||||
|
int L = 0;
|
||||||
|
while (L < g_pak_nlev - 1 && g_lev_w[L] < (int)g_map_scale)
|
||||||
|
L++;
|
||||||
|
return L;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* draw every visible tile of level L (missing ones leave the coarser level
|
||||||
|
* below showing through, so there are no blank gaps while tiles stream in) */
|
||||||
|
static void map_draw_level(int L)
|
||||||
|
{
|
||||||
|
if (L < 0 || L >= g_pak_nlev)
|
||||||
|
return;
|
||||||
|
double span = MAP_IMG_MY1 - MAP_IMG_MY0;
|
||||||
|
double lw = g_lev_w[L], lh = g_lev_h[L];
|
||||||
|
for (int r = 0; r < g_lev_rows[L]; r++) {
|
||||||
|
for (int c = 0; c < g_lev_cols[L]; c++) {
|
||||||
|
double mx0 = (c * TILE_PX) / lw;
|
||||||
|
double mx1 = ((c + 1) * TILE_PX) / lw;
|
||||||
|
double my0 = MAP_IMG_MY0 + ((r * TILE_PX) / lh) * span;
|
||||||
|
double my1 = MAP_IMG_MY0 + (((r + 1) * TILE_PX) / lh) * span;
|
||||||
|
float sx0, sy0, sx1, sy1;
|
||||||
|
map_project(mx0, my0, &sx0, &sy0);
|
||||||
|
map_project(mx1, my1, &sx1, &sy1);
|
||||||
|
if (sx1 < -1 || sx0 > SCREEN_W + 1 ||
|
||||||
|
sy1 < -1 || sy0 > SCREEN_H + 1)
|
||||||
|
continue;
|
||||||
|
vita2d_texture *t = tile_get(L, c, r, 1);
|
||||||
|
if (!t)
|
||||||
|
continue;
|
||||||
|
vita2d_draw_texture_scale(t, sx0, sy0,
|
||||||
|
(sx1 - sx0) / TILE_PX, (sy1 - sy0) / TILE_PX);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* the tiled base map: a coarse overview first (no gaps), then the chosen LOD */
|
||||||
|
static void map_draw_tiles(void)
|
||||||
|
{
|
||||||
|
g_tile_budget = 6; /* cap new decodes per frame (avoids hitching) */
|
||||||
|
if (g_pak_nlev <= 0) {
|
||||||
|
map_draw_grid(); /* fallback if the pak is missing */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
map_draw_level(0);
|
||||||
|
int L = tile_pick_level();
|
||||||
|
if (L > 0)
|
||||||
|
map_draw_level(L);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* faint lat/lon graticule for orientation (the only "base map" there is) */
|
||||||
|
static void map_draw_grid(void)
|
||||||
|
{
|
||||||
|
uint32_t c = RGBA8(40, 44, 54, 255);
|
||||||
|
for (int lon = -180; lon <= 180; lon += 30) {
|
||||||
|
float sx, sy;
|
||||||
|
map_project(merc_x(lon), 0.0, &sx, &sy);
|
||||||
|
if (sx >= 0 && sx <= SCREEN_W)
|
||||||
|
vita2d_draw_rectangle(sx, 0, 1, SCREEN_H, c);
|
||||||
|
}
|
||||||
|
for (int lat = -75; lat <= 75; lat += 15) {
|
||||||
|
float sx, sy;
|
||||||
|
map_project(0.0, merc_y(lat), &sx, &sy);
|
||||||
|
if (sy >= 0 && sy <= SCREEN_H)
|
||||||
|
vita2d_draw_rectangle(0, sy, SCREEN_W, 1, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* draw every cluster bubble; `sel` gets a white selection ring */
|
||||||
|
static void map_draw_clusters(int sel)
|
||||||
|
{
|
||||||
|
uint32_t fill = RGBA8(94, 110, 215, 235);
|
||||||
|
uint32_t dot = RGBA8(120, 140, 235, 235);
|
||||||
|
uint32_t white = RGBA8(245, 245, 250, 255);
|
||||||
|
for (int j = 0; j < g_cl_count; j++) {
|
||||||
|
float x = g_cl_x[j], y = g_cl_y[j];
|
||||||
|
int cnt = g_cl_n[j];
|
||||||
|
float r = map_bubble_r(cnt);
|
||||||
|
if (j == sel)
|
||||||
|
draw_disc(x, y, (r + 3.0f) * 2.0f, white);
|
||||||
|
if (cnt <= 1) {
|
||||||
|
draw_disc(x, y, r * 2.0f, dot);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
draw_disc(x, y, r * 2.0f, fill);
|
||||||
|
char b[12];
|
||||||
|
snprintf(b, sizeof(b), "%d", cnt);
|
||||||
|
int w = text_width(0.8f, b);
|
||||||
|
draw_text(x - w / 2.0f, y + 6.0f, white, 0.8f, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate a Web-Mercator tile pyramid (LODs) from a MapChart world PNG 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].
|
||||||
|
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:
|
||||||
|
|
||||||
|
magic 'MTIL' (4) u32 version u32 tile_size u32 nlevels
|
||||||
|
per level: u32 imgW imgH cols rows
|
||||||
|
u32 ntiles
|
||||||
|
per tile: u16 level u16 col u16 row u32 offset u32 size
|
||||||
|
<concatenated JPEG bytes>
|
||||||
|
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'
|
||||||
|
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]
|
||||||
|
|
||||||
|
src = Image.open(SRC).convert('RGB')
|
||||||
|
SW, SH = src.size
|
||||||
|
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
|
||||||
|
h = round(w * SH / SW)
|
||||||
|
img = src.resize((w, h), Image.LANCZOS)
|
||||||
|
cols = (w + TILE - 1) // TILE
|
||||||
|
rows = (h + TILE - 1) // TILE
|
||||||
|
# pad to whole tiles with the ocean colour so edge tiles aren't black
|
||||||
|
bg = src.getpixel((4, SH // 2)) # a sea pixel (top-left corner area is sea)
|
||||||
|
padded = Image.new('RGB', (cols * TILE, rows * TILE), bg)
|
||||||
|
padded.paste(img, (0, 0))
|
||||||
|
levels.append((w, h, cols, rows, padded))
|
||||||
|
print('level', len(levels)-1, 'img', w, h, 'tiles', cols, 'x', rows, '=', cols*rows)
|
||||||
|
|
||||||
|
# de-dup identical level widths (when native < a planned width)
|
||||||
|
seen = set(); uniq = []
|
||||||
|
for L in levels:
|
||||||
|
if L[0] in seen:
|
||||||
|
continue
|
||||||
|
seen.add(L[0]); uniq.append(L)
|
||||||
|
levels = uniq
|
||||||
|
|
||||||
|
blob = io.BytesIO()
|
||||||
|
index = [] # (level,col,row,offset,size)
|
||||||
|
for li, (w, h, cols, rows, padded) in enumerate(levels):
|
||||||
|
for r in range(rows):
|
||||||
|
for c in range(cols):
|
||||||
|
tile = padded.crop((c*TILE, r*TILE, c*TILE+TILE, r*TILE+TILE))
|
||||||
|
b = io.BytesIO(); tile.save(b, 'JPEG', quality=QUALITY)
|
||||||
|
data = b.getvalue()
|
||||||
|
off = blob.tell(); blob.write(data)
|
||||||
|
index.append((li, c, r, off, len(data)))
|
||||||
|
|
||||||
|
with open(OUT, 'wb') as f:
|
||||||
|
f.write(b'MTIL')
|
||||||
|
f.write(struct.pack('<III', 1, TILE, len(levels)))
|
||||||
|
for (w, h, cols, rows, _) in levels:
|
||||||
|
f.write(struct.pack('<IIII', w, h, cols, rows))
|
||||||
|
f.write(struct.pack('<I', len(index)))
|
||||||
|
for (li, c, r, off, sz) in index:
|
||||||
|
f.write(struct.pack('<HHHII', li, c, r, off, sz))
|
||||||
|
f.write(blob.getvalue())
|
||||||
|
|
||||||
|
import os
|
||||||
|
print('levels', len(levels), 'tiles', len(index),
|
||||||
|
'pak', round(os.path.getsize(OUT)/1e6, 2), 'MB')
|
||||||
Reference in New Issue
Block a user