Public
Edited
Jan 27
1 fork
Importers
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viewof mapboxMap = {
const container = html`<div style="height:500px; width: 800px">`;
yield container;

const floormaterial = new THREE.MeshPhongMaterial({
color: 0xffff00,
opacity: 1,
// wireframe: true,
side: THREE.DoubleSide
});

const shadowmaterial = new THREE.ShadowMaterial({
side: THREE.DoubleSide
});
shadowmaterial.opacity = 1;

const meshCache = {};

viewof elevationOffset.addEventListener("input", () => {
tb.world.children.forEach((model) => {
if (model.userData.name !== "terrain") return;
const newOffset = viewof elevationOffset.value;
const slippyTile = model.userData.slippyTile;
const coords = meshCache[slippyTile].tileStartingCoord;
const newElevation =
coords[2] - meshCache[slippyTile].originalElevationOffset + newOffset;
model.setCoords([coords[0], coords[1], newElevation]);
});
tb.update();
});

const map = new mapboxgl.Map({
container: container,
style: "mapbox://styles/mapbox/satellite-v9",
zoom: 18,
center: [148.9819, -35.39847],
pitch: 60,
transformRequest: (url, resourceType) => {
if (url.includes("mapbox.mapbox-terrain-dem-v1/")) {
// it's a request to the terrain source!
// parse the slippy map tile from the request
const slippyTile = url.split("mapbox-terrain-dem-v1/")[1].split(".")[0];
// check if the tile is already in the cache
if (meshCache[slippyTile]) return;
// a maxMeshError of -1 will be interpreted as "auto"
// -> "auto" is defined as the height of 1px in the terrain raster in meters (pixelInMeter),
// subtracted by the pixelInMeter of the highest zoom (14)
const curMaxMeshError = maxMeshError === -1 ? undefined : maxMeshError;

createMeshForSlippyTile({
slippyTile,
maxMeshError: curMaxMeshError,
elevationOffset: viewof elevationOffset.value,
terrainExaggeration: terrainExaggeration,
mapboxAccessToken: mapboxgl.accessToken
}).then((res) => {
const { geometry, tileStartingCoord } = res;

meshCache[slippyTile] = {
slippyTile,
tileStartingCoord,
originalElevationOffset: viewof elevationOffset.value
};

const selectedMaterial =
shadowMeshMaterial === "Transparent"
? shadowmaterial
: floormaterial;
let threeMesh = new THREE.Mesh(geometry, selectedMaterial);

threeMesh = tb.Object3D({
obj: threeMesh,
units: "meters",
name: "terrain",
slippyTile: slippyTile
});

threeMesh.setRotation({ y: 0, x: 0, z: 90 });
threeMesh.receiveShadow = true;
threeMesh.castShadow = false;
threeMesh.setCoords(tileStartingCoord);

tb.add(threeMesh);
tb.update();
});
}
},
antialias: true
});
map.addControl(new mapboxgl.NavigationControl());
// wait for the map to idle

const tb = (window.tb = new Threebox.Threebox(
map,
map.getCanvas().getContext("webgl"),
{
realSunlight: true,
sky: true,
terrain: true
//enableSelectingObjects: true,
//enableTooltips: true
}
));

let model;
// parameters to ensure the model is georeferenced correctly on the map
const modelOrigin = [148.9819, -35.39847, 659];

let date = new Date("04-27-2023");
let time =
date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds();

container.value = date;

viewof timeSlider.addEventListener("input", () => {
time = +viewof timeSlider.value;
date.setHours(Math.floor(time / 60 / 60));
date.setMinutes(Math.floor(time / 60) % 60);
date.setSeconds(time % 60);
container.value = date;
container.dispatchEvent(new Event("input", { bubbles: true }));
map.triggerRepaint();
});

async function createCustomLayer(layerName, origin) {
// create model url
// see https://observablehq.com/@vicapow/example-of-loading-and-modifying-a-glb-file
const url = await FileAttachment("34M_17.glb").url();

let model;
//create the layer
let customLayer3D = {
id: layerName,
type: "custom",
renderingMode: "3d",
onAdd: function (map, gl) {
// Attribution, no License specified: Model by https://github.com/nasa/
// https://nasa3d.arc.nasa.gov/detail/jpl-vtad-dsn34
let options = {
type: "gltf",
// IMPORTANT - DO NOT REMOVE
// the mtl option MUST be present, it just needs a URL that returns a 200 status code
// if you uncommented / delete the mtl option the actual object won't load
// Thanks @mootari for debugging this
mtl: "https://cdn.jsdelivr.net/npm/threebox-plugin@2.2.7/examples/models/radar/",
obj: url,
units: "meters",
scale: 333.22,
rotation: { x: 90, y: 180, z: 0 },
anchor: "center"
};

tb.loadObj(options, function (model) {
model.setCoords(origin);
model.addTooltip("A radar in the middle of nowhere", true);
tb.add(model);
model.castShadow = true;
tb.lights.dirLight.target = model;
});
},
render: function (gl, matrix) {
tb.setSunlight(date, origin); //set Sun light for the given datetime and lnglat
tb.update();
}
};
return customLayer3D;
}

map.on("load", async function () {
const customLayer = await createCustomLayer("3d-model", modelOrigin);
map.addLayer(customLayer);
});

invalidation.then(() => map.remove());
}
Insert cell
Insert cell
Insert cell
async function createMeshForSlippyTile(options) {
let {
slippyTile,
maxMeshError,
terrainExaggeration = 1,
elevationOffset = 0,
mapboxAccessToken
} = options;

const tileUrl = `https://a.tiles.mapbox.com/v4/mapbox.terrain-rgb/${slippyTile}.png?access_token=${mapboxAccessToken}`;

const tileImageBuffer = await fetch(tileUrl).then((res) => res.arrayBuffer());
const parsedTileImage = await parseImage(tileImageBuffer);
const tileCoords = slippyTile.split("/").map(Number);
const tileSize = parsedTileImage.width;
const metersPerPixel = calcMetersPerPixel(tileCoords, tileSize);
// we use 1px as a default error
if (maxMeshError == null) {
// transform default max. mesh error so that it is 0 on zoom 14
maxMeshError = metersPerPixel - 7.788435922393799;
}
const terrain = mapboxTerrainToGrid(parsedTileImage);
const martini = new Martini(parsedTileImage.width + 1);
const tile = martini.createTile(terrain);
const mesh = tile.getMesh(maxMeshError);
const geometry = new THREE.BufferGeometry();

const gridSize = tileSize + 1;
const vertices = new Float32Array((mesh.vertices.length / 2) * 3);

let index = 0;
for (let i = 0; i < mesh.vertices.length / 2; i++) {
let x = mesh.vertices[i * 2],
y = mesh.vertices[i * 2 + 1];
vertices[index++] = y * metersPerPixel;
vertices[index++] = x * metersPerPixel;
vertices[index++] = terrain[y * gridSize + x] * terrainExaggeration;
}

geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
geometry.setIndex(new THREE.BufferAttribute(mesh.triangles, 1));
geometry.computeVertexNormals();
geometry.computeBoundingBox();
geometry.normalizeNormals();

const [z, x, y] = tileCoords;
const tileGeoJSON = tilebelt.tileToGeoJSON([x, y, z]);
const tileStartingCoord = tileGeoJSON.coordinates[0][2];
const elevation = d3.min(terrain) + elevationOffset;

return {
geometry,
tileStartingCoord: [...tileStartingCoord, elevation]
};
}
Insert cell
function calcMetersPerPixel(tileCoord, width) {
const [z, x, y] = tileCoord;
const numTiles = Math.pow(2, z);
const lat =
2 * Math.atan(Math.exp((1 - (2 * (y + 0.5)) / numTiles) * Math.PI)) -
Math.PI / 2;
return (40075016.7 * Math.cos(lat)) / numTiles / width;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more