Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c9f6a5f
testing opus dataset update
cornhundred Dec 3, 2025
e52dbd0
Enhance image layer management by disposing of old layers and forcing…
cornhundred Dec 3, 2025
230743d
fixing statefulness
cornhundred Dec 3, 2025
6a437f7
camel_case
cornhundred Dec 4, 2025
0c94766
fixed first gene click obs_store state
cornhundred Dec 4, 2025
63c2141
working on opus yearbook
cornhundred Dec 4, 2025
66c8400
fixed initialization error
cornhundred Dec 4, 2025
cb8d3b9
added pagination
cornhundred Dec 4, 2025
114bc20
fixed pagination update
cornhundred Dec 4, 2025
cac145a
fixing portrait image layers
cornhundred Dec 4, 2025
842e612
improved pagination
cornhundred Dec 4, 2025
963f170
test yearbook and dataset_dropdown
cornhundred Dec 6, 2025
3a1c9c6
aws creds for yearbook
cornhundred Dec 6, 2025
3492f3c
aws creds for yearbook
cornhundred Dec 6, 2025
e9a64cd
ruff format
cornhundred Dec 10, 2025
0f9aedf
js lint
cornhundred Dec 11, 2025
c69f6ea
js format
cornhundred Dec 11, 2025
ce16781
linting and format js
cornhundred Dec 11, 2025
d511fc3
merged dataset_dropdown_opus_2
cornhundred Dec 11, 2025
cf07bb0
updating file
cornhundred Dec 11, 2025
77294e3
remove console logs
cornhundred Dec 11, 2025
23bd63b
merging in main
cornhundred Dec 11, 2025
c0f4212
fixing linting errors
cornhundred Dec 11, 2025
e536f42
fixing linting errors
cornhundred Dec 11, 2025
666f3a3
add example notebook
cornhundred Dec 12, 2025
e3c799c
reusing scale bar code
cornhundred Dec 12, 2025
abc5892
cleared state
cornhundred Dec 12, 2025
c09d361
Merge branch 'main' into yearbook_opus
cornhundred Dec 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 95 additions & 95 deletions docs/assets/js/widget.js

Large diffs are not rendered by default.

228 changes: 228 additions & 0 deletions js/deck-gl/core/yearbook_viewports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import { OrthographicView } from 'deck.gl';

import { visibleTiles } from '../../vector_tile/visibleTiles';

/**
* Create multiple OrthographicViews for the yearbook grid layout.
* Each portrait gets its own view with specific x, y, width, height.
*
* @param {number} num_rows - Number of rows in the grid
* @param {number} num_cols - Number of columns in the grid
* @param {number} portrait_size - Size of each portrait in pixels
* @param {number} gap - Gap between portraits in pixels
* @returns {Array<OrthographicView>} Array of deck.gl views
*/
export const create_yearbook_views = (
num_rows,
num_cols,
portrait_size,
gap
) => {
const views = [];

for (let row = 0; row < num_rows; row++) {
for (let col = 0; col < num_cols; col++) {
const index = row * num_cols + col;

// Calculate the position of this portrait
const x = col * (portrait_size + gap);
const y = row * (portrait_size + gap);

views.push(
new OrthographicView({
id: `portrait-${index}`,
x,
y,
width: portrait_size,
height: portrait_size,
controller: {
doubleClickZoom: false,
dragPan: false, // Disable panning in yearbook
scrollZoom: true, // Enable zoom
touchZoom: true,
},
})
);
}
}

return views;
};

/**
* Calculate the viewport bounds for each portrait based on its center.
* When zoom=0, view_width and view_height should be in data/image coordinates.
*
* @param {Array<{cell_id: string, x: number, y: number}>} centers - Center coordinates for each portrait
* @param {number} zoom - Current zoom level (use 0 if view dimensions are already in data coords)
* @param {number} view_width - Width of each portrait view (in data coordinates when zoom=0)
* @param {number} view_height - Height of each portrait view (in data coordinates when zoom=0)
* @returns {Array<{min_x, max_x, min_y, max_y}>} Viewport bounds for each portrait
*/
export const calc_portrait_viewports = (
centers,
zoom,
view_width,
view_height
) => {
const zoomFactor = Math.pow(2, zoom);
const halfWidthZoomed = view_width / (2 * zoomFactor);
const halfHeightZoomed = view_height / (2 * zoomFactor);

return centers.map((center) => ({
cell_id: center.cell_id,
min_x: center.x - halfWidthZoomed,
max_x: center.x + halfWidthZoomed,
min_y: center.y - halfHeightZoomed,
max_y: center.y + halfHeightZoomed,
center_x: center.x,
center_y: center.y,
}));
};

/**
* Get tiles visible across all portraits (discontiguous tile loading).
* This returns a unique set of tiles that cover all portrait viewports.
*
* @param {Array<{cell_id: string, x: number, y: number}>} centers - Center coordinates for each portrait
* @param {number} zoom - Current zoom level
* @param {number} view_width - Width of each portrait view in pixels
* @param {number} view_height - Height of each portrait view in pixels
* @param {number} tile_size - Size of each tile
* @returns {Array<{tileX: number, tileY: number, name: string}>} Unique tiles across all viewports
*/
export const get_discontiguous_tiles = (
centers,
zoom,
view_width,
view_height,
tile_size
) => {
const viewports = calc_portrait_viewports(
centers,
zoom,
view_width,
view_height
);

// Collect all tiles from all viewports
const tile_map = new Map();

viewports.forEach((viewport) => {
const tiles = visibleTiles(
viewport.min_x,
viewport.max_x,
viewport.min_y,
viewport.max_y,
tile_size
);

tiles.forEach((tile) => {
// Use tile name as key to deduplicate
if (!tile_map.has(tile.name)) {
tile_map.set(tile.name, tile);
}
});
});

return Array.from(tile_map.values());
};

