fixes here and there
This commit is contained in:
249
ui/src/App.vue
249
ui/src/App.vue
@@ -30,7 +30,6 @@
|
|||||||
v-model:imperialUnits="imperialUnits"
|
v-model:imperialUnits="imperialUnits"
|
||||||
:visibleSites="visibleSites"
|
:visibleSites="visibleSites"
|
||||||
:allHistoricMarkersHidden="allHistoricMarkersHidden"
|
:allHistoricMarkersHidden="allHistoricMarkersHidden"
|
||||||
:tileRequests="tileRequests"
|
|
||||||
@toggleSite="toggleSite"
|
@toggleSite="toggleSite"
|
||||||
@jumpToSite="jumpToSite"
|
@jumpToSite="jumpToSite"
|
||||||
@toggleAllSites="hideAllHistoricMarkers"
|
@toggleAllSites="hideAllHistoricMarkers"
|
||||||
@@ -39,7 +38,6 @@
|
|||||||
@update:lidarOpacity="updateLidarOpacity"
|
@update:lidarOpacity="updateLidarOpacity"
|
||||||
@update:showLidar="toggleLidar"
|
@update:showLidar="toggleLidar"
|
||||||
@update:showGeometry="toggleGeometry"
|
@update:showGeometry="toggleGeometry"
|
||||||
@dismissRequest="dismissTileRequest"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- ============================================= -->
|
<!-- ============================================= -->
|
||||||
@@ -56,20 +54,12 @@
|
|||||||
<!-- CONTEXT MENU -->
|
<!-- CONTEXT MENU -->
|
||||||
<!-- ============================================= -->
|
<!-- ============================================= -->
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
:visible="contextMenu.visible"
|
ref="contextMenuRef"
|
||||||
:x="contextMenu.x"
|
:parseMoundBuffer="parseMoundBuffer"
|
||||||
:y="contextMenu.y"
|
@dropPin="handleDropPin"
|
||||||
:lngLat="contextMenu.lngLat || { lng: 0, lat: 0 }"
|
@startMeasure="handleStartMeasure"
|
||||||
:tileData="contextMenu.tileData"
|
@addImagesToMap="handleAddImagesToMap"
|
||||||
:imagesLoaded="contextMenu.imagesLoaded"
|
@openSandbox="handleOpenSandbox"
|
||||||
:moundLoaded="contextMenu.moundLoaded"
|
|
||||||
:apiError="contextMenu.apiError"
|
|
||||||
@dropPin="dropPin"
|
|
||||||
@startMeasure="startMeasure"
|
|
||||||
@requestTile="requestTileFromContextMenu"
|
|
||||||
@loadImages="loadImagesFromContextMenu"
|
|
||||||
@loadMound="loadMoundFromContextMenu"
|
|
||||||
@openSandbox="openSandboxFromContextMenu"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- ============================================= -->
|
<!-- ============================================= -->
|
||||||
@@ -122,6 +112,7 @@ import { KNOWN_SITES } from './data/historicSites.js';
|
|||||||
// UTILITIES
|
// UTILITIES
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
import { calculateDistance, calculateBearing, extendRay } from './utils/geometry.js';
|
import { calculateDistance, calculateBearing, extendRay } from './utils/geometry.js';
|
||||||
|
import { webMercatorToLonLat } from './utils/coordinates.js';
|
||||||
import { useTilesStore } from './stores/tiles.js';
|
import { useTilesStore } from './stores/tiles.js';
|
||||||
|
|
||||||
// For generating the pre-baked tiles:
|
// For generating the pre-baked tiles:
|
||||||
@@ -162,7 +153,7 @@ const sandboxRef = ref(null);
|
|||||||
const sandboxVisible = ref(false);
|
const sandboxVisible = ref(false);
|
||||||
const sandboxOffscreen = ref(false);
|
const sandboxOffscreen = ref(false);
|
||||||
const baseLayer = ref('osm');
|
const baseLayer = ref('osm');
|
||||||
const historicMarkersExpanded = ref(true);
|
const historicMarkersExpanded = ref(false);
|
||||||
const visibleSites = ref({});
|
const visibleSites = ref({});
|
||||||
const allHistoricMarkersHidden = ref(false);
|
const allHistoricMarkersHidden = ref(false);
|
||||||
const showLidar = ref(true);
|
const showLidar = ref(true);
|
||||||
@@ -175,10 +166,6 @@ const lidarOpacity = ref(80);
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
const currentTileData = ref(null);
|
const currentTileData = ref(null);
|
||||||
|
|
||||||
// Tile request tracking: { requestId: { lat, lng, status, message, tileId } }
|
|
||||||
const tileRequests = ref({});
|
|
||||||
let nextRequestId = 1;
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// REFS - Geometry State
|
// REFS - Geometry State
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -191,16 +178,7 @@ const nextFeatureId = ref(1);
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
// REFS - UI Overlays
|
// REFS - UI Overlays
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
const contextMenu = ref({
|
const contextMenuRef = ref(null);
|
||||||
visible: false,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
lngLat: null,
|
|
||||||
tileData: null, // API tile metadata
|
|
||||||
imagesLoaded: false, // JPG/PNG loaded on map
|
|
||||||
moundLoaded: false, // .mound data in cache
|
|
||||||
apiError: false // Failed to fetch tile info
|
|
||||||
});
|
|
||||||
|
|
||||||
const popup = ref({
|
const popup = ref({
|
||||||
visible: false,
|
visible: false,
|
||||||
@@ -220,31 +198,6 @@ const highlightedCitation = ref(null);
|
|||||||
let map = null;
|
let map = null;
|
||||||
let drawingHandler = null;
|
let drawingHandler = null;
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// COORDINATE UTILITIES
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
function webMercatorToLonLat(x, y) {
|
|
||||||
const R = 6378137;
|
|
||||||
const lon = (x / R) * (180 / Math.PI);
|
|
||||||
const lat = (2 * Math.atan(Math.exp(y / R)) - Math.PI / 2) * (180 / Math.PI);
|
|
||||||
return [lon, lat];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// TILE UTILITIES
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
// Check if tile images are loaded on map
|
|
||||||
function areTileImagesLoaded(tileId) {
|
|
||||||
return tilesStore.areImagesOnMap(tileId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if mound data is cached
|
|
||||||
function isMoundDataCached(tileId) {
|
|
||||||
return tilesStore.hasMoundData(tileId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// FILE PARSING
|
// FILE PARSING
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -296,13 +249,13 @@ function parseMoundBuffer(buffer) {
|
|||||||
|
|
||||||
async function loadTileImages(tileId, tileMetadata, imageUrl = null) {
|
async function loadTileImages(tileId, tileMetadata, imageUrl = null) {
|
||||||
// If no explicit imageUrl provided (loading from server), skip if already loaded
|
// If no explicit imageUrl provided (loading from server), skip if already loaded
|
||||||
if (!imageUrl && areTileImagesLoaded(tileId)) {
|
if (!imageUrl && tilesStore.areImagesOnMap(tileId)) {
|
||||||
console.log(`Images for ${tileId} already loaded`);
|
console.log(`Images for ${tileId} already loaded`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If imageUrl is provided (sandbox render), allow overwriting
|
// If imageUrl is provided (sandbox render), allow overwriting
|
||||||
const isOverwriting = imageUrl && areTileImagesLoaded(tileId);
|
const isOverwriting = imageUrl && tilesStore.areImagesOnMap(tileId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Determine which format to load (prefer PNG over JPG)
|
// Determine which format to load (prefer PNG over JPG)
|
||||||
@@ -381,157 +334,46 @@ async function loadInitialTiles() {
|
|||||||
console.log(`Loaded ${tilesStore.getAllTileIdsWithImages.length} initial tiles`);
|
console.log(`Loaded ${tilesStore.getAllTileIdsWithImages.length} initial tiles`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadMoundData(tileId) {
|
|
||||||
if (isMoundDataCached(tileId)) {
|
|
||||||
console.log(`Mound data for ${tileId} already cached`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// CONTEXT MENU HANDLERS
|
// CONTEXT MENU HANDLER
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
async function handleContextMenu(e) {
|
async function handleContextMenu(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const lng = e.lngLat.lng;
|
if (contextMenuRef.value) {
|
||||||
const lat = e.lngLat.lat;
|
contextMenuRef.value.show(e.point.x, e.point.y, {
|
||||||
|
lng: e.lngLat.lng,
|
||||||
// Reset context menu state
|
lat: e.lngLat.lat
|
||||||
contextMenu.value = {
|
});
|
||||||
visible: false,
|
|
||||||
x: e.point.x,
|
|
||||||
y: e.point.y,
|
|
||||||
lngLat: { lng, lat },
|
|
||||||
tileData: null,
|
|
||||||
imagesLoaded: false,
|
|
||||||
moundLoaded: false,
|
|
||||||
apiError: false
|
|
||||||
};
|
|
||||||
|
|
||||||
// Try to fetch tile metadata from API
|
|
||||||
try {
|
|
||||||
const tileData = await tilesStore.fetchMetadataByCoords(lat, lng);
|
|
||||||
|
|
||||||
if (tileData) {
|
|
||||||
contextMenu.value.tileData = tileData;
|
|
||||||
contextMenu.value.imagesLoaded = areTileImagesLoaded(tileData.id);
|
|
||||||
contextMenu.value.moundLoaded = isMoundDataCached(tileData.id);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to fetch tile info:', err);
|
|
||||||
contextMenu.value.apiError = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show context menu
|
|
||||||
contextMenu.value.visible = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestTileFromContextMenu() {
|
// ============================================================================
|
||||||
const { lng, lat } = contextMenu.value.lngLat;
|
// CONTEXT MENU - MAP OPERATION HANDLERS
|
||||||
contextMenu.value.visible = false;
|
// ============================================================================
|
||||||
|
|
||||||
const requestId = `req-${nextRequestId++}`;
|
async function handleAddImagesToMap(tileId) {
|
||||||
|
const tileMetadata = tilesStore.getMetadata(tileId);
|
||||||
// Initialize request tracking
|
if (tileMetadata) {
|
||||||
tileRequests.value[requestId] = {
|
|
||||||
lat,
|
|
||||||
lng,
|
|
||||||
status: 'looking_up',
|
|
||||||
message: null,
|
|
||||||
tileId: null
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start SSE request
|
|
||||||
tilesStore.requestTileProcessing(
|
|
||||||
lat,
|
|
||||||
lng,
|
|
||||||
async (data) => {
|
|
||||||
// Update request status
|
|
||||||
tileRequests.value[requestId].status = data.status;
|
|
||||||
tileRequests.value[requestId].message = data.message || null;
|
|
||||||
|
|
||||||
if (data.tile_id) {
|
|
||||||
tileRequests.value[requestId].tileId = data.tile_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// On ready: load mound and open sandbox
|
|
||||||
if (data.status === 'ready' && data.tile_id) {
|
|
||||||
try {
|
try {
|
||||||
await loadMoundData(data.tile_id);
|
await loadTileImages(tileId, tileMetadata);
|
||||||
await openSandboxWithTile(data.tile_id);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to load mound after request:', err);
|
|
||||||
tileRequests.value[requestId].status = 'error';
|
|
||||||
tileRequests.value[requestId].message = 'Failed to load data';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-dismiss after 10 seconds if complete
|
|
||||||
if (data.status === 'ready' || data.status === 'error') {
|
|
||||||
setTimeout(() => {
|
|
||||||
delete tileRequests.value[requestId];
|
|
||||||
}, 10000);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
console.error('Request tile error:', error);
|
|
||||||
tileRequests.value[requestId].status = 'error';
|
|
||||||
tileRequests.value[requestId].message = 'Connection failed';
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
delete tileRequests.value[requestId];
|
|
||||||
}, 10000);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadImagesFromContextMenu() {
|
|
||||||
const tileData = contextMenu.value.tileData;
|
|
||||||
contextMenu.value.visible = false;
|
|
||||||
|
|
||||||
if (tileData) {
|
|
||||||
try {
|
|
||||||
await loadTileImages(tileData.id, tileData);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load images:', err);
|
console.error('Failed to load images:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadMoundFromContextMenu() {
|
function handleOpenSandbox(tileId) {
|
||||||
const tileData = contextMenu.value.tileData;
|
openSandboxWithTile(tileId);
|
||||||
contextMenu.value.visible = false;
|
|
||||||
|
|
||||||
if (tileData) {
|
|
||||||
try {
|
|
||||||
await loadMoundData(tileData.id);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to load mound data:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openSandboxFromContextMenu() {
|
function handleDropPin(lngLat) {
|
||||||
const tileData = contextMenu.value.tileData;
|
dropPin(lngLat);
|
||||||
contextMenu.value.visible = false;
|
|
||||||
|
|
||||||
if (tileData) {
|
|
||||||
await openSandboxWithTile(tileData.id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function dismissTileRequest(requestId) {
|
function handleStartMeasure(lngLat) {
|
||||||
delete tileRequests.value[requestId];
|
startMeasure(lngLat);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -657,10 +499,6 @@ function openSandbox() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function openSandboxWithTile(tileId) {
|
async function openSandboxWithTile(tileId) {
|
||||||
if (!isMoundDataCached(tileId)) {
|
|
||||||
await loadMoundData(tileId);
|
|
||||||
}
|
|
||||||
|
|
||||||
sandboxVisible.value = true;
|
sandboxVisible.value = true;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -714,7 +552,8 @@ function setDrawMode(mode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
drawingHandler = (e) => {
|
drawingHandler = (e) => {
|
||||||
if (contextMenu.value.visible) return;
|
// Don't draw if context menu is visible
|
||||||
|
if (contextMenuRef.value?.visible) return;
|
||||||
|
|
||||||
drawPoints.value.push([e.lngLat.lng, e.lngLat.lat]);
|
drawPoints.value.push([e.lngLat.lng, e.lngLat.lat]);
|
||||||
|
|
||||||
@@ -758,8 +597,8 @@ function completeDrawing() {
|
|||||||
setDrawMode(null);
|
setDrawMode(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function dropPin() {
|
function dropPin(lngLat) {
|
||||||
const { lng, lat } = contextMenu.value.lngLat;
|
const { lng, lat } = lngLat;
|
||||||
|
|
||||||
const feature = {
|
const feature = {
|
||||||
type: 'Feature',
|
type: 'Feature',
|
||||||
@@ -777,13 +616,11 @@ function dropPin() {
|
|||||||
geometryFeatures.value.features.push(feature);
|
geometryFeatures.value.features.push(feature);
|
||||||
updateGeometryLayer();
|
updateGeometryLayer();
|
||||||
saveGeometry();
|
saveGeometry();
|
||||||
contextMenu.value.visible = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function startMeasure() {
|
function startMeasure(lngLat) {
|
||||||
contextMenu.value.visible = false;
|
|
||||||
setDrawMode('line');
|
setDrawMode('line');
|
||||||
const { lng, lat } = contextMenu.value.lngLat;
|
const { lng, lat } = lngLat;
|
||||||
drawPoints.value = [[lng, lat]];
|
drawPoints.value = [[lng, lat]];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -882,7 +719,8 @@ onMounted(() => {
|
|||||||
type: 'raster',
|
type: 'raster',
|
||||||
tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
|
tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
|
||||||
tileSize: 256,
|
tileSize: 256,
|
||||||
attribution: '© OpenStreetMap contributors'
|
attribution: '© OpenStreetMap contributors',
|
||||||
|
maxZoom: 20
|
||||||
},
|
},
|
||||||
satellite: {
|
satellite: {
|
||||||
type: 'raster',
|
type: 'raster',
|
||||||
@@ -890,7 +728,8 @@ onMounted(() => {
|
|||||||
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'
|
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'
|
||||||
],
|
],
|
||||||
tileSize: 256,
|
tileSize: 256,
|
||||||
attribution: '© Esri'
|
attribution: '© Esri',
|
||||||
|
maxZoom: 21
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
layers: [
|
layers: [
|
||||||
@@ -945,12 +784,16 @@ onMounted(() => {
|
|||||||
popup.value.visible = false;
|
popup.value.visible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
contextMenu.value.visible = false;
|
if (contextMenuRef.value) {
|
||||||
|
contextMenuRef.value.hide();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
contextMenu.value.visible = false;
|
if (contextMenuRef.value) {
|
||||||
|
contextMenuRef.value.hide();
|
||||||
|
}
|
||||||
popup.value.visible = false;
|
popup.value.visible = false;
|
||||||
bibliographyVisible.value = false;
|
bibliographyVisible.value = false;
|
||||||
if (drawMode.value) {
|
if (drawMode.value) {
|
||||||
|
|||||||
@@ -9,8 +9,9 @@
|
|||||||
<!-- ============================================= -->
|
<!-- ============================================= -->
|
||||||
<div class="context-menu-header">
|
<div class="context-menu-header">
|
||||||
{{ formatCoordinate(lngLat.lng, 'lng') }}, {{ formatCoordinate(lngLat.lat, 'lat') }}
|
{{ formatCoordinate(lngLat.lng, 'lng') }}, {{ formatCoordinate(lngLat.lat, 'lat') }}
|
||||||
<span v-if="tileData?.id" class="tile-name">{{ tileData.id }}</span>
|
<span v-if="tileMetadata?.id" class="tile-name">{{ tileMetadata.id }}</span>
|
||||||
<span v-if="apiError" class="tile-error">⚠️ Lookup failed</span>
|
<span v-if="metadataError" class="tile-error">⚠️ Lookup failed</span>
|
||||||
|
<span v-if="processingStatus" class="tile-status">{{ processingStatus }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ============================================= -->
|
<!-- ============================================= -->
|
||||||
@@ -18,40 +19,47 @@
|
|||||||
<!-- ============================================= -->
|
<!-- ============================================= -->
|
||||||
|
|
||||||
<!-- Always available -->
|
<!-- Always available -->
|
||||||
<button @click="$emit('dropPin')" class="context-menu-item">📍 Drop Pin</button>
|
<button @click="handleDropPin" class="context-menu-item">📍 Drop Pin</button>
|
||||||
<button @click="$emit('startMeasure')" class="context-menu-item">📏 Measure from here</button>
|
<button @click="handleStartMeasure" class="context-menu-item">📏 Measure from here</button>
|
||||||
|
|
||||||
<!-- State A: No tile exists (or error checking) -->
|
<!-- State A: No tile exists (or error checking) -->
|
||||||
<button
|
<button
|
||||||
v-if="!tileData && !apiError"
|
v-if="!tileMetadata && !metadataError && !requestingTile"
|
||||||
@click="$emit('requestTile')"
|
@click="handleRequestTile"
|
||||||
class="context-menu-item"
|
class="context-menu-item"
|
||||||
>
|
>
|
||||||
📥 Request Tile
|
📥 Request Tile
|
||||||
</button>
|
</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 -->
|
<!-- State B: Tile exists but images not loaded -->
|
||||||
<button
|
<button
|
||||||
v-if="tileData && !imagesLoaded && (tileData.jpg_available || tileData.png_available)"
|
v-if="tileMetadata && !imagesOnMap && hasAvailableImages"
|
||||||
@click="$emit('loadImages')"
|
@click="handleLoadImages"
|
||||||
class="context-menu-item"
|
class="context-menu-item"
|
||||||
|
:disabled="loadingImages"
|
||||||
>
|
>
|
||||||
📦 Load Tile Images
|
{{ loadingImages ? '⏳ Loading...' : '📦 Load Tile Images' }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- State C: Images loaded, but mound not loaded -->
|
<!-- State C: Images loaded, but mound not loaded -->
|
||||||
<button
|
<button
|
||||||
v-if="tileData && !moundLoaded"
|
v-if="tileMetadata && !hasMoundData"
|
||||||
@click="$emit('loadMound')"
|
@click="handleLoadMound"
|
||||||
class="context-menu-item"
|
class="context-menu-item"
|
||||||
|
:disabled="loadingMound"
|
||||||
>
|
>
|
||||||
🔬 Load Interactive Data
|
{{ loadingMound ? '⏳ Loading...' : '🔬 Load Interactive Data' }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- State D: Mound loaded, ready to open sandbox -->
|
<!-- State D: Mound loaded, ready to open sandbox -->
|
||||||
<button
|
<button
|
||||||
v-if="moundLoaded"
|
v-if="hasMoundData"
|
||||||
@click="$emit('openSandbox')"
|
@click="handleOpenSandbox"
|
||||||
class="context-menu-item"
|
class="context-menu-item"
|
||||||
>
|
>
|
||||||
🔬 Open in Shading Sandbox
|
🔬 Open in Shading Sandbox
|
||||||
@@ -60,48 +68,213 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref, computed, watch } from 'vue';
|
||||||
import { formatCoordinate } from '../utils/coordinates.js';
|
import { formatCoordinate } from '../utils/coordinates.js';
|
||||||
|
import { useTilesStore } from '../stores/tiles.js';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// INTERFACE
|
// PROPS & EMITS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: {
|
parseMoundBuffer: {
|
||||||
type: Boolean,
|
type: Function,
|
||||||
required: true
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -147,6 +320,26 @@ defineEmits(['dropPin', 'startMeasure', 'requestTile', 'loadImages', 'loadMound'
|
|||||||
font-weight: 600;
|
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 */
|
/* MENU ITEMS */
|
||||||
/* ============================================= */
|
/* ============================================= */
|
||||||
@@ -166,4 +359,14 @@ defineEmits(['dropPin', 'startMeasure', 'requestTile', 'loadImages', 'loadMound'
|
|||||||
.context-menu-item:hover {
|
.context-menu-item:hover {
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.context-menu-item:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item:disabled:hover {
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -290,15 +290,16 @@ const handleResize = () => {
|
|||||||
renderer.setSize(width, height);
|
renderer.setSize(width, height);
|
||||||
renderer.setPixelRatio(window.devicePixelRatio);
|
renderer.setPixelRatio(window.devicePixelRatio);
|
||||||
|
|
||||||
// Use exact normalized terrain bounds (no padding = no black bars)
|
// Use exact tile boundaries (guaranteed square)
|
||||||
if (geometryCache) {
|
if (geometryCache) {
|
||||||
camera.left = -geometryCache.normalizedSpanX / 2;
|
const halfSpan = geometryCache.tileSpan / 2;
|
||||||
camera.right = geometryCache.normalizedSpanX / 2;
|
camera.left = -halfSpan;
|
||||||
camera.top = geometryCache.normalizedSpanY / 2;
|
camera.right = halfSpan;
|
||||||
camera.bottom = -geometryCache.normalizedSpanY / 2;
|
camera.top = halfSpan;
|
||||||
|
camera.bottom = -halfSpan;
|
||||||
} else {
|
} else {
|
||||||
// Fallback: square frustum if no tile loaded yet
|
// Fallback: square frustum if no tile loaded yet
|
||||||
const viewSize = 6;
|
const viewSize = 500; // Reasonable default in world units
|
||||||
camera.left = -viewSize;
|
camera.left = -viewSize;
|
||||||
camera.right = viewSize;
|
camera.right = viewSize;
|
||||||
camera.top = viewSize;
|
camera.top = viewSize;
|
||||||
@@ -325,32 +326,30 @@ const loadTileData = (tileData, newTileId) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Calculate bounds center
|
// Tile bounds (guaranteed square, defines camera frustum)
|
||||||
const centerX = (tileData.bounds.minX + tileData.bounds.maxX) / 2;
|
const tileBounds = tileData.tileBounds || tileData.bounds;
|
||||||
const centerY = (tileData.bounds.minY + tileData.bounds.maxY) / 2;
|
const tileCenterX = (tileBounds.minX + tileBounds.maxX) / 2;
|
||||||
const centerZ = (tileData.bounds.minZ + tileData.bounds.maxZ) / 2;
|
const tileCenterY = (tileBounds.minY + tileBounds.maxY) / 2;
|
||||||
|
const tileSpan = tileBounds.maxX - tileBounds.minX; // Should equal maxY - minY (square)
|
||||||
|
|
||||||
// Calculate spans
|
// Mesh bounds (actual point data extent, for Z normalization)
|
||||||
const spanX = tileData.bounds.maxX - tileData.bounds.minX;
|
const meshBounds = tileData.bounds;
|
||||||
const spanY = tileData.bounds.maxY - tileData.bounds.minY;
|
const meshCenterZ = (meshBounds.minZ + meshBounds.maxZ) / 2;
|
||||||
const spanZ = tileData.bounds.maxZ - tileData.bounds.minZ;
|
const meshSpanZ = meshBounds.maxZ - meshBounds.minZ;
|
||||||
const maxSpan = Math.max(spanX, spanY);
|
|
||||||
|
|
||||||
// Calculate actual aspect ratio of the tile
|
// Z normalization: scale Z to be perceptible relative to tile span
|
||||||
const tileAspect = spanX / spanY;
|
// Use 10% of tile span as base Z scale (adjust this factor as needed for aesthetics)
|
||||||
|
const zNormalizationFactor = (tileSpan * 0.1) / meshSpanZ;
|
||||||
// Normalize XY to fit in view, maintaining actual aspect ratio
|
|
||||||
const normalizeScale = 10 / maxSpan;
|
|
||||||
|
|
||||||
// Z scaling: make Z variation visible but proportional
|
|
||||||
const zScale = normalizeScale * (maxSpan * 0.1) / spanZ;
|
|
||||||
|
|
||||||
// Transform positions
|
// Transform positions
|
||||||
const transformedPositions = new Float32Array(tileData.positions.length);
|
const transformedPositions = new Float32Array(tileData.positions.length);
|
||||||
for (let i = 0; i < tileData.positions.length; i += 3) {
|
for (let i = 0; i < tileData.positions.length; i += 3) {
|
||||||
transformedPositions[i] = (tileData.positions[i] - centerX) * normalizeScale;
|
// X/Y: keep world coordinates, just centered on tile center
|
||||||
transformedPositions[i + 1] = (tileData.positions[i + 1] - centerY) * normalizeScale;
|
transformedPositions[i] = tileData.positions[i] - tileCenterX;
|
||||||
transformedPositions[i + 2] = (tileData.positions[i + 2] - centerZ) * zScale;
|
transformedPositions[i + 1] = tileData.positions[i + 1] - tileCenterY;
|
||||||
|
|
||||||
|
// Z: normalized relative to tile span, then will be height-exaggerated later
|
||||||
|
transformedPositions[i + 2] = (tileData.positions[i + 2] - meshCenterZ) * zNormalizationFactor;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create geometry
|
// Create geometry
|
||||||
@@ -367,20 +366,14 @@ const loadTileData = (tileData, newTileId) => {
|
|||||||
baseZ[i + 2] = transformedPositions[i + 2]; // Only Z values
|
baseZ[i + 2] = transformedPositions[i + 2]; // Only Z values
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate exact bounds of normalized terrain (no padding = no black bars)
|
|
||||||
const normalizedSpanX = spanX * normalizeScale;
|
|
||||||
const normalizedSpanY = spanY * normalizeScale;
|
|
||||||
|
|
||||||
geometryCache = {
|
geometryCache = {
|
||||||
geometry,
|
geometry,
|
||||||
baseZ,
|
baseZ,
|
||||||
spanZ,
|
tileBounds: tileBounds,
|
||||||
zScale,
|
tileSpan: tileSpan,
|
||||||
tileAspect,
|
zNormalizationFactor: zNormalizationFactor,
|
||||||
normalizedSpanX,
|
// Store original bounds for reference
|
||||||
normalizedSpanY,
|
originalBounds: { ...meshBounds }
|
||||||
// Store original Web Mercator bounds for MapLibre positioning
|
|
||||||
originalBounds: { ...tileData.bounds }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create material and mesh
|
// Create material and mesh
|
||||||
@@ -392,14 +385,15 @@ const loadTileData = (tileData, newTileId) => {
|
|||||||
mesh = new THREE.Mesh(geometry, material);
|
mesh = new THREE.Mesh(geometry, material);
|
||||||
scene.add(mesh);
|
scene.add(mesh);
|
||||||
|
|
||||||
// Configure camera frustum to match tile aspect ratio
|
// Configure camera frustum to match tile boundaries (guaranteed square)
|
||||||
camera.left = -normalizedSpanX / 2;
|
const halfSpan = tileSpan / 2;
|
||||||
camera.right = normalizedSpanX / 2;
|
camera.left = -halfSpan;
|
||||||
camera.top = normalizedSpanY / 2;
|
camera.right = halfSpan;
|
||||||
camera.bottom = -normalizedSpanY / 2;
|
camera.top = halfSpan;
|
||||||
|
camera.bottom = -halfSpan;
|
||||||
|
|
||||||
// Adjust near/far to accommodate height exaggeration
|
// Adjust near/far to accommodate height exaggeration
|
||||||
const maxZExtent = (spanZ * zScale / 2) * 20; // Max exaggeration
|
const maxZExtent = (tileSpan * 0.1) * 20; // Max exaggeration (0.1 factor * 20x scale)
|
||||||
camera.near = 0.1;
|
camera.near = 0.1;
|
||||||
camera.far = 100 + maxZExtent * 2;
|
camera.far = 100 + maxZExtent * 2;
|
||||||
camera.updateProjectionMatrix();
|
camera.updateProjectionMatrix();
|
||||||
@@ -491,28 +485,21 @@ const renderTile = async () => {
|
|||||||
const originalHeight = renderer.domElement.height;
|
const originalHeight = renderer.domElement.height;
|
||||||
const originalPixelRatio = renderer.getPixelRatio();
|
const originalPixelRatio = renderer.getPixelRatio();
|
||||||
|
|
||||||
// Calculate render dimensions based on tile aspect ratio
|
// Tiles are guaranteed square, so render dimensions are always square
|
||||||
let renderWidth = size;
|
const renderWidth = size;
|
||||||
let renderHeight = size;
|
const renderHeight = size;
|
||||||
if (geometryCache) {
|
|
||||||
const tileAspect = geometryCache.normalizedSpanX / geometryCache.normalizedSpanY;
|
|
||||||
if (tileAspect > 1) {
|
|
||||||
renderHeight = Math.round(size / tileAspect);
|
|
||||||
} else {
|
|
||||||
renderWidth = Math.round(size * tileAspect);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set render size
|
// Set render size
|
||||||
renderer.setSize(renderWidth, renderHeight);
|
renderer.setSize(renderWidth, renderHeight);
|
||||||
renderer.setPixelRatio(1); // No pixel ratio multiplier for export
|
renderer.setPixelRatio(1); // No pixel ratio multiplier for export
|
||||||
|
|
||||||
// Update camera to match exact tile bounds (no black bars)
|
// Update camera to match exact tile bounds (guaranteed square)
|
||||||
if (geometryCache) {
|
if (geometryCache) {
|
||||||
camera.left = -geometryCache.normalizedSpanX / 2;
|
const halfSpan = geometryCache.tileSpan / 2;
|
||||||
camera.right = geometryCache.normalizedSpanX / 2;
|
camera.left = -halfSpan;
|
||||||
camera.top = geometryCache.normalizedSpanY / 2;
|
camera.right = halfSpan;
|
||||||
camera.bottom = -geometryCache.normalizedSpanY / 2;
|
camera.top = halfSpan;
|
||||||
|
camera.bottom = -halfSpan;
|
||||||
}
|
}
|
||||||
camera.updateProjectionMatrix();
|
camera.updateProjectionMatrix();
|
||||||
|
|
||||||
@@ -588,28 +575,21 @@ const renderTileWithSettings = async (tileData, renderSettings, resolution = 102
|
|||||||
const originalHeight = renderer.domElement.height;
|
const originalHeight = renderer.domElement.height;
|
||||||
const originalPixelRatio = renderer.getPixelRatio();
|
const originalPixelRatio = renderer.getPixelRatio();
|
||||||
|
|
||||||
// Calculate render dimensions based on tile aspect ratio
|
// Tiles are guaranteed square, so render dimensions are always square
|
||||||
let renderWidth = resolution;
|
const renderWidth = resolution;
|
||||||
let renderHeight = resolution;
|
const renderHeight = resolution;
|
||||||
if (geometryCache) {
|
|
||||||
const tileAspect = geometryCache.normalizedSpanX / geometryCache.normalizedSpanY;
|
|
||||||
if (tileAspect > 1) {
|
|
||||||
renderHeight = Math.round(resolution / tileAspect);
|
|
||||||
} else {
|
|
||||||
renderWidth = Math.round(resolution * tileAspect);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set render size
|
// Set render size
|
||||||
renderer.setSize(renderWidth, renderHeight);
|
renderer.setSize(renderWidth, renderHeight);
|
||||||
renderer.setPixelRatio(1);
|
renderer.setPixelRatio(1);
|
||||||
|
|
||||||
// Update camera to match exact tile bounds (no black bars)
|
// Update camera to match exact tile bounds (guaranteed square)
|
||||||
if (geometryCache) {
|
if (geometryCache) {
|
||||||
camera.left = -geometryCache.normalizedSpanX / 2;
|
const halfSpan = geometryCache.tileSpan / 2;
|
||||||
camera.right = geometryCache.normalizedSpanX / 2;
|
camera.left = -halfSpan;
|
||||||
camera.top = geometryCache.normalizedSpanY / 2;
|
camera.right = halfSpan;
|
||||||
camera.bottom = -geometryCache.normalizedSpanY / 2;
|
camera.top = halfSpan;
|
||||||
|
camera.bottom = -halfSpan;
|
||||||
}
|
}
|
||||||
camera.updateProjectionMatrix();
|
camera.updateProjectionMatrix();
|
||||||
|
|
||||||
@@ -623,12 +603,13 @@ const renderTileWithSettings = async (tileData, renderSettings, resolution = 102
|
|||||||
renderer.setSize(originalWidth / originalPixelRatio, originalHeight / originalPixelRatio);
|
renderer.setSize(originalWidth / originalPixelRatio, originalHeight / originalPixelRatio);
|
||||||
renderer.setPixelRatio(originalPixelRatio);
|
renderer.setPixelRatio(originalPixelRatio);
|
||||||
|
|
||||||
// Restore camera with exact normalized bounds
|
// Restore camera with exact tile bounds
|
||||||
if (geometryCache) {
|
if (geometryCache) {
|
||||||
camera.left = -geometryCache.normalizedSpanX / 2;
|
const halfSpan = geometryCache.tileSpan / 2;
|
||||||
camera.right = geometryCache.normalizedSpanX / 2;
|
camera.left = -halfSpan;
|
||||||
camera.top = geometryCache.normalizedSpanY / 2;
|
camera.right = halfSpan;
|
||||||
camera.bottom = -geometryCache.normalizedSpanY / 2;
|
camera.top = halfSpan;
|
||||||
|
camera.bottom = -halfSpan;
|
||||||
}
|
}
|
||||||
camera.updateProjectionMatrix();
|
camera.updateProjectionMatrix();
|
||||||
|
|
||||||
|
|||||||
@@ -40,3 +40,13 @@ export function formatDistance(meters, useImperial = false) {
|
|||||||
export function formatBearing(degrees) {
|
export function formatBearing(degrees) {
|
||||||
return `${degrees.toFixed(1)}°`;
|
return `${degrees.toFixed(1)}°`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert web Mercator to lat/long
|
||||||
|
*/
|
||||||
|
export function webMercatorToLonLat(x, y) {
|
||||||
|
const R = 6378137;
|
||||||
|
const lon = (x / R) * (180 / Math.PI);
|
||||||
|
const lat = (2 * Math.atan(Math.exp(y / R)) - Math.PI / 2) * (180 / Math.PI);
|
||||||
|
return [lon, lat];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user