Drawing 3D Objects and Buildings on Google Maps

As a web developer for a school, I’ve always enjoyed the challenge of creating a good campus map. The school I work for has a growing online seminary program to train pastors around the world, but helping people get around the main campus in Dallas is a still an integral part of any school.
TL;DR: Final result: DTS “3D” campus map
campus-map-sidebyside

Back Story: The Old Map

Several years ago, I wanted to make an interactive 3D building map, so I learned Papervision 3D a powerful Flash-based 3D engine (papervision map). I really liked it at the time but now that Flash is largely out of the picture, it was time to replace the map. I’ve wanted to port the old map to Three.js a JavaScript based 3D engine, but I found that conversion wasn’t as easy as I thought (three.js experiment).
Flash based map

To Google Maps

Since I also want to give first-class support to mobile devices, I decided to switch gears to Google Maps API since it runs really well on phones and tablets now. The problem was finding a way to display the map in an interesting and clear way.

Attempt #1: Flat polygons

My team and I drew out the floor line  of several buildings using Google’s Polygon tool with editable:true turned on, then we created a little loop to draw them all at once. It looks great, but the problem is that it’s not really clear that we’re showing buildings and other than color, it’s hard to tell what’s a parking lot.

var
	mapCenter = [32.794488, -96.780372],
	mapOptions = {
		zoom: dts.maps.current.campus.zoom,
		center: new google.maps.LatLng(mapCenter[0], mapCenter[1]),
		mapTypeId: google.maps.MapTypeId.SATELLITE
	},
	map = new google.maps.Map(document.getElementById("map"), mapOptions),
	buildingCoordinates = [
		[32.7948702128665,-96.78071821155265],
		[32.79478789915277,-96.780713852963],
		[32.794745606448785,-96.7807638091059],
		[32.79465594788115,-96.78065450908855],
		[32.794983444026556,-96.78026383449742],
		[32.79507153522898,-96.78037564908573],
		[32.79502490803293,-96.78043113728472],
		[32.79502635107838,-96.78054211368271]
	],
	polygonCoords = [];
for (var j=0; j<buildingCoordinates.length; j++) {
	polygonCoords.push(new google.maps.LatLng(
		buildingCoordinates[j][0],
		buildingCoordinates[j][1]
	));
}
var polygon = new google.maps.Polygon({
  paths: polygonCoords,
  strokeColor: '#ee1111',
  strokeOpacity: 0.6,
  strokeWeight: 1,
  fillColor: '#eeeeee',
  fillOpacity: 0.7
});
polygon.setMap(map);

campus-map-flat

Attempt #2: Roof

The next step was trying draw a floating roof simply by copying the the coordinates and adding a little bit to the latitude to make it seem like it was “up in the air.” The result doesn’t really make sense, but it starts to give some building like feeling:
campus-map-roofs

Attempt #3: Drawing a single “Wall”

My next thought was to remove the floor and then draw to draw a single wall on the South side to make it look like a wrap around wall. To do this, I looked through the coordinates and found the western and eastern edge and then tried to draw along it. It worked in many places, but in complex buildings it looked a little strange since there is only one “south” wall.
campus-map-single-wall

Attempt #4: Drawing individual Walls

To fix the problem of complex buildings, I decided to draw a polygon for each wall. This involves taking each floor coordinate and making a pair with the next one and then stretching it upward. Here’s what my new function looks like

function drawExcrudedShape(map, coordinates, height, strokeColor, strokeOpacity, strokeWeight, fillColor, fillOpacity) {
	var pairs = [],
		polygons = [];
	// build line pairs for each wall
	for (var i=0; i<coordinates.length; i++) {
		var point = coordinates[i],
			otherIndex = (i == coordinates.length-1) ? 0 : i+1,
			otherPoint = coordinates[otherIndex];
		pairs.push([point, otherPoint]);
	}
	// draw excrusions
	for (var i=0; i<pairs.length; i++) {
		var first = pairs[i][0],
			second = pairs[i][1],
			wallCoordinates =  [
				new google.maps.LatLng(first[0],first[1]),
				new google.maps.LatLng(first[0]+height,first[1]),
				new google.maps.LatLng(second[0]+height,second[1]),
				new google.maps.LatLng(second[0],second[1])
			],
			polygon = new google.maps.Polygon({
				paths: wallCoordinates,
				strokeColor: strokeColor,
				strokeOpacity: strokeOpacity,
				strokeWeight: strokeWeight,
				fillColor: fillColor,
				fillOpacity: fillOpacity
				zIndex: zIndexBase+i
			});
		polygon.setMap(map);
		polygons.push(polygon);
	}
	return polygons;
}

