3D Building with Shadow
Shadow Length: 1.0x
Buildings: 0
Status: Loading...
html
<!DOCTYPE html>
<html>
<head>
<title>3D Buildings with Shadows</title>
<meta charset="utf-8" />
<meta
name="viewport"
content="initial-scale=1,maximum-scale=1,user-scalable=no"
/>
<link
href="https://cdn.mapmetrics-atlas.net/versions/latest/mapmetrics-gl.css"
rel="stylesheet"
/>
<script src="https://cdn.mapmetrics-atlas.net/versions/latest/mapmetrics-gl.js"></script>
<style>
body {
margin: 0;
padding: 0;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
.controls {
position: absolute;
top: 10px;
right: 10px;
background: white;
padding: 15px;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
z-index: 1000;
max-width: 300px;
}
.control-group {
margin: 10px 0;
padding: 10px 0;
border-bottom: 1px solid #eee;
}
h3 {
margin: 0 0 10px 0;
}
label {
display: block;
margin: 5px 0;
font-size: 12px;
}
input[type="range"] {
width: 100%;
}
button {
padding: 8px 12px;
margin: 2px;
border: 1px solid #ccc;
border-radius: 3px;
background: #f9f9f9;
cursor: pointer;
}
button:hover {
background: #e9e9e9;
}
.info {
position: absolute;
bottom: 10px;
left: 10px;
background: white;
padding: 10px;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
font-family: monospace;
font-size: 11px;
}
.shadow-style-btns {
display: flex;
gap: 5px;
margin-top: 10px;
}
.shadow-style-btns button {
flex: 1;
font-size: 11px;
}
</style>
</head>
<body>
<div id="map"></div>
<div class="controls">
<h3>3D Buildings with Shadows</h3>
<div class="control-group">
<label>
<input type="checkbox" id="shadowToggle" checked /> Enable Shadows
</label>
</div>
<div class="control-group">
<label>Sun Azimuth: <span id="azimuthValue">180</span>°</label>
<input
type="range"
id="azimuthSlider"
min="0"
max="360"
value="180"
step="1"
/>
<label>Sun Altitude: <span id="altitudeValue">45</span>°</label>
<input
type="range"
id="altitudeSlider"
min="5"
max="85"
value="45"
step="1"
/>
</div>
<div class="control-group">
<label>Shadow Intensity: <span id="intensityValue">50</span>%</label>
<input
type="range"
id="intensitySlider"
min="0"
max="100"
value="50"
step="5"
/>
</div>
<div class="shadow-style-btns">
<button onclick="setSunPosition(90, 15)">Morning</button>
<button onclick="setSunPosition(180, 70)">Noon</button>
<button onclick="setSunPosition(270, 15)">Evening</button>
</div>
</div>
<div class="info">
<div>Shadow Length: <span id="shadowLength">1.0</span>x</div>
<div>Buildings: <span id="buildingCount">0</span></div>
<div>Status: <span id="status">Loading...</span></div>
</div>
<script>
const token = `YOUR_TOKEN`;
// Initialize map with the MapMetrics Atlas style
const map = new mapmetricsgl.Map({
container: "map",
style: `https://gateway.mapmetrics-atlas.net/styles/?fileName=${token}`,
center: [5.47, 51.49], // Center of your building data
zoom: 16,
pitch: 60,
bearing: -30,
});
let shadowsEnabled = true;
let currentAzimuth = 180;
let currentAltitude = 45;
let currentIntensity = 50; // Shadow intensity as percentage (0=white, 100=black)
map.on("load", () => {
console.log("Map loaded");
document.getElementById("status").textContent = "Map loaded";
// Add the 3D buildings source
map.addSource("buildings", {
type: "vector",
tiles: [
"https://building.mapmetrics-atlas.net/data/buildings/{z}/{x}/{y}.pbf",
],
minzoom: 13,
maxzoom: 16,
});
// Add 3D buildings layer
map.addLayer({
id: "buildings-3d",
type: "fill-extrusion",
source: "buildings",
"source-layer": "building",
paint: {
"fill-extrusion-color": "#aaa",
"fill-extrusion-height": [
"case",
["has", "height"],
["get", "height"],
["has", "estimated_height"],
["get", "estimated_height"],
["has", "building:levels"],
["*", ["to-number", ["get", "building:levels"]], 3],
["has", "floors"],
["*", ["to-number", ["get", "floors"]], 3],
10,
],
"fill-extrusion-base": 0,
"fill-extrusion-opacity": 0.9,
},
});
// Add shadow layers
addShadowLayers();
document.getElementById("status").textContent =
"Buildings & Shadows Ready";
// Initial shadow update
updateShadows();
updateBuildingCount();
});
function addShadowLayers() {
// Create a GeoJSON source for shadow polygons
map.addSource("shadow-polygons", {
type: "geojson",
data: {
type: "FeatureCollection",
features: [],
},
});
// Add shadow polygon layer - control intensity via color, not opacity
map.addLayer(
{
id: "shadow-polygons",
type: "fill",
source: "shadow-polygons",
paint: {
"fill-color": "#808080", // Grey color (will be adjustable)
"fill-opacity": 0.7, // Fixed opacity
},
},
"buildings-3d"
); // Place shadows before buildings
}
function generateShadowPolygons() {
const features = [];
try {
const buildings = map.queryRenderedFeatures({
layers: ["buildings-3d"],
});
if (!buildings || buildings.length === 0) return features;
const azimuthRad = (currentAzimuth * Math.PI) / 180;
const altitudeRad = Math.max(0.01, (currentAltitude * Math.PI) / 180);
// Calculate physically accurate shadow length
let shadowLength = 1 / Math.tan(altitudeRad);
shadowLength = Math.min(10, shadowLength); // Cap at 10x height
if (currentAltitude < 5) {
shadowLength *= 0.5; // Reduce for very low sun
}
// Get current map center for accurate world coordinates
const center = map.getCenter();
const currentLat = center.lat;
// Calculate shadow offset in world coordinates (degrees)
const metersPerDegLat = 111320;
const metersPerDegLng =
111320 * Math.cos((currentLat * Math.PI) / 180);
// Convert shadow direction to degrees
const shadowDirLat =
(Math.cos(azimuthRad) * shadowLength) / metersPerDegLat;
const shadowDirLng =
(-Math.sin(azimuthRad) * shadowLength) / metersPerDegLng;
buildings.forEach((building) => {
if (!building.geometry || building.geometry.type !== "Polygon")
return;
const props = building.properties || {};
const height =
props.height ||
props.estimated_height ||
(props["building:levels"]
? parseFloat(props["building:levels"]) * 3
: 10) ||
(props.floors ? parseFloat(props.floors) * 3 : 10);
const footprint = building.geometry.coordinates[0];
if (!footprint || footprint.length < 3) return;
// Create shadow polygon (projected footprint) in world coordinates
const shadowFootprint = footprint.map((coord) => [
coord[0] + shadowDirLng * height,
coord[1] + shadowDirLat * height,
]);
// Create the main shadow polygon
features.push({
type: "Feature",
geometry: {
type: "Polygon",
coordinates: [shadowFootprint],
},
});
// Create side faces of the shadow volume
for (let i = 0; i < footprint.length - 1; i++) {
const nextI = (i + 1) % (footprint.length - 1);
features.push({
type: "Feature",
geometry: {
type: "Polygon",
coordinates: [
[
footprint[i],
footprint[nextI],
shadowFootprint[nextI],
shadowFootprint[i],
footprint[i],
],
],
},
});
}
});
} catch (error) {
console.error("Error generating shadow polygons:", error);
}
return {
type: "FeatureCollection",
features: features,
};
}
function updateShadows() {
// Update polygon shadows
if (map.getSource("shadow-polygons")) {
const shadowData = shadowsEnabled
? generateShadowPolygons()
: {
type: "FeatureCollection",
features: [],
};
map.getSource("shadow-polygons").setData(shadowData);
// Update visibility
map.setLayoutProperty(
"shadow-polygons",
"visibility",
shadowsEnabled ? "visible" : "none"
);
// Calculate grey color based on intensity (0=white, 100=black)
const greyValue = Math.round(255 - (currentIntensity / 100) * 255);
const hexGrey = greyValue.toString(16).padStart(2, "0");
const shadowColor = `#${hexGrey}${hexGrey}${hexGrey}`;
// Update shadow color, keeping opacity fixed at 0.7
map.setPaintProperty("shadow-polygons", "fill-color", shadowColor);
map.setPaintProperty("shadow-polygons", "fill-opacity", 0.7);
}
// Calculate and display shadow length
const altitudeRad = Math.max(0.1, (currentAltitude * Math.PI) / 180);
const shadowLength = Math.min(3, 1 / Math.tan(altitudeRad));
document.getElementById("shadowLength").textContent =
shadowLength.toFixed(2);
}
function setSunPosition(azimuth, altitude) {
currentAzimuth = azimuth;
currentAltitude = altitude;
document.getElementById("azimuthSlider").value = azimuth;
document.getElementById("altitudeSlider").value = altitude;
document.getElementById("azimuthValue").textContent = azimuth;
document.getElementById("altitudeValue").textContent = altitude;
updateShadows();
}
function updateBuildingCount() {
const features = map.queryRenderedFeatures({
layers: ["buildings-3d"],
});
document.getElementById("buildingCount").textContent = features.length;
}
// Event listeners
document
.getElementById("shadowToggle")
.addEventListener("change", (e) => {
shadowsEnabled = e.target.checked;
updateShadows();
});
document
.getElementById("azimuthSlider")
.addEventListener("input", (e) => {
currentAzimuth = parseFloat(e.target.value);
document.getElementById("azimuthValue").textContent = currentAzimuth;
updateShadows();
});
document
.getElementById("altitudeSlider")
.addEventListener("input", (e) => {
currentAltitude = parseFloat(e.target.value);
document.getElementById("altitudeValue").textContent =
currentAltitude;
updateShadows();
});
document
.getElementById("intensitySlider")
.addEventListener("input", (e) => {
currentIntensity = parseInt(e.target.value);
document.getElementById("intensityValue").textContent =
currentIntensity;
updateShadows();
});
// Update on map movement
map.on("moveend", () => {
updateShadows();
updateBuildingCount();
});
// Add navigation controls
map.addControl(new mapmetricsgl.NavigationControl());
// Add popup on click for building info
map.on("click", "buildings-3d", (e) => {
const coordinates = e.lngLat;
const properties = e.features[0].properties;
const actualHeight =
properties.height ||
properties.estimated_height ||
(properties["building:levels"]
? parseInt(properties["building:levels"]) * 3
: null) ||
(properties.floors ? parseInt(properties.floors) * 3 : null) ||
10;
const popupContent = `
<strong>Building Info:</strong><br>
Height: ${actualHeight}m<br>
Levels: ${properties["building:levels"] || "N/A"}<br>
Type: ${
properties["building"] || properties["building:type"] || "N/A"
}<br>
Name: ${properties.name || "N/A"}
`;
new mapmetricsgl.Popup()
.setLngLat(coordinates) // place popup at clicked point
.setHTML(popupContent) // set the content
.addTo(map); // add popup to the map
});
// Change cursor on hover
map.on("mouseenter", "buildings-3d", () => {
map.getCanvas().style.cursor = "pointer";
});
map.on("mouseleave", "buildings-3d", () => {
map.getCanvas().style.cursor = "";
});
</script>
</body>
</html>