import * as THREE from "https://unpkg.com/three@0.168.0/build/three.module.js"; import { GeoApiClient } from "../libs/geo-api-client/dist/index.js"; class BrowserStorage { getItem(key) { return localStorage.getItem(key); } setItem(key, value) { localStorage.setItem(key, value); } removeItem(key) { localStorage.removeItem(key); } } const statusEl = document.getElementById("status"); const apiBaseEl = document.getElementById("apiBase"); const publicKeyPreviewEl = document.getElementById("publicKeyPreview"); const collectionInfoEl = document.getElementById("collectionInfo"); const collectionNameEl = document.getElementById("collectionName"); const assetFileEl = document.getElementById("assetFile"); const assetNameEl = document.getElementById("assetName"); const assetDescEl = document.getElementById("assetDesc"); const assetsListEl = document.getElementById("assetsList"); let client = new GeoApiClient(apiBaseEl.value.trim(), new BrowserStorage()); let keys = null; let accessToken = ""; let collectionId = ""; let selectedLngLat = null; let pendingMarker = null; let map; let threeLayer; let threeScene; let threeRenderer; let threeCamera; const featureMeshes = new Map(); function setStatus(message) { statusEl.textContent = message; } function extFromFilename(name) { const idx = name.lastIndexOf("."); if (idx <= 0) return ""; return name.slice(idx + 1).toLowerCase(); } function kindFromExt(ext) { return ext === "gltf" || ext === "glb" ? "3d" : "image"; } async function sha256Hex(file) { const buffer = await file.arrayBuffer(); const digest = await crypto.subtle.digest("SHA-256", buffer); return Array.from(new Uint8Array(digest)) .map((b) => b.toString(16).padStart(2, "0")) .join(""); } function setClientBase(baseUrl) { const normalized = baseUrl.trim().replace(/\/+$/g, ""); client = new GeoApiClient(normalized, new BrowserStorage()); if (accessToken) client.setAccessToken(accessToken); localStorage.setItem("geo_api_base", normalized); setStatus(`API base updated: ${normalized}`); } function createThreeLayer() { return { id: "threejs-custom-layer", type: "custom", renderingMode: "3d", onAdd(m, gl) { threeCamera = new THREE.Camera(); threeScene = new THREE.Scene(); const ambient = new THREE.AmbientLight(0xffffff, 0.8); const directional = new THREE.DirectionalLight(0xffffff, 0.7); directional.position.set(0, -70, 100).normalize(); threeScene.add(ambient); threeScene.add(directional); threeRenderer = new THREE.WebGLRenderer({ canvas: m.getCanvas(), context: gl, antialias: true, }); threeRenderer.autoClear = false; }, render(gl, matrix) { const m = new THREE.Matrix4().fromArray(matrix); threeCamera.projectionMatrix = m; threeRenderer.resetState(); threeRenderer.render(threeScene, threeCamera); map.triggerRepaint(); gl.disable(gl.DEPTH_TEST); }, }; } function disposeMesh(mesh) { if (!mesh) return; if (mesh.geometry) mesh.geometry.dispose(); if (mesh.material) { if (Array.isArray(mesh.material)) { for (const mat of mesh.material) mat.dispose(); } else { mesh.material.dispose(); } } } function clearFeatureMeshes() { for (const mesh of featureMeshes.values()) { threeScene.remove(mesh); disposeMesh(mesh); } featureMeshes.clear(); } function addObjectMesh(featureId, lng, lat, isPublic, kind) { const merc = maplibregl.MercatorCoordinate.fromLngLat({ lng, lat }, 0); const meters = merc.meterInMercatorCoordinateUnits(); const is3D = kind === "3d"; const geometry = is3D ? new THREE.BoxGeometry(1.2, 1.2, 2.2) : new THREE.PlaneGeometry(1.8, 1.8); const color = isPublic ? 0x44dd88 : 0xdd5566; const material = new THREE.MeshStandardMaterial({ color, transparent: true, opacity: 0.92 }); const mesh = new THREE.Mesh(geometry, material); mesh.position.set(merc.x, merc.y, merc.z); mesh.scale.setScalar(meters * (is3D ? 18 : 24)); if (!is3D) { mesh.rotation.x = Math.PI / 2; } threeScene.add(mesh); featureMeshes.set(featureId, mesh); } async function ensureKeys() { keys = await client.ensureKeysInStorage(); publicKeyPreviewEl.textContent = `Public key: ${keys.publicKey.slice(0, 24)}...`; } async function register() { if (!keys) await ensureKeys(); await client.registerBySigningServiceKey(keys.publicKey, keys.privateKey); } async function login() { if (!keys) await ensureKeys(); accessToken = await client.loginWithSignature(keys.publicKey, keys.privateKey); client.setAccessToken(accessToken); } async function ensureCollection() { if (collectionId) return collectionId; const created = await client.createCollection(collectionNameEl.value.trim() || "MapLibre 3D objects demo"); collectionId = created.id; collectionInfoEl.textContent = `${created.name} (${created.id})`; return collectionId; } function renderAssets(features) { assetsListEl.innerHTML = ""; for (const feature of features) { const assets = Array.isArray(feature.properties?.assets) ? feature.properties.assets : []; for (const asset of assets) { const absoluteLink = client.resolveRelativeLink(asset.link); const card = document.createElement("div"); card.className = "asset-card"; card.innerHTML = `