Extrude Polygons for 3D Indoor Mapping in Maptoolkit Maps JS
Indoor maps in 3D are built by storing room polygons as GeoJSON features with height and base-height properties, then rendering them with a fill-extrusion layer that reads those values. The result is a scaled architectural view of the interior that can include walls, rooms, and corridors at different heights. Use this approach for shopping mall directories, office building navigation, airport gate maps, or any interior space that benefits from a 3D overhead perspective.
const API_KEY = 'YOUR_API_KEY';
const floorplan = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: { type: 'Polygon', coordinates: [[[11.390, 47.275], [11.392, 47.275], [11.392, 47.277], [11.390, 47.277], [11.390, 47.275]]] },
properties: { level: 1, name: 'Lobby', color: '#aee', height: 4 }
},
{
type: 'Feature',
geometry: { type: 'Polygon', coordinates: [[[11.392, 47.275], [11.394, 47.275], [11.394, 47.277], [11.392, 47.277], [11.392, 47.275]]] },
properties: { level: 1, name: 'Hall A', color: '#fea', height: 6 }
},
{
type: 'Feature',
geometry: { type: 'Polygon', coordinates: [[[11.390, 47.277], [11.394, 47.277], [11.394, 47.279], [11.390, 47.279], [11.390, 47.277]]] },
properties: { level: 2, name: 'Hall B', color: '#eaf', height: 8 }
}
]
};
const map = new maptoolkit.Map({
container: 'map',
apiKey: API_KEY,
style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
center: [11.392, 47.277],
zoom: 16,
pitch: 50,
bearing: 20,
attributionControl: { compact: false }
});
map.addControl(new maptoolkit.NavigationControl(), 'top-right');
map.on('load', () => {
map.addSource('terrain-rgb', {
type: 'raster-dem',
tiles: [`https://tiles.maptoolkit.net/terrain/{z}/{x}/{y}.webp?api_key=${API_KEY}`],
tileSize: 256,
minzoom: 5,
maxzoom: 12,
encoding: 'terrarium'
});
map.setTerrain({ source: 'terrain-rgb', exaggeration: 1.5 });
// Force one render after the DEM source loads so terrain shows on initial load
map.once('idle', () => map.triggerRepaint());
map.addSource('floorplan', { type: 'geojson', data: floorplan });
map.addLayer({
id: 'rooms-extrusion',
type: 'fill-extrusion',
source: 'floorplan',
paint: {
'fill-extrusion-color': ['get', 'color'],
'fill-extrusion-height': ['get', 'height'],
'fill-extrusion-base': 0,
'fill-extrusion-opacity': 0.8
}
});
});<!DOCTYPE html>
<html lang="en">
<head>
<title>Extrude Polygons for 3D Indoor Mapping - Maptoolkit Maps JS</title>
<meta property="og:description" content="Use fill-extrusion layers to create a 3D indoor map from a GeoJSON floor plan." />
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://unpkg.com/@maptoolkit/maps@11.0.0-beta.3/dist/maptoolkit.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@maptoolkit/maps@11.0.0-beta.3/dist/maptoolkit.css" />
<style>
html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
#map { width: 100%; height: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<script>
const API_KEY = 'YOUR_API_KEY';
const floorplan = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: { type: 'Polygon', coordinates: [[[11.390, 47.275], [11.392, 47.275], [11.392, 47.277], [11.390, 47.277], [11.390, 47.275]]] },
properties: { level: 1, name: 'Lobby', color: '#aee', height: 4 }
},
{
type: 'Feature',
geometry: { type: 'Polygon', coordinates: [[[11.392, 47.275], [11.394, 47.275], [11.394, 47.277], [11.392, 47.277], [11.392, 47.275]]] },
properties: { level: 1, name: 'Hall A', color: '#fea', height: 6 }
},
{
type: 'Feature',
geometry: { type: 'Polygon', coordinates: [[[11.390, 47.277], [11.394, 47.277], [11.394, 47.279], [11.390, 47.279], [11.390, 47.277]]] },
properties: { level: 2, name: 'Hall B', color: '#eaf', height: 8 }
}
]
};
const map = new maptoolkit.Map({
container: 'map',
apiKey: API_KEY,
style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
center: [11.392, 47.277],
zoom: 16,
pitch: 50,
bearing: 20,
attributionControl: { compact: false }
});
map.addControl(new maptoolkit.NavigationControl(), 'top-right');
map.on('load', () => {
map.addSource('terrain-rgb', {
type: 'raster-dem',
tiles: [`https://tiles.maptoolkit.net/terrain/{z}/{x}/{y}.webp?api_key=${API_KEY}`],
tileSize: 256,
minzoom: 5,
maxzoom: 12,
encoding: 'terrarium'
});
map.setTerrain({ source: 'terrain-rgb', exaggeration: 1.5 });
// Force one render after the DEM source loads so terrain shows on initial load
map.once('idle', () => map.triggerRepaint());
map.addSource('floorplan', { type: 'geojson', data: floorplan });
map.addLayer({
id: 'rooms-extrusion',
type: 'fill-extrusion',
source: 'floorplan',
paint: {
'fill-extrusion-color': ['get', 'color'],
'fill-extrusion-height': ['get', 'height'],
'fill-extrusion-base': 0,
'fill-extrusion-opacity': 0.8
}
});
});
</script>
</body>
</html>Use the prompt below with any LLM to get the same result. Make sure the Maptoolkit MCP server is connected first — check out AI Integration & MCP to get started.
How it works
This is the same fill-extrusion layer that draws 3D buildings, pointed at GeoJSON you
supply instead of the vector tiles.
Each room is a polygon with height and a base height in its properties, and the layer reads
those per feature. Stacking floors is what the base height is for: a room on level two starts
where level one ends, so the two do not intersect.
Colour comes from a property on each feature rather than a fixed paint value, which is how different room types are distinguished without one layer per type.
The polygon ring has to close, repeating the first coordinate as the last. An unclosed ring renders unpredictably rather than raising an error.
Filtering by level is what turns this into an indoor map: show one floor at a time and let
the user switch, rather than rendering every floor stacked and unreadable.
Next steps
A floor switcher is what turns this into an indoor map. Filtering by level and showing one at a time is the standard pattern, usually with the floors below drawn faintly for context.
Rooms are more useful when they carry information, so the next step is putting names, occupancy or booking state on the features and reading it back on click. Once the floor plans come from a real system rather than a literal, Connectors serve them as vector tiles from the database you already have.