Public
Edited
Apr 4, 2023
Insert cell
Insert cell
Insert cell
viewof map_3d = {
const container = html`<div style="height:500px;" />`;
yield container;

// Mapbox bearing to THREE.JS radians
// formula (for rotation):
// - 2 * deg / 360 * Math.PI

function degreeToRadian(deg) {
return ((-2 * deg) / 360) * Math.PI;
}

const modelLocation = {
center: [8.353338, 52.090696],
zoom: 18,
pitch: 80,
bearing: 270,
scalingFactor: 25
};

const map = new mapboxgl.Map({
container: container,
style: "mapbox://styles/toffi/clg29mx6b001t01mpfx95t5sq",
zoom: modelLocation.zoom,
center: modelLocation.center,
pitch: modelLocation.pitch,
bearing: modelLocation.bearing,
preserveDrawingBuffer: true,
antialias: true
});

// parameters to ensure the model is georeferenced correctly on the map
const modelOrigin = modelLocation.center;
const modelAltitude = 195.5;
const modelRotate = [Math.PI / 2, degreeToRadian(modelLocation.bearing), 0];

const modelAsMercatorCoordinate = mapboxgl.MercatorCoordinate.fromLngLat(
modelOrigin,
modelAltitude
);

// transformation parameters to position, rotate and scale the 3D model onto the map
const modelTransform = {
translateX: modelAsMercatorCoordinate.x,
translateY: modelAsMercatorCoordinate.y,
translateZ: modelAsMercatorCoordinate.z,
rotateX: modelRotate[0],
rotateY: modelRotate[1],
rotateZ: modelRotate[2],
/* Since the 3D model is in real world meters, a scale transform needs to be
* applied since the CustomLayerInterface expects units in MercatorCoordinates.
*/
scale:
modelAsMercatorCoordinate.meterInMercatorCoordinateUnits() *
modelLocation.scalingFactor
};

map.on("move", () => {
// get current map bearing, convert -180 - 180 deg value
// to 0-360 deg value
const curBearing = (map.getBearing() + 360) % 360;
// update model y rotation accordingly
modelTransform.rotateY = degreeToRadian(curBearing);
// query current land use
const features = map.queryRenderedFeatures({
layers: ["landuse"],
filter: ["==", "class", "wood"]
});
console.log(features);
});

const spriteUrl = await FileAttachment("pngwing.com.png").url();

// configuration of the custom layer for a 3D model per the CustomLayerInterface
const customLayer = {
id: "3d-model",
type: "custom",
renderingMode: "3d",
onAdd: function (map, gl) {
this.camera = new THREE.Camera();
this.scene = new THREE.Scene();

const textureMap = new THREE.TextureLoader().load(spriteUrl);
const material = new THREE.SpriteMaterial({ map: textureMap });

const sprite = new THREE.Sprite(material);
this.scene.add(sprite);

this.map = map;

// use the Mapbox GL JS map canvas for three.js
this.renderer = new THREE.WebGLRenderer({
canvas: map.getCanvas(),
context: gl,
antialias: true
});

this.renderer.autoClear = false;
},
render: function (gl, matrix) {
const rotationX = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(1, 0, 0),
modelTransform.rotateX
);
const rotationY = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(0, 1, 0),
modelTransform.rotateY
);
const rotationZ = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(0, 0, 1),
modelTransform.rotateZ
);

const m = new THREE.Matrix4().fromArray(matrix);
const l = new THREE.Matrix4()
.makeTranslation(
modelTransform.translateX,
modelTransform.translateY,
modelTransform.translateZ
)
.scale(
new THREE.Vector3(
modelTransform.scale,
-modelTransform.scale,
modelTransform.scale
)
)
.multiply(rotationX)
.multiply(rotationY)
.multiply(rotationZ);

this.camera.projectionMatrix = m.multiply(l);
this.renderer.resetState();
this.renderer.render(this.scene, this.camera);
this.map.triggerRepaint();
}
};

map.on("style.load", () => {
map.addLayer(customLayer);
});
}
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