440 lines
10 KiB
Vue
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> |