Files
MoundHunters/ui/src/App.vue
2026-01-21 00:57:53 +01:00

440 lines
10 KiB
Vue

<template>
<div id="app">
<div ref="mapContainer" class="map-container"></div>
<ShadingSandbox
:visible="sandboxVisible"
:offscreen="sandboxOffscreen"
:initial-settings="DEFAULT_RENDER_SETTINGS"
ref="sandboxRef"
@close="sandboxVisible = false"
@renderComplete="onRenderComplete"
@error="onRenderError"
/>
<div class="layer-controls">
<div class="control-section">
<label>Base Map:</label>
<label><input type="radio" value="osm" v-model="baseLayer" @change="updateBaseLayer"> Street</label>
<label><input type="radio" value="satellite" v-model="baseLayer" @change="updateBaseLayer"> Satellite</label>
</div>
<div class="control-section">
<label>
<input type="checkbox" v-model="showOctagon" @change="toggleOctagon">
Show Octagon
</label>
<label>
<input type="checkbox" v-model="showLidar" @change="toggleLidar">
Show Lidar
</label>
</div>
<div class="control-section">
<button class="sandbox-btn" @click="openSandbox">Open Shading Sandbox</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
import ShadingSandbox from './ShadingSandbox.vue';
// Default render settings - used everywhere
const DEFAULT_RENDER_SETTINGS = {
azimuth: 90,
altitude: 60,
intensity: 1.2,
heightScale: 3,
terrainColor: 0x9A9996
};
// Refs
const mapContainer = ref(null);
const sandboxRef = ref(null);
const sandboxVisible = ref(false);
const sandboxOffscreen = ref(false);
const baseLayer = ref('osm');
const showOctagon = ref(true);
const showLidar = ref(true);
const tileCache = ref({});
const currentTileData = ref(null);
// Map instance
let map = null;
// Tile names to load
const TILE_NAMES = [
'BS19820747',
'BS19820748',
'BS19830747',
'BS19830748'
];
// Newark Octagon coordinates
const octagonCoords = [
[-82.44133, 40.05431],
[-82.44264, 40.05311],
[-82.44464, 40.05242],
[-82.44631, 40.05342],
[-82.44728, 40.05500],
[-82.44589, 40.05633],
[-82.44389, 40.05697],
[-82.44192, 40.05589],
[-82.44133, 40.05431], // Close the polygon
];
const octagonCenter = [-82.44383, 40.05469];
// Coordinate conversion utilities
function webMercatorToLonLat(x, y) {
const R = 6378137; // Earth's radius in meters
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];
}
// Parse .mound binary file
async function parseMoundFile(url) {
const response = await fetch(url);
const buffer = await response.arrayBuffer();
const view = new DataView(buffer);
let offset = 0;
const magic = String.fromCharCode(
view.getUint8(offset++),
view.getUint8(offset++),
view.getUint8(offset++),
view.getUint8(offset++)
);
if (magic !== 'LIDR') {
throw new Error('Invalid .mound file');
}
const version = view.getUint32(offset, true); offset += 4;
const pointCount = view.getUint32(offset, true); offset += 4;
const triangleCount = view.getUint32(offset, true); offset += 4;
const minX = view.getFloat32(offset, true); offset += 4;
const minY = view.getFloat32(offset, true); offset += 4;
const minZ = view.getFloat32(offset, true); offset += 4;
const maxX = view.getFloat32(offset, true); offset += 4;
const maxY = view.getFloat32(offset, true); offset += 4;
const maxZ = view.getFloat32(offset, true); offset += 4;
offset += 24; // Skip reserved bytes
const positions = new Float32Array(buffer, offset, pointCount * 3);
offset += pointCount * 3 * 4;
const indices = new Uint32Array(buffer, offset, triangleCount * 3);
return {
version,
pointCount,
triangleCount,
bounds: { minX, minY, minZ, maxX, maxY, maxZ },
positions,
indices
};
}
// Load and render all lidar tiles
async function loadLidarTiles() {
console.log('Loading and rendering tiles...');
for (const tileName of TILE_NAMES) {
try {
console.log(`Loading ${tileName}...`);
const data = await parseMoundFile(`/tiles/${tileName}.mound`);
// Cache tile data
tileCache.value[tileName] = data;
// Render using ShadingSandbox
if (sandboxRef.value) {
const result = await sandboxRef.value.renderTileWithSettings(
data,
DEFAULT_RENDER_SETTINGS,
512
);
if (result.success) {
console.log(`Rendered ${tileName} in ${result.renderTime}ms (${result.width}x${result.height})`);
// Convert bounds to lat/lon
const sw = webMercatorToLonLat(data.bounds.minX, data.bounds.minY);
const ne = webMercatorToLonLat(data.bounds.maxX, data.bounds.maxY);
// Add image source to map
map.addSource(`tile-${tileName}`, {
type: 'image',
url: result.dataURL,
coordinates: [
[sw[0], ne[1]], // top-left
[ne[0], ne[1]], // top-right
[ne[0], sw[1]], // bottom-right
[sw[0], sw[1]] // bottom-left
]
});
// Add layer
map.addLayer({
id: `tile-layer-${tileName}`,
type: 'raster',
source: `tile-${tileName}`,
paint: {
'raster-opacity': 0.8
}
});
console.log(`Added ${tileName} to map at [${sw[0].toFixed(5)}, ${sw[1].toFixed(5)}] - [${ne[0].toFixed(5)}, ${ne[1].toFixed(5)}]`);
} else {
console.error(`Failed to render ${tileName}:`, result.error);
}
}
} catch (err) {
console.error(`Failed to load ${tileName}:`, err);
}
}
console.log('All tiles loaded');
}
// UI handlers
function updateBaseLayer() {
if (!map) return;
if (baseLayer.value === 'osm') {
map.setLayoutProperty('osm-layer', 'visibility', 'visible');
map.setLayoutProperty('satellite-layer', 'visibility', 'none');
} else {
map.setLayoutProperty('osm-layer', 'visibility', 'none');
map.setLayoutProperty('satellite-layer', 'visibility', 'visible');
}
}
function toggleOctagon() {
if (!map) return;
const visibility = showOctagon.value ? 'visible' : 'none';
map.setLayoutProperty('octagon-fill', 'visibility', visibility);
map.setLayoutProperty('octagon-outline', 'visibility', visibility);
}
function toggleLidar() {
if (!map) return;
const visibility = showLidar.value ? 'visible' : 'none';
for (const tileName of TILE_NAMES) {
const layerId = `tile-layer-${tileName}`;
if (map.getLayer(layerId)) {
map.setLayoutProperty(layerId, 'visibility', visibility);
}
}
}
function openSandbox() {
const firstTile = Object.keys(tileCache.value)[0];
if (firstTile) {
currentTileData.value = tileCache.value[firstTile];
sandboxVisible.value = true;
// Load tile data after sandbox mounts
setTimeout(() => {
if (sandboxRef.value) {
sandboxRef.value.loadTileData(currentTileData.value);
}
}, 100);
}
}
function onRenderComplete(data) {
console.log('Render complete:', {
size: `${data.width}x${data.height}`,
renderTime: data.renderTime,
settings: data.settings
});
}
function onRenderError(err) {
console.error('Renderer error:', err);
}
// Lifecycle
onMounted(() => {
map = new maplibregl.Map({
container: mapContainer.value,
style: {
version: 8,
sources: {
'osm': {
type: 'raster',
tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
tileSize: 256,
attribution: '© OpenStreetMap contributors'
},
'satellite': {
type: 'raster',
tiles: [
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'
],
tileSize: 256,
attribution: 'Esri, Maxar, Earthstar Geographics'
}
},
layers: [
{
id: 'osm-layer',
type: 'raster',
source: 'osm',
layout: { visibility: 'visible' }
},
{
id: 'satellite-layer',
type: 'raster',
source: 'satellite',
layout: { visibility: 'none' }
}
]
},
center: octagonCenter,
zoom: 15
});
map.on('load', async () => {
// Add octagon overlay
map.addSource('octagon', {
type: 'geojson',
data: {
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [octagonCoords]
}
}
});
map.addLayer({
id: 'octagon-fill',
type: 'fill',
source: 'octagon',
paint: {
'fill-color': '#ff0000',
'fill-opacity': 0.2
}
});
map.addLayer({
id: 'octagon-outline',
type: 'line',
source: 'octagon',
paint: {
'line-color': '#ff0000',
'line-width': 2
}
});
// Initialize sandbox offscreen for tile rendering
sandboxOffscreen.value = true;
sandboxVisible.value = true;
await new Promise(resolve => setTimeout(resolve, 100));
// Load and render tiles
await loadLidarTiles();
// Hide sandbox
sandboxVisible.value = false;
sandboxOffscreen.value = false;
});
});
onUnmounted(() => {
if (map) {
map.remove();
map = null;
}
});
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
#app {
width: 100vw;
height: 100vh;
position: relative;
}
.map-container {
width: 100%;
height: 100%;
}
.layer-controls {
position: absolute;
bottom: 20px;
left: 20px;
background: white;
padding: 15px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
font-family: Arial, sans-serif;
font-size: 14px;
}
.control-section {
margin-bottom: 10px;
}
.control-section:last-child {
margin-bottom: 0;
}
.control-section label {
display: block;
margin: 5px 0;
cursor: pointer;
}
.control-section label:first-child {
font-weight: bold;
margin-bottom: 8px;
cursor: default;
}
.control-section input[type="radio"],
.control-section input[type="checkbox"] {
margin-right: 8px;
cursor: pointer;
}
.sandbox-btn {
width: 100%;
padding: 10px;
margin-top: 10px;
background: #4A9EFF;
color: white;
border: none;
border-radius: 4px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.sandbox-btn:hover {
background: #2E8FE3;
}
.sandbox-btn:active {
transform: scale(0.98);
}
</style>