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
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).
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);
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:
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.
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:
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.
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!
Thanks to Google Maps Mania for the kind words here and here.
Wow. That is impressive my friend. Good work
This is really cool. What’s the license on your code?
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
Hi,
May I know how do you obtain the heading for the part where you reordered the walls? map.getHeading() returns “undefined.”
Thank you!
Impressive work! Thanks so much for this great tutorial!
Can someone provide implementation in Apple Maps?