395 lines
11 KiB
Vue
395 lines
11 KiB
Vue
<template>
|
|
<div id="app">
|
|
<div ref="mapContainer" class="map-container"></div>
|
|
<canvas ref="threeCanvas" class="three-canvas"></canvas>
|
|
|
|
<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>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { ref, onMounted, onUnmounted } from 'vue';
|
|
import maplibregl from 'maplibre-gl';
|
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
|
import * as THREE from 'three';
|
|
|
|
export default {
|
|
name: 'App',
|
|
setup() {
|
|
const mapContainer = ref(null);
|
|
const threeCanvas = ref(null);
|
|
let map = null;
|
|
let scene = null;
|
|
let camera = null;
|
|
let renderer = null;
|
|
let lidarMeshes = [];
|
|
const baseLayer = ref('osm');
|
|
const showOctagon = ref(true);
|
|
const showLidar = ref(true);
|
|
|
|
// Newark Octagon coordinates (converted from DMS to decimal)
|
|
const octagonCoords = [
|
|
[-82.44133, 40.05431], // 40°03'15.5"N 82°26'28.8"W
|
|
[-82.44264, 40.05311], // 40°03'11.2"N 82°26'33.5"W
|
|
[-82.44464, 40.05242], // 40°03'08.7"N 82°26'40.7"W
|
|
[-82.44631, 40.05342], // 40°03'12.3"N 82°26'46.7"W
|
|
[-82.44728, 40.05500], // 40°03'18.0"N 82°26'50.2"W
|
|
[-82.44589, 40.05633], // 40°03'22.8"N 82°26'45.2"W
|
|
[-82.44389, 40.05697], // 40°03'25.1"N 82°26'38.0"W
|
|
[-82.44192, 40.05589], // 40°03'21.2"N 82°26'30.9"W
|
|
[-82.44133, 40.05431], // Close the polygon
|
|
];
|
|
|
|
const octagonCenter = [-82.44383, 40.05469];
|
|
|
|
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, USDA FSA, USGS, Aerogrid, IGN, IGP, and the GIS User Community'
|
|
}
|
|
},
|
|
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', () => {
|
|
// Add octagon source and layer
|
|
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 Three.js
|
|
initThreeJS();
|
|
loadLidarTiles();
|
|
|
|
// Update Three.js on map move
|
|
map.on('move', updateThreeCamera);
|
|
map.on('zoom', updateThreeCamera);
|
|
});
|
|
});
|
|
|
|
const 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');
|
|
}
|
|
};
|
|
|
|
const toggleOctagon = () => {
|
|
if (!map) return;
|
|
|
|
const visibility = showOctagon.value ? 'visible' : 'none';
|
|
map.setLayoutProperty('octagon-fill', 'visibility', visibility);
|
|
map.setLayoutProperty('octagon-outline', 'visibility', visibility);
|
|
};
|
|
|
|
const toggleLidar = () => {
|
|
lidarMeshes.forEach(mesh => {
|
|
mesh.visible = showLidar.value;
|
|
});
|
|
if (renderer) renderer.render(scene, camera);
|
|
};
|
|
|
|
const initThreeJS = () => {
|
|
scene = new THREE.Scene();
|
|
|
|
camera = new THREE.OrthographicCamera(
|
|
-1, 1, 1, -1, 0.1, 10000
|
|
);
|
|
camera.position.z = 1;
|
|
|
|
renderer = new THREE.WebGLRenderer({
|
|
canvas: threeCanvas.value,
|
|
alpha: true,
|
|
antialias: true
|
|
});
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.setClearColor(0x000000, 0);
|
|
|
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
|
|
scene.add(ambientLight);
|
|
|
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
|
directionalLight.position.set(1, 1, 1);
|
|
scene.add(directionalLight);
|
|
|
|
window.addEventListener('resize', handleResize);
|
|
|
|
updateThreeCamera();
|
|
};
|
|
|
|
const handleResize = () => {
|
|
if (!renderer || !camera) return;
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
updateThreeCamera();
|
|
};
|
|
|
|
const updateThreeCamera = () => {
|
|
if (!map || !camera || !renderer) return;
|
|
|
|
const bounds = map.getBounds();
|
|
const ne = bounds.getNorthEast();
|
|
const sw = bounds.getSouthWest();
|
|
|
|
camera.left = sw.lng;
|
|
camera.right = ne.lng;
|
|
camera.top = ne.lat;
|
|
camera.bottom = sw.lat;
|
|
camera.updateProjectionMatrix();
|
|
|
|
renderer.render(scene, camera);
|
|
};
|
|
|
|
const parseMoundFile = async (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
|
|
|
|
const positions = new Float32Array(buffer, offset, pointCount * 3);
|
|
offset += pointCount * 3 * 4;
|
|
|
|
const indices = new Uint32Array(buffer, offset, triangleCount * 3);
|
|
|
|
return {
|
|
pointCount,
|
|
triangleCount,
|
|
bounds: { minX, minY, minZ, maxX, maxY, maxZ },
|
|
positions,
|
|
indices
|
|
};
|
|
};
|
|
|
|
const loadLidarTiles = async () => {
|
|
const tiles = [
|
|
'BS19820747',
|
|
'BS19820748',
|
|
'BS19830747',
|
|
'BS19830748'
|
|
];
|
|
|
|
for (const tileName of tiles) {
|
|
try {
|
|
console.log(`Loading ${tileName}...`);
|
|
const data = await parseMoundFile(`/tiles/${tileName}.mound`);
|
|
|
|
console.log(`${tileName} bounds:`, data.bounds);
|
|
console.log(`First few positions:`, data.positions.slice(0, 15));
|
|
|
|
const geometry = new THREE.BufferGeometry();
|
|
geometry.setAttribute('position', new THREE.BufferAttribute(data.positions, 3));
|
|
geometry.setIndex(new THREE.BufferAttribute(data.indices, 1));
|
|
geometry.computeVertexNormals();
|
|
|
|
const material = new THREE.MeshLambertMaterial({
|
|
color: 0x8B7355,
|
|
side: THREE.DoubleSide,
|
|
wireframe: false
|
|
});
|
|
|
|
const mesh = new THREE.Mesh(geometry, material);
|
|
scene.add(mesh);
|
|
lidarMeshes.push(mesh);
|
|
|
|
console.log(`Loaded ${tileName}: ${data.pointCount} points, ${data.triangleCount} triangles`);
|
|
console.log(`Mesh position:`, mesh.position);
|
|
console.log(`Mesh in scene:`, scene.children.length);
|
|
} catch (err) {
|
|
console.error(`Failed to load ${tileName}:`, err);
|
|
}
|
|
}
|
|
|
|
console.log('Map bounds:', map.getBounds().toArray());
|
|
console.log('Camera:', camera);
|
|
|
|
updateThreeCamera();
|
|
};
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('resize', handleResize);
|
|
if (renderer) {
|
|
renderer.dispose();
|
|
}
|
|
});
|
|
|
|
return {
|
|
mapContainer,
|
|
threeCanvas,
|
|
baseLayer,
|
|
showOctagon,
|
|
showLidar,
|
|
updateBaseLayer,
|
|
toggleOctagon,
|
|
toggleLidar
|
|
};
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
#app {
|
|
width: 100vw;
|
|
height: 100vh;
|
|
position: relative;
|
|
}
|
|
|
|
.map-container {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.three-canvas {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
</style> |