fixes here and there

This commit is contained in:
2026-01-25 12:55:30 +01:00
parent bd5a59d827
commit 6300de22f0
4 changed files with 366 additions and 329 deletions

View File

@@ -9,8 +9,9 @@
<!-- ============================================= -->
<div class="context-menu-header">
{{ formatCoordinate(lngLat.lng, 'lng') }}, {{ formatCoordinate(lngLat.lat, 'lat') }}
<span v-if="tileData?.id" class="tile-name">{{ tileData.id }}</span>
<span v-if="apiError" class="tile-error"> Lookup failed</span>
<span v-if="tileMetadata?.id" class="tile-name">{{ tileMetadata.id }}</span>
<span v-if="metadataError" class="tile-error"> Lookup failed</span>
<span v-if="processingStatus" class="tile-status">{{ processingStatus }}</span>
</div>
<!-- ============================================= -->
@@ -18,40 +19,47 @@
<!-- ============================================= -->
<!-- Always available -->
<button @click="$emit('dropPin')" class="context-menu-item">📍 Drop Pin</button>
<button @click="$emit('startMeasure')" class="context-menu-item">📏 Measure from here</button>
<button @click="handleDropPin" class="context-menu-item">📍 Drop Pin</button>
<button @click="handleStartMeasure" class="context-menu-item">📏 Measure from here</button>
<!-- State A: No tile exists (or error checking) -->
<button
v-if="!tileData && !apiError"
@click="$emit('requestTile')"
v-if="!tileMetadata && !metadataError && !requestingTile"
@click="handleRequestTile"
class="context-menu-item"
>
📥 Request Tile
</button>
<!-- Show status while requesting -->
<div v-if="requestingTile" class="context-menu-status">
{{ processingStatus || 'Requesting tile...' }}
</div>
<!-- State B: Tile exists but images not loaded -->
<button
v-if="tileData && !imagesLoaded && (tileData.jpg_available || tileData.png_available)"
@click="$emit('loadImages')"
v-if="tileMetadata && !imagesOnMap && hasAvailableImages"
@click="handleLoadImages"
class="context-menu-item"
:disabled="loadingImages"
>
📦 Load Tile Images
{{ loadingImages ? '⏳ Loading...' : '📦 Load Tile Images' }}
</button>
<!-- State C: Images loaded, but mound not loaded -->
<button
v-if="tileData && !moundLoaded"
@click="$emit('loadMound')"
v-if="tileMetadata && !hasMoundData"
@click="handleLoadMound"
class="context-menu-item"
:disabled="loadingMound"
>
🔬 Load Interactive Data
{{ loadingMound ? '⏳ Loading...' : '🔬 Load Interactive Data' }}
</button>
<!-- State D: Mound loaded, ready to open sandbox -->
<button
v-if="moundLoaded"
@click="$emit('openSandbox')"
v-if="hasMoundData"
@click="handleOpenSandbox"
class="context-menu-item"
>
🔬 Open in Shading Sandbox
@@ -60,48 +68,213 @@
</template>
<script setup>
import { ref, computed, watch } from 'vue';
import { formatCoordinate } from '../utils/coordinates.js';
import { useTilesStore } from '../stores/tiles.js';
// ============================================================================
// INTERFACE
// PROPS & EMITS
// ============================================================================
const props = defineProps({
visible: {
type: Boolean,
parseMoundBuffer: {
type: Function,
required: true
},
x: {
type: Number,
required: true
},
y: {
type: Number,
required: true
},
lngLat: {
type: Object, // { lng, lat }
required: true
},
tileData: {
type: Object, // Tile metadata from API
default: null
},
imagesLoaded: {
type: Boolean,
default: false
},
moundLoaded: {
type: Boolean,
default: false
},
apiError: {
type: Boolean,
default: false
}
});
defineEmits(['dropPin', 'startMeasure', 'requestTile', 'loadImages', 'loadMound', 'openSandbox']);
const emit = defineEmits([
'dropPin',
'startMeasure',
'addImagesToMap', // Images loaded, ready to add to map
'openSandbox' // Mound loaded, ready to open sandbox
]);
// ============================================================================
// STORE & STATE
// ============================================================================
const tilesStore = useTilesStore();
// Own state
const visible = ref(false);
const x = ref(0);
const y = ref(0);
const lngLat = ref({ lng: 0, lat: 0 });
// Loading/processing states
const loadingImages = ref(false);
const loadingMound = ref(false);
const metadataError = ref(false);
const requestingTile = ref(false);
const processingStatus = ref(null);
// ============================================================================
// COMPUTED - TILE STATE FROM STORE
// ============================================================================
const tileId = computed(() => {
return tilesStore.findTileByCoords(lngLat.value.lat, lngLat.value.lng);
});
const tileMetadata = computed(() => {
return tileId.value ? tilesStore.getMetadata(tileId.value) : null;
});
const imagesOnMap = computed(() => {
return tileId.value ? tilesStore.areImagesOnMap(tileId.value) : false;
});
const hasMoundData = computed(() => {
return tileId.value ? tilesStore.hasMoundData(tileId.value) : false;
});
const hasAvailableImages = computed(() => {
if (!tileMetadata.value) return false;
return tileMetadata.value.jpg_available || tileMetadata.value.png_available;
});
// ============================================================================
// WATCHERS - FETCH METADATA WHEN MENU OPENS
// ============================================================================
watch(visible, async (isVisible) => {
if (isVisible && !tileId.value) {
await fetchMetadata();
}
});
// ============================================================================
// EXPOSED METHODS
// ============================================================================
function show(mouseX, mouseY, coordinates) {
x.value = mouseX;
y.value = mouseY;
lngLat.value = coordinates;
visible.value = true;
// Reset states
metadataError.value = false;
requestingTile.value = false;
processingStatus.value = null;
}
function hide() {
visible.value = false;
}
defineExpose({ show, hide });
// ============================================================================
// ACTIONS - METADATA
// ============================================================================
async function fetchMetadata() {
metadataError.value = false;
try {
await tilesStore.fetchMetadataByCoords(lngLat.value.lat, lngLat.value.lng);
} catch (err) {
console.error('Failed to fetch tile metadata:', err);
metadataError.value = true;
}
}
// ============================================================================
// ACTIONS - TILE OPERATIONS
// ============================================================================
async function handleRequestTile() {
requestingTile.value = true;
processingStatus.value = 'Looking up tile...';
const eventSource = tilesStore.requestTileProcessing(
lngLat.value.lat,
lngLat.value.lng,
async (data) => {
processingStatus.value = data.message || data.status;
// On ready: auto-load mound and open sandbox
if (data.status === 'ready' && data.tile_id) {
try {
await tilesStore.fetchMoundData(data.tile_id, props.parseMoundBuffer);
emit('openSandbox', data.tile_id);
// Close connection and hide menu
eventSource.close();
setTimeout(() => hide(), 500);
} catch (err) {
console.error('Failed to load mound after processing:', err);
processingStatus.value = 'Error loading data';
requestingTile.value = false;
}
}
if (data.status === 'error') {
requestingTile.value = false;
setTimeout(() => {
if (visible.value) {
processingStatus.value = null;
}
}, 5000);
}
},
(error) => {
console.error('Tile processing error:', error);
processingStatus.value = 'Connection failed';
requestingTile.value = false;
setTimeout(() => {
if (visible.value) {
processingStatus.value = null;
}
}, 5000);
}
);
}
async function handleLoadImages() {
if (!tileId.value || loadingImages.value) return;
loadingImages.value = true;
try {
emit('addImagesToMap', tileId.value);
} finally {
loadingImages.value = false;
}
}
async function handleLoadMound() {
if (!tileId.value || loadingMound.value) return;
loadingMound.value = true;
try {
await tilesStore.fetchMoundData(tileId.value, props.parseMoundBuffer);
} catch (err) {
console.error('Failed to load mound data:', err);
} finally {
loadingMound.value = false;
}
}
function handleOpenSandbox() {
if (!tileId.value) return;
emit('openSandbox', tileId.value);
hide();
}
function handleDropPin() {
emit('dropPin', lngLat.value);
hide();
}
function handleStartMeasure() {
emit('startMeasure', lngLat.value);
hide();
}
</script>
<style scoped>
@@ -147,6 +320,26 @@ defineEmits(['dropPin', 'startMeasure', 'requestTile', 'loadImages', 'loadMound'
font-weight: 600;
}
.context-menu-header .tile-status {
display: block;
margin-top: 4px;
font-size: 11px;
color: #2196F3;
font-weight: 500;
}
/* ============================================= */
/* STATUS DISPLAY */
/* ============================================= */
.context-menu-status {
padding: 10px 12px;
font-size: 13px;
color: #666;
background: #f9f9f9;
border-top: 1px solid #eee;
}
/* ============================================= */
/* MENU ITEMS */
/* ============================================= */
@@ -166,4 +359,14 @@ defineEmits(['dropPin', 'startMeasure', 'requestTile', 'loadImages', 'loadMound'
.context-menu-item:hover {
background: #f0f0f0;
}
.context-menu-item:disabled {
cursor: not-allowed;
opacity: 0.6;
background: #f9f9f9;
}
.context-menu-item:disabled:hover {
background: #f9f9f9;
}
</style>