switch to pinia for state mangement

This commit is contained in:
2026-01-25 11:47:18 +01:00
parent 4cec4b1bb4
commit bd5a59d827
7 changed files with 383 additions and 361 deletions

View File

@@ -122,15 +122,7 @@ import { KNOWN_SITES } from './data/historicSites.js';
// UTILITIES
// ============================================================================
import { calculateDistance, calculateBearing, extendRay } from './utils/geometry.js';
import { getTileByCoordinates, getTileById, getTileMoundData, requestTileProcessing } from './utils/api.js';
import {
getMetadata, setMetadata, findTileByCoords, hasMetadata,
getMoundData, setMoundData, hasMoundData,
getImageUrl, setImagesOnMap, areImagesOnMap, removeImagesFromMap,
setLoading, isMetadataLoading, isMoundLoading, isImageLoading,
isMetadataLoaded, isMoundLoaded, isReadyToRender, getImageAvailability,
getAllTileIdsWithImages, getAllTileIdsWithMounds
} from './utils/tileCache.js';
import { useTilesStore } from './stores/tiles.js';
// For generating the pre-baked tiles:
// import { batchRenderTiles } from './utils/batch-renderer.js';
@@ -140,6 +132,8 @@ import {
// CONSTANTS
// ============================================================================
const tilesStore = useTilesStore();
const DEFAULT_RENDER_SETTINGS = {
azimuth: 90,
altitude: 60,
@@ -243,12 +237,12 @@ function webMercatorToLonLat(x, y) {
// Check if tile images are loaded on map
function areTileImagesLoaded(tileId) {
return areImagesOnMap(tileId);
return tilesStore.areImagesOnMap(tileId);
}
// Check if mound data is cached
function isMoundDataCached(tileId) {
return hasMoundData(tileId);
return tilesStore.hasMoundData(tileId);
}
// ============================================================================
@@ -314,8 +308,8 @@ async function loadTileImages(tileId, tileMetadata, imageUrl = null) {
// Determine which format to load (prefer PNG over JPG)
if (!imageUrl) {
imageUrl = tileMetadata.png_available
? getImageUrl(tileId, 'png')
: getImageUrl(tileId, 'jpg');
? tilesStore.getImageUrl(tileId, 'png')
: tilesStore.getImageUrl(tileId, 'jpg');
}
// Remove old source/layer if overwriting
@@ -356,12 +350,7 @@ async function loadTileImages(tileId, tileMetadata, imageUrl = null) {
}
}, 'lidar-datum');
setImagesOnMap(tileId);
// Also cache the metadata if we received it
if (tileMetadata) {
setMetadata(tileId, tileMetadata);
}
tilesStore.markImagesOnMap(tileId);
} catch (err) {
console.error(`Failed to load images for ${tileId}:`, err);
throw err;
@@ -374,12 +363,8 @@ async function loadInitialTiles() {
for (const tileName of TILE_NAMES) {
try {
// Fetch tile metadata by ID
const tileMetadata = await getTileById(tileName);
// Cache the metadata
setMetadata(tileName, tileMetadata);
// Fetch tile metadata by ID (this also caches it)
const tileMetadata = await tilesStore.fetchMetadataById(tileName);
// Only load if tile is ready and has images available
if (tileMetadata.status === 'ready' && (tileMetadata.png_available || tileMetadata.jpg_available)) {
@@ -393,7 +378,7 @@ async function loadInitialTiles() {
}
}
console.log(`Loaded ${getAllTileIdsWithImages().length} initial tiles`);
console.log(`Loaded ${tilesStore.getAllTileIdsWithImages.length} initial tiles`);
}
async function loadMoundData(tileId) {
@@ -402,18 +387,12 @@ async function loadMoundData(tileId) {
return;
}
setLoading(tileId, 'mound', true);
try {
const buffer = await getTileMoundData(tileId);
const data = parseMoundBuffer(buffer);
setMoundData(tileId, data);
await tilesStore.fetchMoundData(tileId, parseMoundBuffer);
console.log(`Loaded mound data for ${tileId}`);
} catch (err) {
console.error(`Failed to load mound data for ${tileId}:`, err);
throw err;
} finally {
setLoading(tileId, 'mound', false);
}
}
@@ -441,12 +420,9 @@ async function handleContextMenu(e) {
// Try to fetch tile metadata from API
try {
const tileData = await getTileByCoordinates(lat, lng);
const tileData = await tilesStore.fetchMetadataByCoords(lat, lng);
if (tileData) {
// Cache the metadata
setMetadata(tileData.id, tileData);
contextMenu.value.tileData = tileData;
contextMenu.value.imagesLoaded = areTileImagesLoaded(tileData.id);
contextMenu.value.moundLoaded = isMoundDataCached(tileData.id);
@@ -476,7 +452,7 @@ async function requestTileFromContextMenu() {
};
// Start SSE request
requestTileProcessing(
tilesStore.requestTileProcessing(
lat,
lng,
async (data) => {
@@ -641,7 +617,7 @@ function toggleLidar() {
if (!map) return;
const visibility = showLidar.value ? 'visible' : 'none';
for (const tileId of getAllTileIdsWithImages()) {
for (const tileId of tilesStore.getAllTileIdsWithImages) {
const layerId = `tile-image-layer-${tileId}`;
if (map.getLayer(layerId)) {
map.setLayoutProperty(layerId, 'visibility', visibility);
@@ -653,7 +629,7 @@ function updateLidarOpacity() {
if (!map) return;
const opacity = lidarOpacity.value / 100;
for (const tileId of getAllTileIdsWithImages()) {
for (const tileId of tilesStore.getAllTileIdsWithImages) {
const layerId = `tile-image-layer-${tileId}`;
if (map.getLayer(layerId)) {
map.setPaintProperty(layerId, 'raster-opacity', opacity);
@@ -669,10 +645,10 @@ function openSandbox() {
sandboxVisible.value = true;
setTimeout(() => {
const tilesWithMounds = getAllTileIdsWithMounds();
const tilesWithMounds = tilesStore.getAllTileIdsWithMounds;
if (sandboxRef.value && tilesWithMounds.length > 0) {
const firstTileId = tilesWithMounds[0];
const firstTile = getMoundData(firstTileId);
const firstTile = tilesStore.getMoundData(firstTileId);
if (firstTile) {
sandboxRef.value.loadTileData(firstTile);
}
@@ -688,7 +664,7 @@ async function openSandboxWithTile(tileId) {
sandboxVisible.value = true;
setTimeout(() => {
const moundData = getMoundData(tileId);
const moundData = tilesStore.getMoundData(tileId);
if (sandboxRef.value && moundData) {
sandboxRef.value.loadTileData(moundData, tileId);
}
@@ -701,7 +677,7 @@ function onRenderComplete(result) {
// Result should contain: { dataURL, tileId, ... }
if (result.tileId && result.dataURL) {
const metadata = getMetadata(result.tileId);
const metadata = tilesStore.getMetadata(result.tileId);
if (metadata) {
// Load the sandbox-rendered image onto the map
loadTileImages(result.tileId, metadata, result.dataURL);

View File

@@ -1,6 +1,9 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const pinia = createPinia();
const app = createApp(App)
app.mount('#app')
app.use(pinia)
app.mount('#app')

347
ui/src/stores/tiles.js Normal file
View File

@@ -0,0 +1,347 @@
// ============================================================================
// TILES STORE (Pinia)
// Centralized tile state management with integrated API calls
// ============================================================================
import { defineStore } from 'pinia';
import { getTileByCoordinates, getTileById, getTileMoundData, requestTileProcessing } from '../utils/api.js';
export const useTilesStore = defineStore('tiles', {
// ==========================================================================
// STATE
// ==========================================================================
state: () => ({
// Metadata cache - Map<tileId, metadata>
metadata: new Map(),
// Mound data cache - Map<tileId, { positions, indices, bounds }>
mounds: new Map(),
// Images on map tracker - Set<tileId>
imagesOnMap: new Set(),
// Loading state - Map<tileId, { metadata, mound, jpg, png }>
loading: new Map(),
}),
// ==========================================================================
// GETTERS
// ==========================================================================
getters: {
// ------------------------------------------------------------------------
// Metadata getters
// ------------------------------------------------------------------------
getMetadata: (state) => (tileId) => {
return state.metadata.get(tileId) || null;
},
hasMetadata: (state) => (tileId) => {
return state.metadata.has(tileId);
},
findTileByCoords: (state) => (lat, lng) => {
for (const [tileId, meta] of state.metadata) {
if (lat >= meta.min_lat && lat <= meta.max_lat &&
lng >= meta.min_lng && lng <= meta.max_lng) {
return tileId;
}
}
return null;
},
getAllTileIds: (state) => {
return Array.from(state.metadata.keys());
},
// ------------------------------------------------------------------------
// Mound data getters
// ------------------------------------------------------------------------
getMoundData: (state) => (tileId) => {
return state.mounds.get(tileId) || null;
},
hasMoundData: (state) => (tileId) => {
return state.mounds.has(tileId);
},
getAllTileIdsWithMounds: (state) => {
return Array.from(state.mounds.keys());
},
// ------------------------------------------------------------------------
// Image getters
// ------------------------------------------------------------------------
getImageUrl: () => (tileId, type) => {
const API_BASE = ''; // Same origin
return `${API_BASE}/tiles/${type}/${tileId}.${type}`;
},
areImagesOnMap: (state) => (tileId) => {
return state.imagesOnMap.has(tileId);
},
getAllTileIdsWithImages: (state) => {
return Array.from(state.imagesOnMap);
},
getImageAvailability: (state) => (tileId) => {
const meta = state.metadata.get(tileId);
if (!meta) {
return { jpg: false, png: false };
}
return {
jpg: meta.jpg_available || false,
png: meta.png_available || false
};
},
// ------------------------------------------------------------------------
// Loading state getters
// ------------------------------------------------------------------------
isMetadataLoading: (state) => (tileId) => {
return state.loading.get(tileId)?.metadata || false;
},
isMoundLoading: (state) => (tileId) => {
return state.loading.get(tileId)?.mound || false;
},
isImageLoading: (state) => (tileId, type) => {
return state.loading.get(tileId)?.[type] || false;
},
// ------------------------------------------------------------------------
// Composite state getters
// ------------------------------------------------------------------------
isMetadataLoaded: (state) => (tileId) => {
return state.metadata.has(tileId);
},
isMoundLoaded: (state) => (tileId) => {
return state.mounds.has(tileId);
},
isReadyToRender: (state) => (tileId) => {
return state.metadata.has(tileId) && state.mounds.has(tileId);
},
// ------------------------------------------------------------------------
// Statistics
// ------------------------------------------------------------------------
stats() {
return {
metadataCount: this.metadata.size,
moundCount: this.mounds.size,
imagesOnMapCount: this.imagesOnMap.size,
loadingCount: this.loading.size
};
}
},
// ==========================================================================
// ACTIONS
// ==========================================================================
actions: {
// ------------------------------------------------------------------------
// Metadata actions
// ------------------------------------------------------------------------
/**
* Fetch and cache tile metadata by coordinates
* @param {number} lat
* @param {number} lng
* @returns {Promise<Object|null>} Metadata or null if not found
*/
async fetchMetadataByCoords(lat, lng) {
// Check if we already have it cached
const cachedTileId = this.findTileByCoords(lat, lng);
if (cachedTileId) {
return this.metadata.get(cachedTileId);
}
// Set loading state
const tempId = `temp-${lat}-${lng}`;
this._setLoading(tempId, 'metadata', true);
try {
const metadata = await getTileByCoordinates(lat, lng);
if (metadata) {
this.metadata.set(metadata.id, metadata);
return metadata;
}
return null;
} catch (err) {
console.error('Failed to fetch metadata:', err);
throw err;
} finally {
this._setLoading(tempId, 'metadata', false);
}
},
/**
* Fetch and cache tile metadata by ID
* @param {string} tileId
* @returns {Promise<Object>}
*/
async fetchMetadataById(tileId) {
// Return from cache if available
if (this.metadata.has(tileId)) {
return this.metadata.get(tileId);
}
this._setLoading(tileId, 'metadata', true);
try {
const metadata = await getTileById(tileId);
this.metadata.set(tileId, metadata);
return metadata;
} catch (err) {
console.error('Failed to fetch metadata by ID:', err);
throw err;
} finally {
this._setLoading(tileId, 'metadata', false);
}
},
/**
* Manually set metadata (for cases where you already have it)
* @param {string} tileId
* @param {Object} metadata
*/
setMetadata(tileId, metadata) {
this.metadata.set(tileId, metadata);
},
// ------------------------------------------------------------------------
// Mound data actions
// ------------------------------------------------------------------------
/**
* Fetch and cache mound data
* @param {string} tileId
* @param {Function} parseFunction - Function to parse the mound buffer
* @returns {Promise<Object>} Parsed mound data
*/
async fetchMoundData(tileId, parseFunction) {
// Return from cache if available
if (this.mounds.has(tileId)) {
return this.mounds.get(tileId);
}
this._setLoading(tileId, 'mound', true);
try {
const buffer = await getTileMoundData(tileId);
const moundData = parseFunction(buffer);
this.mounds.set(tileId, moundData);
return moundData;
} catch (err) {
console.error('Failed to fetch mound data:', err);
throw err;
} finally {
this._setLoading(tileId, 'mound', false);
}
},
/**
* Manually set mound data (for cases where you already have it parsed)
* @param {string} tileId
* @param {Object} moundData
*/
setMoundData(tileId, moundData) {
this.mounds.set(tileId, moundData);
},
// ------------------------------------------------------------------------
// Image tracking actions
// ------------------------------------------------------------------------
/**
* Mark images as loaded on map
* @param {string} tileId
*/
markImagesOnMap(tileId) {
this.imagesOnMap.add(tileId);
},
/**
* Remove images from map tracking
* @param {string} tileId
*/
removeImagesFromMap(tileId) {
this.imagesOnMap.delete(tileId);
},
// ------------------------------------------------------------------------
// Tile processing request (SSE)
// ------------------------------------------------------------------------
/**
* Request tile processing with progress updates
* @param {number} lat
* @param {number} lng
* @param {Function} onMessage - Callback for status updates
* @param {Function} onError - Callback for errors
* @returns {EventSource} Connection (call .close() to cancel)
*/
requestTileProcessing(lat, lng, onMessage, onError) {
return requestTileProcessing(lat, lng, onMessage, onError);
},
// ------------------------------------------------------------------------
// Bulk operations
// ------------------------------------------------------------------------
/**
* Clear all caches
*/
clearAll() {
this.metadata.clear();
this.mounds.clear();
this.imagesOnMap.clear();
this.loading.clear();
},
/**
* Remove a specific tile from all caches
* @param {string} tileId
*/
removeTile(tileId) {
this.metadata.delete(tileId);
this.mounds.delete(tileId);
this.imagesOnMap.delete(tileId);
this.loading.delete(tileId);
},
// ------------------------------------------------------------------------
// Internal helpers
// ------------------------------------------------------------------------
/**
* Initialize loading state for a tile if not present
* @private
*/
_initLoadingState(tileId) {
if (!this.loading.has(tileId)) {
this.loading.set(tileId, {
metadata: false,
mound: false,
jpg: false,
png: false
});
}
},
/**
* Set loading state for a specific data type
* @private
*/
_setLoading(tileId, dataType, isLoading) {
this._initLoadingState(tileId);
this.loading.get(tileId)[dataType] = isLoading;
},
}
});

View File

@@ -1,6 +1,5 @@
// ============================================================================
// API UTILITIES
// All backend API interactions for the Hopewell Road Lidar application
// ============================================================================
const API_BASE = ''; // Same origin
@@ -97,8 +96,6 @@ export function requestTileProcessing(lat, lng, onMessage, onError) {
// ============================================================================
// TILE FILE FETCHING
// ============================================================================
// NOTE: Image URL generation is now in tileCache.js via getImageUrl()
// This keeps all cache-related logic in one place
/**
* Fetch tile MOUND data (binary point cloud)

View File

@@ -1,301 +0,0 @@
// ============================================================================
// TILE CACHE SYSTEM
// Centralized storage and lookup for all tile-related data
// ============================================================================
import { ref } from 'vue';
// ============================================================================
// CACHE STORAGE
// ============================================================================
// Metadata from API - keyed by tile ID
// Entry: { id, status, min_lat, max_lat, min_lng, max_lng, error_message,
// jpg_available, png_available, created_at, updated_at }
const metadataCache = ref(new Map());
// Mound data - keyed by tile ID
// Entry: { positions: Float32Array, indices: Uint32Array, bounds: {...} }
const moundCache = ref(new Map());
// Loading state tracker - keyed by tile ID
// Entry: { metadata: bool, mound: bool, jpg: bool, png: bool }
const loadingState = ref(new Map());
// Images loaded on map - keyed by tile ID
// Simple Set tracking which tiles have their images rendered
const imagesOnMap = ref(new Set());
// ============================================================================
// METADATA METHODS
// ============================================================================
/**
* Get tile metadata by ID
* @param {string} tileId
* @returns {Object|null} Metadata object or null if not cached
*/
export function getMetadata(tileId) {
return metadataCache.value.get(tileId) || null;
}
/**
* Store tile metadata
* @param {string} tileId
* @param {Object} metadata - API metadata object
*/
export function setMetadata(tileId, metadata) {
metadataCache.value.set(tileId, metadata);
}
/**
* Find tile ID by coordinates (linear scan with bounds check)
* @param {number} lat
* @param {number} lng
* @returns {string|null} Tile ID or null if no tile contains these coordinates
*/
export function findTileByCoords(lat, lng) {
for (const [tileId, meta] of metadataCache.value) {
if (lat >= meta.min_lat && lat <= meta.max_lat &&
lng >= meta.min_lng && lng <= meta.max_lng) {
return tileId;
}
}
return null;
}
/**
* Check if metadata is cached for a tile
* @param {string} tileId
* @returns {boolean}
*/
export function hasMetadata(tileId) {
return metadataCache.value.has(tileId);
}
// ============================================================================
// MOUND DATA METHODS
// ============================================================================
/**
* Get mound data for a tile
* @param {string} tileId
* @returns {Object|null} Mound data object or null if not cached
*/
export function getMoundData(tileId) {
return moundCache.value.get(tileId) || null;
}
/**
* Store mound data for a tile
* @param {string} tileId
* @param {Object} moundData - { positions, indices, bounds }
*/
export function setMoundData(tileId, moundData) {
moundCache.value.set(tileId, moundData);
}
/**
* Check if mound data is cached for a tile
* @param {string} tileId
* @returns {boolean}
*/
export function hasMoundData(tileId) {
return moundCache.value.has(tileId);
}
// ============================================================================
// IMAGE METHODS
// ============================================================================
/**
* Get image URL for a tile
* @param {string} tileId
* @param {string} type - 'jpg' or 'png'
* @returns {string} Image URL
*/
export function getImageUrl(tileId, type) {
const API_BASE = ''; // Same origin
return `${API_BASE}/tiles/${type}/${tileId}.${type}`;
}
/**
* Mark tile images as loaded on map
* @param {string} tileId
*/
export function setImagesOnMap(tileId) {
imagesOnMap.value.add(tileId);
}
/**
* Check if tile images are on the map
* @param {string} tileId
* @returns {boolean}
*/
export function areImagesOnMap(tileId) {
return imagesOnMap.value.has(tileId);
}
/**
* Remove tile images from map tracking
* @param {string} tileId
*/
export function removeImagesFromMap(tileId) {
imagesOnMap.value.delete(tileId);
}
// ============================================================================
// LOADING STATE METHODS
// ============================================================================
/**
* Initialize loading state for a tile
* @param {string} tileId
*/
function initLoadingState(tileId) {
if (!loadingState.value.has(tileId)) {
loadingState.value.set(tileId, {
metadata: false,
mound: false,
jpg: false,
png: false
});
}
}
/**
* Set loading state for a specific data type
* @param {string} tileId
* @param {string} dataType - 'metadata' | 'mound' | 'jpg' | 'png'
* @param {boolean} isLoading
*/
export function setLoading(tileId, dataType, isLoading) {
initLoadingState(tileId);
loadingState.value.get(tileId)[dataType] = isLoading;
}
/**
* Check if metadata is loading
* @param {string} tileId
* @returns {boolean}
*/
export function isMetadataLoading(tileId) {
return loadingState.value.get(tileId)?.metadata || false;
}
/**
* Check if mound data is loading
* @param {string} tileId
* @returns {boolean}
*/
export function isMoundLoading(tileId) {
return loadingState.value.get(tileId)?.mound || false;
}
/**
* Check if image is loading
* @param {string} tileId
* @param {string} type - 'jpg' or 'png'
* @returns {boolean}
*/
export function isImageLoading(tileId, type) {
return loadingState.value.get(tileId)?.[type] || false;
}
// ============================================================================
// STATUS CHECK METHODS
// ============================================================================
/**
* Check if tile has metadata loaded
* @param {string} tileId
* @returns {boolean}
*/
export function isMetadataLoaded(tileId) {
return hasMetadata(tileId);
}
/**
* Check if tile has mound data loaded
* @param {string} tileId
* @returns {boolean}
*/
export function isMoundLoaded(tileId) {
return hasMoundData(tileId);
}
/**
* Check if tile is ready to render (has both metadata and mound data)
* @param {string} tileId
* @returns {boolean}
*/
export function isReadyToRender(tileId) {
return hasMetadata(tileId) && hasMoundData(tileId);
}
/**
* Check if images are available for a tile (from metadata)
* @param {string} tileId
* @returns {{ jpg: boolean, png: boolean }}
*/
export function getImageAvailability(tileId) {
const meta = getMetadata(tileId);
if (!meta) {
return { jpg: false, png: false };
}
return {
jpg: meta.jpg_available || false,
png: meta.png_available || false
};
}
// ============================================================================
// BULK OPERATIONS
// ============================================================================
/**
* Get all tile IDs that have images on the map
* @returns {string[]}
*/
export function getAllTileIdsWithImages() {
return Array.from(imagesOnMap.value);
}
/**
* Get all tile IDs that have mound data
* @returns {string[]}
*/
export function getAllTileIdsWithMounds() {
return Array.from(moundCache.value.keys());
}
/**
* Get all tile IDs that have metadata
* @returns {string[]}
*/
export function getAllTileIds() {
return Array.from(metadataCache.value.keys());
}
/**
* Clear all caches (useful for testing/debugging)
*/
export function clearAllCaches() {
metadataCache.value.clear();
moundCache.value.clear();
loadingState.value.clear();
imagesOnMap.value.clear();
}
/**
* Get cache statistics (useful for debugging)
* @returns {Object}
*/
export function getCacheStats() {
return {
metadataCount: metadataCache.value.size,
moundCount: moundCache.value.size,
imagesOnMapCount: imagesOnMap.value.size,
loadingCount: loadingState.value.size
};
}