/**
* Create initial view states for all portraits.
*
* @param {Array<{cell_id: string, x: number, y: number}>} centers - Center coordinates for each portrait
* @param {number} zoom - Initial zoom level
* @returns {Object} View states keyed by view id
*/
export const create_initial_view_states = (centers, zoom) => {
const view_states = {};

centers.forEach((center, index) => {
const view_id = `portrait-${index}`;
view_states[view_id] = {
target: [center.x, center.y, 0],
zoom,
};
});

return view_states;
};

/**
* Update all view states with a new zoom level (keeping centers the same).
*
* @param {Object} current_view_states - Current view states
* @param {number} new_zoom - New zoom level
* @returns {Object} Updated view states
*/
export const update_view_states_zoom = (current_view_states, new_zoom) => {
const updated_states = {};

Object.entries(current_view_states).forEach(([view_id, state]) => {
updated_states[view_id] = {
...state,
zoom: new_zoom,
};
});

return updated_states;
};

/**
* Calculate the total portraits per page.
*
* @param {number} num_rows - Number of rows
* @param {number} num_cols - Number of columns
* @returns {number} Total portraits per page
*/
export const get_portraits_per_page = (num_rows, num_cols) => {
return num_rows * num_cols;
};

/**
* Calculate total pages needed.
*
* @param {number} total_cells - Total number of cells
* @param {number} num_rows - Number of rows
* @param {number} num_cols - Number of columns
* @returns {number} Total pages
*/
export const get_total_pages = (total_cells, num_rows, num_cols) => {
const portraits_per_page = get_portraits_per_page(num_rows, num_cols);
return Math.max(1, Math.ceil(total_cells / portraits_per_page));
};

/**
* Get cells for a specific page.
*
* @param {Array<string>} cells - All cell ids
* @param {number} page - Page number (0-indexed)
* @param {number} num_rows - Number of rows
* @param {number} num_cols - Number of columns
* @returns {Array<string>} Cell ids for the page
*/
export const get_cells_for_page = (cells, page, num_rows, num_cols) => {
const portraits_per_page = get_portraits_per_page(num_rows, num_cols);
const start_index = page * portraits_per_page;
return cells.slice(start_index, start_index + portraits_per_page);
};

/**
* Filter data points to those visible in any portrait.
*
* @param {Array<{x: number, y: number}>} data - Data points with x, y coordinates
* @param {Array<{min_x, max_x, min_y, max_y}>} viewports - Viewport bounds
* @returns {Array} Filtered data points
*/
export const filter_data_in_viewports = (data, viewports) => {
return data.filter((point) => {
return viewports.some(
(viewport) =>
point.x >= viewport.min_x &&
point.x <= viewport.max_x &&
point.y >= viewport.min_y &&
point.y <= viewport.max_y
);
});
};
73 changes: 73 additions & 0 deletions js/deck-gl/layers/image_layers.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const make_image_layer = (viz_state, info, datasetIndex = 0, cacheKey = '') => {
minZoom: -7,
maxZoom: 0,
maxCacheSize: 0, // Disable internal tile caching
maxRequests: 6, // Limit concurrent tile requests
extent: [0, 0, viz_state.dimensions.width, viz_state.dimensions.height],
getTileData: create_get_tile_data(
viz_state.global_base_url,
Expand Down Expand Up @@ -108,3 +109,75 @@ export const update_opacity_single_image_layer = (
: layer
);
};

/**
* Create image layers for yearbook.
* Creates one set of image layers per portrait, each with its own extent.
* This ensures tiles are loaded correctly for each discontiguous region.
*
* @param {Object} viz_state - Visualization state
* @param {Array<{cell_id: string, x: number, y: number}>} portrait_centers - Portrait centers
* @param {number} portrait_data_size - Portrait size in data coordinates
* @param {string} cacheKey - Cache key for layer IDs (page-based for reuse)
* @returns {Array} Image layers for all portraits
*/
export const make_yearbook_image_layers = async (
viz_state,
portrait_centers,
portrait_data_size,
cacheKey = null
) => {
const { image_info } = viz_state.img;
const { max_pyramid_zoom, tile_size } = viz_state.img.landscape_parameters;
const layerCacheKey = cacheKey || Date.now().toString(36);

const all_layers = [];
const half_size = portrait_data_size / 2;
// Padding should be generous to cover zoomed-in views and tile boundaries
const padding = Math.max(tile_size * 3, portrait_data_size * 0.5);

portrait_centers.forEach((center, portrait_index) => {
// Each portrait gets its own extent covering its visible area plus padding
const extent = [
Math.max(0, center.x - half_size - padding),
Math.max(0, center.y - half_size - padding),
Math.min(viz_state.dimensions.width, center.x + half_size + padding),
Math.min(viz_state.dimensions.height, center.y + half_size + padding),
];

image_info.forEach((info) => {
const opacity = 5;
// Include portrait index in layer ID for proper updates
const layerId = `yb-${info.button_name}-p${portrait_index}-${layerCacheKey}`;

const image_layer = new TileLayer({
id: layerId,
tileSize: viz_state.dimensions.tileSize,
refinementStrategy: 'no-overlap',
minZoom: -7,
maxZoom: 0,
maxCacheSize: 50,
maxRequests: 6,
extent,
getTileData: create_get_tile_data(
viz_state.global_base_url,
info.name,
viz_state.img.image_format,
max_pyramid_zoom,
options,
viz_state.aws
),
renderSubLayers: create_render_tile_sublayers(
viz_state.dimensions,
info.color,
opacity
),
...getModelMatrixProps(viz_state.rotation),
});

all_layers.push(image_layer);
});
});

return all_layers;
};
Loading
Loading