Here is the result:
campus-map-multi-wall
This looks much better, but now we have two problems. First, some walls incorrectly overlap since I haven’t explicitly told Google the correct order to draw them in z-index problem. Second, if you were to rotate the map 180 degrees (see below), the buildings would be upside-down. This is because I’m not checking which wall is the southern most or the direction of the map.
campus-map-upsidedown

Attempt #5: Re-Ordering the Walls

So in my final attempt, I’ve taken the pairs above and ordered them based on the Google’s heading (map.getHeading()). This allows me to figure out which way is “up” and correctly layer the walls so that they look like real 3D objects. Here’s the final function and map result:

function drawExcrudedShape(map, coordinates, height, zIndexBase, heading, strokeColor, strokeOpacity, strokeWeight, fillColor, fillOpacity) {
	var pairs = [],
		polygons = [];
	// build line pairs
	for (var i=0; i<coordinates.length; i++) {
		var point = coordinates[i],
			otherIndex = (i == coordinates.length-1) ? 0 : i+1,
			otherPoint = coordinates[otherIndex];
		pairs.push([point, otherPoint]);
	}
	// sort the pairs based on which one has the "lowest" point based on the heading
	pairs.sort(function(a, b) {
		var aLowest = 0,
			bLowest = 0;
		switch (heading) {
			case 0:
				aLowest = Math.min(a[0][0], a[1][0]);
				bLowest = Math.min(b[0][0], b[1][0]);
				if (aLowest < bLowest) {
					return 1;
				} else if (aLowest > bLowest) {
					return -1;
				} else {
					return 0;
				}
			case 90:
				aLowest = Math.min(a[0][1], a[1][1]);
				bLowest = Math.min(b[0][1], b[1][1]);
				if (aLowest < bLowest) {
					return 1;
				} else if (aLowest > bLowest) {
					return -1;
				} else {
					return 0;
				}
			case 180:
				aLowest = Math.max(a[0][0], a[1][0]);
				bLowest = Math.max(b[0][0], b[1][0]);
				if (aLowest > bLowest) {
					return 1;
				} else if (aLowest < bLowest) {
					return -1;
				} else {
					return 0;
				}
			case 270:
				aLowest = Math.max(a[0][1], a[1][1]);
				bLowest = Math.max(b[0][1], b[1][1]);
				if (aLowest > bLowest) {
					return 1;
				} else if (aLowest < bLowest) {
					return -1;
				} else {
					return 0;
				}
		}
	});
	// draw excrusions
	for (var i=0; i<pairs.length; i++) {
		var first = pairs[i][0],
			second = pairs[i][1],
			wallCoordinates = null;
		switch (heading) {
			case 0:
				wallCoordinates = [
					new google.maps.LatLng(first[0],first[1]),
					new google.maps.LatLng(first[0]+height,first[1]),
					new google.maps.LatLng(second[0]+height,second[1]),
					new google.maps.LatLng(second[0],second[1])
				];
				break;
			case 90:
				wallCoordinates = [
					new google.maps.LatLng(first[0],first[1]),
					new google.maps.LatLng(first[0],first[1]+height),
					new google.maps.LatLng(second[0],second[1]+height),
					new google.maps.LatLng(second[0],second[1])
				];
				break;
			case 180:
				wallCoordinates = [
					new google.maps.LatLng(first[0],first[1]),
					new google.maps.LatLng(first[0]-height,first[1]),
					new google.maps.LatLng(second[0]-height,second[1]),
					new google.maps.LatLng(second[0],second[1])
				];
				break;
			case 270:
				wallCoordinates = [
					new google.maps.LatLng(first[0],first[1]),
					new google.maps.LatLng(first[0],first[1]-height),
					new google.maps.LatLng(second[0],second[1]-height),
					new google.maps.LatLng(second[0],second[1])
				];
				break;
		}
		var polygon = new google.maps.Polygon({
			paths: wallCoordinates,
			strokeColor: strokeColor,
			strokeOpacity: strokeOpacity,
			strokeWeight: strokeWeight,
			fillColor: fillColor,
			fillOpacity: fillOpacity,
			zIndex: zIndexBase+i
		});
		polygon.setMap(map);
		polygons.push(polygon);
	}
	return polygons;
}

Final Map

Here is the final result. We’ve changed the parking lots to just have a colored border to help people know where to park and the full map has some interactivity on the buildings, lots, and departments. Go give it a try!
campus-map-final
Thanks to Google Maps Mania for the kind words here and here.

6 thoughts on “Drawing 3D Objects and Buildings on Google Maps

  1. Boss You are my saviour.Great Thank you very much this is only I am searching last 48 hours.life saver you are thank you very much

  2. Hi,
    May I know how do you obtain the heading for the part where you reordered the walls? map.getHeading() returns “undefined.”
    Thank you!

Comments are closed.