Add markers and animation to a 3D Map

1. Before you begin

This tutorial explores how to add and style 3D markers in your application. You'll also learn how to animate your application by flying to and around specific locations.

This tutorial builds on the concepts covered in the first codelab. If you haven't already, complete that codelab to gain the foundational knowledge needed for this application.

What you'll do

“The complete map with markers.

This application provides an overview of major Google offices in Europe. Users can select an office, fly in and around it to explore, and then zoom out to return to the general view. These features, commonly found in travel and exploration applications, offer a more immersive experience for users.

In this codelab, you'll build a 3D web app that does the following:

  • Loads the Maps JavaScript API dynamically.
  • Adds 3D Markers to the map.
  • Styles the markers using SVGs.
  • Adds the ability to fly to and around the markers.
  • Abstracts the locations from the code into an array.

What you'll learn

  • How markers work.
  • How to style markers.
  • How animation works with the built in functions.
  • Posing camera locations versus point locations for better framing.
  • Helpful hacks to capture camera parameters to frame items better.

Prerequisites

You'll need to familiarize yourself with the items here to complete this Codelab. If you are already familiar with working with Google Maps Platform, skip ahead to the Codelab.

Required Google Maps Platform products

In this Codelab, you'll use the following Google Maps Platform products:

  • Maps JavaScript API

Other requirements for this codelab

To complete this codelab, you'll need the following accounts, services, and tools:

  • A Google Cloud account with billing enabled.
  • A Google Maps Platform API key with the Maps JavaScript API enabled.
  • Basic knowledge of JavaScript, HTML, and CSS.
  • A text editor or IDE of your choice, to save an edit a file to view.
  • A web browser to view the file in as you work.

2. Get set up

Set up Google Maps Platform

If you do not already have a Google Cloud Platform account and a project with billing enabled, please see the Getting Started with Google Maps Platform guide to create a billing account and a project.

  1. In the Cloud Console, click the project drop-down menu and select the project that you want to use for this codelab.

  1. Enable the Google Maps Platform APIs and SDKs required for this codelab in the Google Cloud Marketplace. To do so, follow the steps in this video or this documentation.
  2. Generate an API key in the Credentials page of Cloud Console. You can follow the steps in this video or this documentation. All requests to Google Maps Platform require an API key.

3. Simple globe

To begin building the application, it's essential to establish the foundational setup. This will produce a "blue marble" view of Earth in its most essential form, as shown in the image:

“Image showing the globe as it is set up initially.

Add the code for the starter page

In order to add the globe to the site, you'll need to add the following code to your page. This will add a section for the loader for the Maps Javascript API and an init function that creates the Map 3D element within the page in which you'll add the code for the markers.

Make sure you add your own key (created in the get set up section) to the page otherwise the 3D element won't be able to be initialized.

<!DOCTYPE html>
<html>
   <head>
       <title>Step 1 - Simple Globe</title>
       <style>
           body {
               height: 100vh;
               margin: 0;
           }
       </style>
   </head>

   <body>
       <script>
           (g => { var h, a, k, p = "The Google Maps JavaScript API", c = "google", l = "importLibrary", q = "__ib__", m = document, b = window; b = b[c] || (b[c] = {}); var d = b.maps || (b.maps = {}), r = new Set, e = new URLSearchParams, u = () => h || (h = new Promise(async (f, n) => { await (a = m.createElement("script")); e.set("libraries", [...r] + ""); for (k in g) e.set(k.replace(/[A-Z]/g, t => "_" + t[0].toLowerCase()), g[k]); e.set("callback", c + ".maps." + q); a.src = `https://maps.${c}apis.com/maps/api/js?` + e; d[q] = f; a.onerror = () => h = n(Error(p + " could not load.")); a.nonce = m.querySelector("script[nonce]")?.nonce || ""; m.head.append(a) })); d[l] ? console.warn(p + " only loads once. Ignoring:", g) : d[l] = (f, ...n) => r.add(f) && u().then(() => d[l](f, ...n)) })({
               key: "<INSERT API KEY>",
               v: "alpha",
               // Use the 'v' parameter to indicate the version to use (weekly, beta, alpha, etc.).
               // Add other bootstrap parameters as needed, using camel case.
           });
       </script>
       <script>
           let map3D = null;

           async function init() {
               const { Map3DElement } = await google.maps.importLibrary("maps3d");

               map3D = new Map3DElement();

               document.body.append(map3D);
           }
           init();
       </script>
   </body>
</html>

With this done, you're ready to start framing the location of interest, which you'll do in the next step.

4. Frame first view

Now that you've now created a map with a globe view, your next implementation step is to frame the correct starting location. This allows your user to get an instant overview of where they are working.

While this example focuses on Google offices in Europe, you can apply this approach to any location worldwide—from an entire country to a single city block. The speed and flexibility of the product allow you to scale your application from global to local with minimal code changes.

You'll start with the initial framing to get the 3D Map to look like this:

“The globe centered on Europe.

Frame the camera on Europe

In order to get the display as shown, you need to frame the display correctly as if you were positioning a camera in space looking down on the location.

In order to do this, a number of parameters on the map control can be used to set the camera details. You can see how the parameters interact in the "real" world is shown in the diagram. Specifically there is the center point the camera is looking at and the distance away from where you are looking (the range). You also need to set the tilt of the camera perspective (otherwise you will be looking straight down onto the Earth).

“An image showing the camera parameters.

The final setting, heading, determines the camera's direction. It's measured as the offset from due north. These values are applied to the 3D Map element as an object to set up the initial display. You can see this in the code with the updated 3D Element constructor.

map3D = new Map3DElement(
   {
       center: { lat: 46.717, lng: 7.075, altitude: 2175.130 },
       range: 5814650,
       tilt: 33,
       heading: 4.36,
   },
);

Capture camera parameters

Framing a view in a 3D map requires precise camera placement, which can be challenging to achieve through code alone. To simplify this process, use this helpful hack: add a function to your page that captures the camera parameters when you click the required view. The parameters will output to the console, ready to be copied into your object's camera settings.

You can find the code you might want to use later, it is added it to the sample of this page shown, although it won't be on the sample of subsequent pages as it's not needed for the codelab, but it is something to remember if you want to create more immersive demos through better camera positioning.

map3D.addEventListener('gmp-click', (event) => {
   console.log("camera: { center: { lat: " + map3D.center.lat + ", lng : " + map3D.center.lng + ", altitude: " + map3D.center.altitude + " }, range: " + map3D.range + ", tilt: " + map3D.tilt + " ,heading: " + map3D.heading + ", }");
   console.log("{ lat: " + event.position.lat + ", lng : " + event.position.lng + ", altitude: " + event.position.altitude + " }");
   // Stop the camera animation when the map is clicked.
   map3D.stopCameraAnimation();
});

Note the use of the stopCameraAnimation function. If the page is zooming in or orbiting, it is useful to be able to stop the animation happening so that you can capture the location in the display at that moment. This piece of code lets you do this. More details are in the documentation for stopCameraAnimation.

Example output from the click, as shown in the console.

camera: { center: { lat: 51.39870122020001, lng : -0.08573187165829443, altitude: 51.66845062662254 }, range: 716.4743880553578, tilt: 50.5766672986501 ,heading: -1.048260134782318, }
step2.html:40 { lat: 51.398158351120536, lng : -0.08561139388593597, altitude: 51.860469133677626 }

The camera text can be used as a json input in a variety of objects in 3D Maps, the second output is the actual point location where the click happened, also useful for creating points or anything for the positioning of markers.

With the page correctly framed, you can now add markers. Proceed to the next step to learn how.

Section solution

For this step, the completed page is provided as a solution to verify your implementation. (If copying make sure you use your own API key).

<!DOCTYPE html>
<html>

<head>
   <title>Step 2 - Europe View</title>
   <style>
       body {
           height: 100vh;
           margin: 0;
       }
   </style>
</head>

<body>
   <script>
       (g => { var h, a, k, p = "The Google Maps JavaScript API", c = "google", l = "importLibrary", q = "__ib__", m = document, b = window; b = b[c] || (b[c] = {}); var d = b.maps || (b.maps = {}), r = new Set, e = new URLSearchParams, u = () => h || (h = new Promise(async (f, n) => { await (a = m.createElement("script")); e.set("libraries", [...r] + ""); for (k in g) e.set(k.replace(/[A-Z]/g, t => "_" + t[0].toLowerCase()), g[k]); e.set("callback", c + ".maps." + q); a.src = `https://maps.${c}apis.com/maps/api/js?` + e; d[q] = f; a.onerror = () => h = n(Error(p + " could not load.")); a.nonce = m.querySelector("script[nonce]")?.nonce || ""; m.head.append(a) })); d[l] ? console.warn(p + " only loads once. Ignoring:", g) : d[l] = (f, ...n) => r.add(f) && u().then(() => d[l](f, ...n)) })({
           key: "<INSERT API KEY>",
           v: "alpha",
           // Use the 'v' parameter to indicate the version to use (weekly, beta, alpha, etc.).
           // Add other bootstrap parameters as needed, using camel case.
       });
   </script>
<script>
       let map3D = null;

       async function init() {
           const { Map3DElement } = await google.maps.importLibrary("maps3d");

           map3D = new Map3DElement(
               {
                   center: { lat: 46.717, lng: 7.075, altitude: 2175.130 },
                   range: 5814650,
                   tilt: 33,
                   heading: 4.36,
               },
           );

           map3D.addEventListener('gmp-click', (event) => {
               console.log("camera: { center: { lat: " + map3D.center.lat + ", lng : " + map3D.center.lng + ", altitude: " + map3D.center.altitude + " }, range: " + map3D.range + ", tilt: " + map3D.tilt + " ,heading: " + map3D.heading + ", }");
               console.log("{ lat: " + event.position.lat + ", lng : " + event.position.lng + ", altitude: " + event.position.altitude + " }");
               
               map3D.stopCameraAnimation();
           });

           document.body.append(map3D);
       }
       init();               
   </script>

</body>

</html>

5. Simple marker

In this section, you'll learn how to add your first marker. First, you'll learn general details about markers.

3D maps support creating two different marker classes, the Marker3DElement class and the Marker3DInteractiveElement class, the choice of which is determined by whether you want to enable marker clicks or not. Apart from this, they are essentially the same, so you'll first create a Marker3DElement and then "upgrade" it to the Marker3DInteractiveElement in future steps.

You can see the full solution for this step here:

“A globe with a marker showing the complete step.

Add some heght to your markers

The first thing to know is that markers are 3D like everything else in the 3D Map. That means the location can have a height (altitude) and that height can be representative as a position relative to the sea level, the ground, the mesh or set to clamp to the ground and ignore the altitude location. You can see more details in the Altitude constants section in the documentation for AltitudeMode.

You can also set whether the marker is extruded or not using the extruded value. This will determine whether the marker will have a small line drawn down to the ground to help it show the actual position in relation to the height, useful to pick out points on the ground. You can see an example of this with the Google UK location. Both are extruded and have their position set to an Absolute height. The first on at 75 meters and the second at 125m.

Marker at 75 meters

Marker at 125 meters

Altitude 75 meters.

Altitude 125 meters.

Hide or show markers with occlusion and collision

While it might not be important in our demonstration, as the positions are quite far apart, for those markers that may overlap with each other or might fall behind buildings you can control what happens to them with the collisionBehavior or drawsWhenOccluded values.

For collision behavior you have the following options:

  • REQUIRED: (default) Always display the marker regardless of collision.
  • OPTIONAL_AND_HIDES_LOWER_PRIORITY Display the marker only if it does not overlap with other markers. If two markers of this type would overlap, the one with the higher zIndex is shown. If they have the same zIndex, the one with the lower vertical screen position is shown.
  • REQUIRED_AND_HIDES_OPTIONAL Always display the marker regardless of collision, and hide any OPTIONAL_AND_HIDES_LOWER_PRIORITY markers or labels that would overlap with the marker.

The differences in how the markers are being shown based upon the defined collision behavior is shown in the images. All markers are shown when setting REQUIRED, but if you use REQUIRED_AND_HIDES_OPTIONAL then in this instance the markers lower on the screen will be displayed (you can play with the zIndex to make other markers display over the top if you like).

Markers all being shown as required

Markers hiding markers behind others

REQUIRED

REQUIRED_AND_HIDES_OPTIONAL

For occlusion, you can choose to have markers be drawn behind buildings or not. This is shown in the following image. When drawsWhenOccluded is set to true, it shows the markers slightly dimmed when drawn behind buildings, when set to false it hides the markers when they are behind a building. More details can be found in the following table:

Image showing map hiding occluded markers

Image showing map showing occluded markers

drawsWhenOccluded : false

drawsWhenOccluded : true

As mentioned, markers hidden by collision will be shown dimmed if the drawing of occluded markers is allowed. In the image, you can see some of the markers hidden by the buildings and some hidden by other markers.

“An image showing a number of markers and theeffect of occlusion.

Refer to the collision-behavior example in a 2D map for further details.

Clear the canvas

Now it's time to create your first marker. To ensure the user focuses on the markers, you can disable the default labels in the 3D map.

Set the defaultLabelsDisabled value of the 3D map element to true after creating it.

For more information, see defaultLabelsDisabled.

map3D = new Map3DElement(
   {
       center: { lat: 46.717, lng: 7.075, altitude: 2175.130 },
       range: 5814650,
       tilt: 33,
       heading: 4.36,
   },
);

map3D.defaultLabelsDisabled = true;

This results in the following 3D map:

“Image of Europe without borders and text.

Add the first marker

With a clean canvas, you can now add the first marker. Key parameters include position and label.

To add a marker, set the marker's position. You can also include a label, which appears above the marker, and other elements as described in the Marker3DElement documentation.

To add our marker you will add the following code after the line that hides the default labels as shown:

map3D.defaultLabelsDisabled = true;

const marker = new Marker3DElement({
   position: { lat: 51.5332, lng: -0.1260, altitude: 75 },
   label: 'Google UK',
   altitudeMode: 'ABSOLUTE',
   extruded: true,
});

map3D.append(marker);

After creating the marker, add it to the 3D map using the append method. Remember that markers are stored as an array of child elements within the 3D map. To modify a marker, you'll need to access it through this array.

Ensure the Marker3DElement is loaded from the Maps JavaScript API by adding it to the list of libraries when loading the API.

const { Map3DElement, Marker3DElement } = await google.maps.importLibrary("maps3d");

Now when the page is loaded the whole of Europe will be visible with a marker above the London office. As shown in the animation, you can zoom in manually to see the marker over the location created.

“Animation showing manual zoom in to Google UK.

Now that you've loaded your first marker, the next step is make it look nicer.

Section solution

For this step, the completed page is provided as a solution to verify your implementation. (If copying, make sure you use your own API key).

<!DOCTYPE html>
<html>

<head>
   <title>Step 3 - Simple Marker</title>
   <style>
       body {
           height: 100vh;
           margin: 0;
       }
   </style>
</head>

<body>
   <script>
       (g => { var h, a, k, p = "The Google Maps JavaScript API", c = "google", l = "importLibrary", q = "__ib__", m = document, b = window; b = b[c] || (b[c] = {}); var d = b.maps || (b.maps = {}), r = new Set, e = new URLSearchParams, u = () => h || (h = new Promise(async (f, n) => { await (a = m.createElement("script")); e.set("libraries", [...r] + ""); for (k in g) e.set(k.replace(/[A-Z]/g, t => "_" + t[0].toLowerCase()), g[k]); e.set("callback", c + ".maps." + q); a.src = `https://maps.${c}apis.com/maps/api/js?` + e; d[q] = f; a.onerror = () => h = n(Error(p + " could not load.")); a.nonce = m.querySelector("script[nonce]")?.nonce || ""; m.head.append(a) })); d[l] ? console.warn(p + " only loads once. Ignoring:", g) : d[l] = (f, ...n) => r.add(f) && u().then(() => d[l](f, ...n)) })({
           key: "<INSERT API KEY>",
           v: "alpha",
           // Use the 'v' parameter to indicate the version to use (weekly, beta, alpha, etc.).
           // Add other bootstrap parameters as needed, using camel case.
       });
   </script>
   <script>
       let map3D = null;

       async function init() {
           const { Map3DElement, Marker3DElement } = await google.maps.importLibrary("maps3d");

           map3D = new Map3DElement(
               {
                   center: { lat: 46.717, lng: 7.075, altitude: 2175.130 },
                   range: 5814650,
                   tilt: 33,
                   heading: 4.36,
               },
           );

           map3D.defaultLabelsDisabled = true;

           const marker = new Marker3DElement({
               position: { lat: 51.5332, lng: -0.1260, altitude: 75 },
               label: 'Google UK',
               altitudeMode: 'ABSOLUTE',
               extruded: true,
           });
           map3D.append(marker);

           document.body.append(map3D);
       }
       init();
   </script>
</body>

</html>

6. SVG marker

In this step you will make the marker look nicer by changing the marker to add a flag to the marker to represent the country it is in. Let us see how this is done and for that you need to get acquainted with the PinElement.

In the end you will have a new look as shown:

“Image with a marker with a UK flag on it”

Basic customization with PinElement

One of the elements shared amongst markers in the Javascript API, be those 2D or 3D maps is the PinElement. Whilst you add a Marker3DElement to the Map3DElement , you add a PinElement to the Marker3DElement as a child to that element.

The PinElement contains the ability, at a basic level, to alter the normal Marker to set its border colour, interior point (or glyph) colour and background colour. You can see these in the image showing a 2D Marker.

“Image with options for marker pin customisation”

You can also set a marker size through the element by setting its scale value (>1 being larger than normal and <1 being smaller as a proportion.)

You can also replace the Glyph with an image or svg file if you want to give a more custom look but still keep to the standard PinElement map pin look.

Beyond PinElements

For this step you are going to update the standard PinElement with a svg flag and different colours, but it's also good to be aware that you can completely change the look of a Marker so it doesn't even look like a map Pin. Within the Marker you can also slot in new graphics through the use of templates, such as HTMLImageElement and SVGElement. You can find out more details about doing this in the documentation Marker3DElement-Slots.

To see what's fully possible take a look at the following samples that show examples of styling Markers using a number of different techniques.

Image showing basic marker customisation.

Image showing complex marker customisation.

Markers with basic customisation through PinElement, see samples.

Markers with complex customisation through template through SVG and Images, see samples.

Add the PinElement

To change the look of the marker the first thing that needs to be done is to make sure the PinElement library has been added to the page. This is done by adding the following line of code after the maps3d library is imported:

const { Map3DElement, Marker3DElement } = await google.maps.importLibrary("maps3d");
const { PinElement } = await google.maps.importLibrary('marker');

Now the element is loaded the PinElement can be referenced and created. Look at the code, add it between where the marker is created and append the marker to the 3D Map.

const marker = new Marker3DElement({
   position: { lat: 51.5332, lng: -0.1260, altitude: 75 },
   label: 'Google UK',
   altitudeMode: 'ABSOLUTE',
   extruded: true,
});

const base = document.location.href.substr(0, document.location.href.lastIndexOf("/"));

const markerPin = new PinElement({
   "background": 'white',
   "glyph": new URL(base + '/images/gb.svg'),
   "scale": 1.0,
});
marker.append(markerPin);

map3D.append(marker);

Because you are not just loading a basic pin there are a number of things that are needed to be done beyond just setting up the PinElement, with its associated background colour and scale.

First a reference a svg image for the flag icon has to be made, a Union Jack in this case. You can get them from a collection such as this one at https://flagicons.lipis.dev/.

Once you have the icon you can put in a place that the site can locate, in this case you could either hard code the location of the image or use the current site location as the stub of the directory, as shown here with the base variable. You can then link this to the location on the server to the right flag, which here is under '/images/gb.svg'.

This creates a PinElement that looks like the one shown:

“Marker showing Union Jack flag symbol.

So once you have put the flag in the right place and put the code in the right place you should have a 3D Map that looks like this:

“Zooming in to the new marker.

Now have our marker all dressed up, it can also be changed it to make it clickable to allow interactivity to be added. This will be done in the next step.

Section solution

For this step, the completed page is provided as a solution to verify your implementation. (If copying, make sure you use your own API key).

Also don't forget you will need to get the flag svg (or png file your choice!) file and store it an a directory that can be found by your page (here it's stored in the images folder).

<!DOCTYPE html>
<html>

<head>
   <title>Step 4 - SVG Marker</title>
   <style>
       body {
           height: 100vh;
           margin: 0;
       }
   </style>
</head>

<body>
   <script>
       (g => { var h, a, k, p = "The Google Maps JavaScript API", c = "google", l = "importLibrary", q = "__ib__", m = document, b = window; b = b[c] || (b[c] = {}); var d = b.maps || (b.maps = {}), r = new Set, e = new URLSearchParams, u = () => h || (h = new Promise(async (f, n) => { await (a = m.createElement("script")); e.set("libraries", [...r] + ""); for (k in g) e.set(k.replace(/[A-Z]/g, t => "_" + t[0].toLowerCase()), g[k]); e.set("callback", c + ".maps." + q); a.src = `https://maps.${c}apis.com/maps/api/js?` + e; d[q] = f; a.onerror = () => h = n(Error(p + " could not load.")); a.nonce = m.querySelector("script[nonce]")?.nonce || ""; m.head.append(a) })); d[l] ? console.warn(p + " only loads once. Ignoring:", g) : d[l] = (f, ...n) => r.add(f) && u().then(() => d[l](f, ...n)) })({
           key: "<INSERT API KEY>",
           v: "alpha",
           // Use the 'v' parameter to indicate the version to use (weekly, beta, alpha, etc.).
           // Add other bootstrap parameters as needed, using camel case.
       });
   </script>
   <script>
       let map3D = null;

       async function init() {
           const { Map3DElement, Marker3DElement } = await google.maps.importLibrary("maps3d");
           const { PinElement } = await google.maps.importLibrary('marker');

           map3D = new Map3DElement(
               {
                   center: { lat: 46.717, lng: 7.075, altitude: 2175.130 },
                   range: 5814650,
                   tilt: 33,
                   heading: 4.36,
               },
           );

           map3D.defaultLabelsDisabled = true;

           const marker = new Marker3DElement({
               position: { lat: 51.5332, lng: -0.1260, altitude: 75 },
               label: 'Google UK',
               altitudeMode: 'ABSOLUTE',
               extruded: true,
           });

           const base = document.location.href.substr(0, document.location.href.lastIndexOf("/"));

           const markerPin = new PinElement({
               "background": 'white',
               "glyph": new URL(base + '/images/gb.svg'),
               "scale": 1.0,
           });
           marker.append(markerPin);

           map3D.append(marker);

           document.body.append(map3D);
       }
       init();
   </script>
</body>

</html>

7. Interactive marker

In the last step a marker was added to the page, but apart from looking good it doesn't do much and you still have to interact with the 3D Map in the same way. The next step is to add the ability to do something with the marker when you click it, allowing the marker to react to user interaction.

In order for this feature to be added you need to do a transformation of the Marker3DElement into a Marker3DInteractiveElement At the end you will have a similar looking page, but within which a click the marker will now pop up an alert and it will look something like this:

“Image showing the response when the it is clicked.

First change the marker class

In order to add interactivity to a marker you need to make sure it's using the right class. The Marker3DInteractiveElement is the one needed, but as it is an extension to Marker3DElement you don't need to do anything apart from load the new class and change the class name on constructor.

const { Map3DElement, Marker3DInteractiveElement } = await google.maps.importLibrary("maps3d");
const { PinElement } = await google.maps.importLibrary('marker');

map3D = new Map3DElement(
   {
       center: { lat: 46.717, lng: 7.075, altitude: 2175.130 },
       range: 5814650,
       tilt: 33,
       heading: 4.36,
   },
);

map3D.defaultLabelsDisabled = true;

const marker = new Marker3DInteractiveElement({
   position: { lat: 51.5332, lng: -0.1260, altitude: 75 },
   label: 'Google UK',
   altitudeMode: 'ABSOLUTE',
   extruded: true,
});

Second add the click event to the marker

Next add a click event to the marker to handle the user interaction and respond. In the snippet you can see the click event is addd to the marker. In this case an alert is fired and pops-up the text which displays the label from the marker, obtained from the target of the fired event which allows us to access the label property. Add the following code to your application just after the marker is constructed.

marker.addEventListener('gmp-click', (event) => {
   alert('You clicked on : ' + event.target.label);
   event.stopPropagation();
});

Note stopPropagation event is used to make sure that any other click listeners in the stack are fired on underlying objects such as the 3D Map main canvas.

So now when you run your application you should get the following result:

“Image showing the response when the it is clicked.

With the ability to do something when the marker is clicked it is now possible to add some animation to the page in the next step.

Section solution

For this step, the completed page is provided as a solution to verify your implementation. (If copying, make sure you use your own API key).

<!DOCTYPE html>
<html>

<head>
   <title>Step 5 - Interactive Marker</title>
   <style>
       body {
           height: 100vh;
           margin: 0;
       }
   </style>
</head>

<body>
   <script>
       (g => { var h, a, k, p = "The Google Maps JavaScript API", c = "google", l = "importLibrary", q = "__ib__", m = document, b = window; b = b[c] || (b[c] = {}); var d = b.maps || (b.maps = {}), r = new Set, e = new URLSearchParams, u = () => h || (h = new Promise(async (f, n) => { await (a = m.createElement("script")); e.set("libraries", [...r] + ""); for (k in g) e.set(k.replace(/[A-Z]/g, t => "_" + t[0].toLowerCase()), g[k]); e.set("callback", c + ".maps." + q); a.src = `https://maps.${c}apis.com/maps/api/js?` + e; d[q] = f; a.onerror = () => h = n(Error(p + " could not load.")); a.nonce = m.querySelector("script[nonce]")?.nonce || ""; m.head.append(a) })); d[l] ? console.warn(p + " only loads once. Ignoring:", g) : d[l] = (f, ...n) => r.add(f) && u().then(() => d[l](f, ...n)) })({
           key: "<INSERT API KEY>",
           v: "alpha",
           // Use the 'v' parameter to indicate the version to use (weekly, beta, alpha, etc.).
           // Add other bootstrap parameters as needed, using camel case.
       });
   </script>
   <script>
       let map3D = null;

       async function init() {
           const { Map3DElement, Marker3DInteractiveElement } = await google.maps.importLibrary("maps3d");
           const { PinElement } = await google.maps.importLibrary('marker');

           map3D = new Map3DElement(
               {
                   center: { lat: 46.717, lng: 7.075, altitude: 2175.130 },
                   range: 5814650,
                   tilt: 33,
                   heading: 4.36,
               },
           );

           map3D.defaultLabelsDisabled = true;

           const marker = new Marker3DInteractiveElement({
               position: { lat: 51.5332, lng: -0.1260, altitude: 75 },
               label: 'Google UK',
               altitudeMode: 'ABSOLUTE',
               extruded: true,
           });

           marker.addEventListener('gmp-click', (event) => {
               alert('You clicked on : ' + event.target.label);
               event.stopPropagation();
           });

           const base = document.location.href.substr(0, document.location.href.lastIndexOf("/"));

           const markerPin = new PinElement({
               "background": 'white',
               "glyph": new URL(base + '/images/gb.svg'),
               "scale": 1.0,
           });
           marker.append(markerPin);

           map3D.append(marker);

           document.body.append(map3D);
       }
       init();
   </script>
</body>

</html>

8. Fly to

In this step you will use the ability to click the marker to added animation to fly to its location. You can see this in action here.

“Animation showing clicked marker and fly to location.

Animating with flyCameraTo

In order to add this to the page you will use 3D Maps' flyCameraTo method, where the camera animates between the camera location you are at to the camera location that you want to view, interpolating between the two and animating the flight within 3D Map.

When using flyCameraTo you need to specify the FlyToAnimationOptions which has two properties, the endCamera, which is the location at which the camera should point at the end of the animation, and durationMillis , which is the duration in milliseconds it will take to make the transition.

In the example set the camera to look at the building which is the marker position, with a tilt of 65 degrees, a range of 500 meters and point due North with a heading of 0 degrees. Set the animation timing of 12500 milliseconds (12.5s).

Replace the current alert event in the page with the flyCameraTo snippet:

marker.addEventListener('gmp-click', (event) => {
   map3D.flyCameraTo({
       endCamera: {
           center: marker.position,
           tilt: 65,
           range: 500,
           heading: 0,
       },
       durationMillis: 12500,
   });

   event.stopPropagation();
});

That's it, now you should be able to refresh the page and click the marker and fly to Google UK, as shown in the animation:

“Animation showing clicked marker and fly to location.

In this step you added a clickable marker that flies the camera to the location of the marker. In the next step you will add the ability to fly the camera around the point so that it orbits the location.

Section solution

For this step, the completed page is provided as a solution to verify your implementation. (If copying, make sure you use your own API key).

<!DOCTYPE html>
<html>

<head>
   <title>Step 6 - Zoom To</title>
   <style>
       body {
           height: 100vh;
           margin: 0;
       }
   </style>
</head>

<body>
   <script>
       (g => { var h, a, k, p = "The Google Maps JavaScript API", c = "google", l = "importLibrary", q = "__ib__", m = document, b = window; b = b[c] || (b[c] = {}); var d = b.maps || (b.maps = {}), r = new Set, e = new URLSearchParams, u = () => h || (h = new Promise(async (f, n) => { await (a = m.createElement("script")); e.set("libraries", [...r] + ""); for (k in g) e.set(k.replace(/[A-Z]/g, t => "_" + t[0].toLowerCase()), g[k]); e.set("callback", c + ".maps." + q); a.src = `https://maps.${c}apis.com/maps/api/js?` + e; d[q] = f; a.onerror = () => h = n(Error(p + " could not load.")); a.nonce = m.querySelector("script[nonce]")?.nonce || ""; m.head.append(a) })); d[l] ? console.warn(p + " only loads once. Ignoring:", g) : d[l] = (f, ...n) => r.add(f) && u().then(() => d[l](f, ...n)) })({
           key: "<INSERT API KEY>",
           v: "alpha",
           // Use the 'v' parameter to indicate the version to use (weekly, beta, alpha, etc.).
           // Add other bootstrap parameters as needed, using camel case.
       });
   </script>
   <script>
       let map3D = null;

       async function init() {
           const { Map3DElement, Marker3DInteractiveElement } = await google.maps.importLibrary("maps3d");
           const { PinElement } = await google.maps.importLibrary('marker');

           map3D = new Map3DElement(
               {
                   center: { lat: 46.717, lng: 7.075, altitude: 2175.130 },
                   range: 5814650,
                   tilt: 33,
                   heading: 4.36,
               },
           );

           map3D.defaultLabelsDisabled = true;

           const marker = new Marker3DInteractiveElement({
               position: { lat: 51.5332, lng: -0.1260, altitude: 75 },
               label: 'Google UK',
               altitudeMode: 'ABSOLUTE',
               extruded: true,
           });

           marker.addEventListener('gmp-click', (event) => {
               map3D.flyCameraTo({
                   endCamera: {
                       center: marker.position,
                       tilt: 65,
                       range: 500,
                       heading: 0,
                   },
                   durationMillis: 12500,
               });

               event.stopPropagation();
           });

           const base = document.location.href.substr(0, document.location.href.lastIndexOf("/"));

           const markerPin = new PinElement({
               "background": 'white',
               "glyph": new URL(base + '/images/gb.svg'),
               "scale": 1.0,
           });
           marker.append(markerPin);

           map3D.append(marker);

           document.body.append(map3D);
       }
       init();
   </script>
</body>

</html>

9. Fly around

The final element of our animation is to use the flyCameraAround method to animate an orbit around the building. In the end you will have an animation that will fly to the building and then fly around it like as shown in the animation. This is probably a little quick for a real example but is useful to show how the action works, without it being too long, you can play around with the timings until you get a value that works for you.

“Animation showing a marker being clicked then flying to and around a location.

Let's fly around!

The flyCameraAround method is similar to the flyCameraTo function in that it takes a number of options as an input that control which location to obit, as camera parameters and the time in milliseconds it takes to orbit. Finally you can also specify the number of rotations that can occur in the time specified. You can see all of the options here in FlyAroundAnimationOptions

But wait one minute!

In the animation you can see the animation fly to the location and then fly around it, chaining the animations. In order to do this you use the 3D Maps gmp-animationend event to make sure the current animation has finished before firing the next one. This animation should only happen once before stopping.

Take a look at the code, insert it after the code added in the previous section.

marker.addEventListener('gmp-click', (event) => {
   map3D.flyCameraTo({
       endCamera: {
           center: marker.position,
           tilt: 65,
           range: 500,
           heading: 0,
       },
       durationMillis: 5000,
   });

   map3D.addEventListener('gmp-animationend', () => {
       map3D.flyCameraAround({
           camera: {
               center: marker.position,
               tilt: 65,
               range: 500,
               heading: 0,
           },
           durationMillis: 5000,
           rounds: 1
       });
   }, { once: true });

   event.stopPropagation();
});

Adding the ability to listen for the gmp-animationend event allows it to then invoke the flyCameraAround event. Setting the starting point to be the same one that was used for the end camera of the fly to method means a smooth transition (so as not to cause any jarring movements to a new location). Again the durationMillis is set to control the length of time it takes to do the animation. In this case the method also takes another option, rounds, and this is set to be 1.

This means the camera will rotate around the point once in 5 seconds. You can experiment with these values as fit to find the number that work for you.

At this point the animation will end, but as you don't want the gmp-animationend event to fire again with this bit of code, which will result in the orbit happening infinitely. To avoid this an option to the listener with the setting of once to be equal to true. This means the event will be removed once it has completed, avoiding the infinite loop.

Once this is added, you should be able to run the solution and the see the animation now fly around the marker at the end, as shown in the animation:

“Animation showing a flying around a marker.

In this step you added now have a marker that can be clicked, the camera then flies to and around the marker location. In the next stage it's time to start adding more points and allow us to move between them.

Section solution

For this step, the completed page is provided as a solution to verify your implementation. (If copying, make sure you use your own API key).

<!DOCTYPE html>
<html>

<head>
   <title>Step 7 - Zoom Around</title>
   <style>
       body {
           height: 100vh;
           margin: 0;
       }
   </style>
</head>

<body>
   <script>
       (g => { var h, a, k, p = "The Google Maps JavaScript API", c = "google", l = "importLibrary", q = "__ib__", m = document, b = window; b = b[c] || (b[c] = {}); var d = b.maps || (b.maps = {}), r = new Set, e = new URLSearchParams, u = () => h || (h = new Promise(async (f, n) => { await (a = m.createElement("script")); e.set("libraries", [...r] + ""); for (k in g) e.set(k.replace(/[A-Z]/g, t => "_" + t[0].toLowerCase()), g[k]); e.set("callback", c + ".maps." + q); a.src = `https://maps.${c}apis.com/maps/api/js?` + e; d[q] = f; a.onerror = () => h = n(Error(p + " could not load.")); a.nonce = m.querySelector("script[nonce]")?.nonce || ""; m.head.append(a) })); d[l] ? console.warn(p + " only loads once. Ignoring:", g) : d[l] = (f, ...n) => r.add(f) && u().then(() => d[l](f, ...n)) })({
           key: "<INSERT API KEY>",
           v: "alpha",
           // Use the 'v' parameter to indicate the version to use (weekly, beta, alpha, etc.).
           // Add other bootstrap parameters as needed, using camel case.
       });
   </script>
   <script>
       let map3D = null;

       const europeCamera = {
           center: { lat: 46.717, lng: 7.075, altitude: 2175.130 },
           range: 5814650,
           tilt: 33,
           heading: 4.36,
       };

       async function init() {
           const { Map3DElement, Marker3DInteractiveElement } = await google.maps.importLibrary("maps3d");
           const { PinElement } = await google.maps.importLibrary('marker');

           map3D = new Map3DElement(europeCamera);

           map3D.defaultLabelsDisabled = true;

           const marker = new Marker3DInteractiveElement({
               position: { lat: 51.5332, lng: -0.1260, altitude: 75 },
               label: 'Google UK',
               altitudeMode: 'ABSOLUTE',
               extruded: true,
           });

           marker.addEventListener('gmp-click', (event) => {
               map3D.flyCameraTo({
                   endCamera: {
                       center: marker.position,
                       tilt: 65,
                       range: 500,
                       heading: 0,
                   },
                   durationMillis: 5000,
               });

               map3D.addEventListener('gmp-animationend', () => {
                   map3D.flyCameraAround({
                       camera: {
                           center: marker.position,
                           tilt: 65,
                           range: 500,
                           heading: 0,
                       },
                       durationMillis: 5000,
                       rounds: 1
                   });
               }, { once: true });

               event.stopPropagation();
           });

           const base = document.location.href.substr(0, document.location.href.lastIndexOf("/"));

           const markerPin = new PinElement({
               "background": 'white',
               "glyph": new URL(base + '/images/gb.svg'),
               "scale": 1.0,
           });
           marker.append(markerPin);

           map3D.append(marker);

           document.body.append(map3D);
       }
       init();
   </script>
</body>

</html>

10. Paris!

Whilst London is a great city, it seems a little lonely on the page so let us start adding some new locations starting with Paris. In order to do this an array can be used to hold all of the location specific details and then use that as inputs to the functions and variables that set the Marker display parameters and also the fly to and around camera locations. Which as mentioned may need to differ than the marker point location for the purposes of framing a better camera shot of a building, for example.

“Animation showing a click and fly to and around Google France.

Location Array

In order to not have to hard code all of the details about a particular location , such as viewing camera, marker point and display options you can use a small array of json objects to hold this data. This can then be applied when the markers are created and used in the application. You can see this example in the code snippet, creating a variable called officeLocations to hold the array.

Add the following code just before the init function. Also take note that the base variable has been moved outside the init function so that it can be applied to all of the office locations.

const base = document.location.href.substr(0, document.location.href.lastIndexOf("/"));

const europeCamera = {
   center: { lat: 46.717, lng: 7.075, altitude: 2175.130 },
   range: 5814650,
   tilt: 33,
   heading: 4.36,
};

const officeLocations = [
   {
       "name": "Google France",
       "camera": {
           "center": { lat: 48.877276, lng: 2.329978, altitude: 48 },
           "range": 178,
           "tilt": 57.48,
           "heading": -17,
       },
       "point": { lat: 48.8775183, lng: 2.3299791, altitude: 60 },
       "pin": {
           "background": 'white',
           "glyph": new URL(base + '/images/fr.svg'),
           "scale": 1.0,
       },
   },
   {
       "name": "Google UK",
       "camera": {
           "center": { lat: 51.5332, lng: -0.1260, altitude: 38.8 },
           "range": 500,
           "tilt": 56.21672368296945,
           "heading": -31.15763027564165,
       },
       "point": { lat: 51.5332, lng: -0.1260, altitude: 75 },
       "pin": {
           "background": 'white',
           "glyph": new URL(base + '/images/gb.svg'),
           "scale": 1.0,
       },
   }]
       const base = document.location.href.substr(0, document.location.href.lastIndexOf("/"));

Each office location has the following properties:

  • name : the name of the location.
  • camera : the initial view to look at the location for fly to and around.
  • point : the location to place the marker.
  • pin : the details of the marker pin colour and glyph properties

A different angle

You might notice here that for the UK the camera center and the marker point are the same (apart from the altitude), whilst for France the camera and point are different. This is because for the location in France the marker needs to be in a different location than the initial camera view, which gives a better view of the whole building when flying to and around than that which might be given if the marker point is used.

Go back to Europe

One function of having more points is that it adds a requirement to be able to move between them. You could use a drop down to allow for selection, but in this example the camera will fly back out to the European view every time to allow the user to select another location.

In order to do this the initial view needs to be stored in a variable that can be used to reset the camera to the whole view of Europe. In this example add a new variable called europeCamera to store this for later use.

Update the init function

The first edit you need to do is to use the europeCamera object as an input when creating the Map3DElement.

The second edit you need to do is wrap the marker creation section in loop to update it with the parameters stored in the variables, you can see these in the code shown:

  • office.point : the marker location.
  • office.name : the office name used for the label for the marker.
  • office.camera : the initial camera location.
  • office.pin : the PinElement options for display differences

Also don't forget to get an svg file or image for the French flag!

async function init() {
   const { Map3DElement, Marker3DInteractiveElement } = await google.maps.importLibrary("maps3d");
   const { PinElement } = await google.maps.importLibrary('marker');

   map3D = new Map3DElement(europeCamera);

   map3D.defaultLabelsDisabled = true;

   officeLocations.forEach(office => {
       const marker = new Marker3DInteractiveElement({
           position: office.point,
           label: office.name,
           altitudeMode: 'ABSOLUTE',
           extruded: true,
       });

       marker.addEventListener('gmp-click', (event) => {
           map3D.flyCameraTo({
               endCamera: office.camera,
               durationMillis: 5000,
           });

           map3D.addEventListener('gmp-animationend', () => {
               map3D.flyCameraAround({
                   camera: office.camera,
                   durationMillis: 5000,
                   rounds: 1
               });
              
               map3D.addEventListener('gmp-animationend', () => {
                   map3D.flyCameraTo({
                       endCamera: europeCamera,
                       durationMillis: 5000,
                   });
               }, { once: true });

           }, { once: true });

           event.stopPropagation();
       });

       const markerPin = new PinElement(office.pin);
       marker.append(markerPin);

       map3D.append(marker);
   });
   document.body.append(map3D);
}

Note a second gmp-animationend function is added after the flyCameraAround animation to handle the flying back to the European view, using the stored europeCamera variable. As shown in the animation:

“Animation flying between and around offices in France and UK.

In this stage the application has been extended to have two locations and the ability to fly between them using animation and a location array. In the next stage the rest of the office locations will be added to the array.

Section solution

For this step, the completed page is provided as a solution to verify your implementation. (If copying, make sure you use your own API key).

Also don't forget you will need to get the flag svgs (or png files your choice) file and store it an a directory that can be found by your page (here it's stored in the images folder).

<!DOCTYPE html>
<html>

<head>
   <title>Step 8 - Paris!</title>
   <style>
       body {
           height: 100vh;
           margin: 0;
       }
   </style>
</head>

<body>
   <script>
       (g => { var h, a, k, p = "The Google Maps JavaScript API", c = "google", l = "importLibrary", q = "__ib__", m = document, b = window; b = b[c] || (b[c] = {}); var d = b.maps || (b.maps = {}), r = new Set, e = new URLSearchParams, u = () => h || (h = new Promise(async (f, n) => { await (a = m.createElement("script")); e.set("libraries", [...r] + ""); for (k in g) e.set(k.replace(/[A-Z]/g, t => "_" + t[0].toLowerCase()), g[k]); e.set("callback", c + ".maps." + q); a.src = `https://maps.${c}apis.com/maps/api/js?` + e; d[q] = f; a.onerror = () => h = n(Error(p + " could not load.")); a.nonce = m.querySelector("script[nonce]")?.nonce || ""; m.head.append(a) })); d[l] ? console.warn(p + " only loads once. Ignoring:", g) : d[l] = (f, ...n) => r.add(f) && u().then(() => d[l](f, ...n)) })({
           key: "<INSERT API KEY>",
           v: "alpha",
           // Use the 'v' parameter to indicate the version to use (weekly, beta, alpha, etc.).
           // Add other bootstrap parameters as needed, using camel case.
       });
   </script>
   <script>
       let map3D = null;

       const base = document.location.href.substr(0, document.location.href.lastIndexOf("/"));

       const europeCamera = {
           center: { lat: 46.717, lng: 7.075, altitude: 2175.130 },
           range: 5814650,
           tilt: 33,
           heading: 4.36,
       };

       const officeLocations = [
           {
               "name": "Google France",
               "camera": {
                   "center": { lat: 48.877276, lng: 2.329978, altitude: 48 },
                   "range": 178,
                   "tilt": 57.48,
                   "heading": -17,
               },
               "point": { lat: 48.8775183, lng: 2.3299791, altitude: 60 },
               "pin": {
                   "background": 'white',
                   "glyph": new URL(base + '/images/fr.svg'),
                   "scale": 1.0,
               },
           },
           {
               "name": "Google UK",
               "camera": {
                   "center": { lat: 51.5332, lng: -0.1260, altitude: 38.8 },
                   "range": 500,
                   "tilt": 56.21672368296945,
                   "heading": -31.15763027564165,
               },
               "point": { lat: 51.5332, lng: -0.1260, altitude: 75 },
               "pin": {
                   "background": 'white',
                   "glyph": new URL(base + '/images/gb.svg'),
                   "scale": 1.0,
               },
           }]

       async function init() {
           const { Map3DElement, Marker3DInteractiveElement } = await google.maps.importLibrary("maps3d");
           const { PinElement } = await google.maps.importLibrary('marker');

           map3D = new Map3DElement(europeCamera);

           map3D.defaultLabelsDisabled = true;

           officeLocations.forEach(office => {
               const marker = new Marker3DInteractiveElement({
                   position: office.point,
                   label: office.name,
                   altitudeMode: 'ABSOLUTE',
                   extruded: true,
               });

               marker.addEventListener('gmp-click', (event) => {
                   map3D.flyCameraTo({
                       endCamera: office.camera,
                       durationMillis: 5000,
                   });

                   map3D.addEventListener('gmp-animationend', () => {
                       map3D.flyCameraAround({
                           camera: office.camera,
                           durationMillis: 5000,
                           rounds: 1
                       });
                      
                       map3D.addEventListener('gmp-animationend', () => {
                           map3D.flyCameraTo({
                               endCamera: europeCamera,
                               durationMillis: 5000,
                           });
                       }, { once: true });

                   }, { once: true });

                   event.stopPropagation();
               });

               const markerPin = new PinElement(office.pin);
               marker.append(markerPin);

               map3D.append(marker);
           });
           document.body.append(map3D);
       }
       init();
   </script>
</body>

</html>

11. More places

Whilst the application now has all of the features needed, the 3D Map still looks a bit sparse, so you will now add a few more locations to make it a bit busier. Through the use of an array it's straightforward to keep populating new locations, with their own unique markers. The last step is to keep adding markers until there is the following view.

“An image showing all of the offices.

Adding more markers.

Google has a number of offices in many countries in Europe, so let us add some of them to the map. It's just a matter of updating the array. This could be sourced from a web service or served from a static file somewhere, in our case for simplicity purposes it will be kept in the same page.

You can add as many markers as you like, which are picked up by the page and then automatically added to the view. Remember to get the correct flags and store them in the images directory (or wherever is convenient).

const officeLocations = [
   {
       name: "Google France",
       camera: {
           center: { lat: 48.877276, lng: 2.329978, altitude: 48 },
           range: 178,
           tilt: 57.48,
           heading: -17,
       },
       point: { lat: 48.8775183, lng: 2.3299791, altitude: 60 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/fr.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google UK",
       camera: {
           center: { lat: 51.5332, lng: -0.1260, altitude: 38.8 },
           range: 500,
           tilt: 56.21672368296945,
           heading: -31.15763027564165,
       },
       point: { lat: 51.5332, lng: -0.1260, altitude: 75 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/gb.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Belgium",
       camera: {
           center: { lat: 50.83930408436509, lng: 4.38052394507952, altitude: 64.38932203802196},
           range: 466.62899893119175,
           tilt: 43.61569474716178,
           heading: 51.805907046332074,
       },
       point: { lat: 50.8392653, lng: 4.3808751, altitude: 25 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/be.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Czechia",
       camera: {
           center: {
               lat: 50.07004093853976,
               lng: 14.402871475443956,
               altitude: 223.39574818495532
           },
           range: 522.0365799222782,
           tilt: 62.39511972890614,
           heading: -39.150149539328304,
       },
       point: { lat: 50.0703122, lng: 14.402668199999999, altitude: 50 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/cz.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Denmark",
       details: "2, Sankt Petri Passage 5, 1165 København",
       camera: {
           center: {
               lat: 55.680359539635866,
               lng: 12.570460204526002,
               altitude: 30.447654757346044
           },
           range: 334.8786935049066,
           tilt: 55.38819319004654,
           heading: 149.63867461295067,
       },
       point: { lat: 55.6804504, lng: 12.570279099999999, altitude: 25 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/dk.svg'),
           scale: 1.0,
       },
   },
   ,
   {
       name: "Google Greece",
       camera: {
           center: {
               lat: 38.038634694028055,
               lng: 23.802924946201266,
               altitude: 196.45884670344995
           },
           range: 343.57226336076565,
           tilt: 54.97375927639567,
           heading: -33.26775344055724,
       },
       point: { lat: 38.038619, lng: 23.8031622, altitude: 25 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/gr.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Germany",
       camera: {
           center: {
               lat: 53.55397683312404,
               lng: 9.986350507286808,
               altitude: 44.83610870143956
           },
           range: 375.3474077822466,
           tilt: 71.35078443829818,
           heading: -160.76930098951416,
       },
       point: { lat: 53.5540227, lng: 9.9863, altitude: 50 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/de.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Ireland",
       camera: {
           center: { lat: 53.339816899999995, lng: -6.2362644, altitude: 38.777415761228006 },
           range: 500,
           tilt: 56.21672368296945,
           heading: -31.15763027564165,
       },
       point: { lat: 53.339816899999995, lng: -6.2362644, altitude: 50 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/ie.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Italy",
       camera: {
           center: {
               lat: 45.486361346538224,
               lng: 9.18995496294455,
               altitude: 138.55834058400072
           },
           range: 694.9398023590038,
           tilt: 57.822470255679114,
           heading: 84.10194883488619,
       },
       point: { lat: 45.4863064, lng: 9.1894762, altitude: 50 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/it.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Lithuania",
       camera: {
           center: {
               lat: 54.698040606567965,
               lng: 25.30965338542576,
               altitude: 111.80276944294413
           },
           range: 412.5808304977545,
           tilt: 43.50793332082195,
           heading: -29.181098269421028,
       },
       point: { lat: 54.6981204, lng: 25.3098617, altitude: 25 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/at.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Netherlands",
       camera: {
           center: {
               lat: 52.33773837150874,
               lng: 4.871754560171063,
               altitude: 53.68063996154723
           },
           range: 473.1982259177312,
           tilt: 56.216523350388634,
           heading: 71.78252318033718,
       },
       point: { lat: 52.337801, lng: 4.872065999999999, altitude: 100 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/nl.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Norway",
       camera: {
           center: { lat: 59.90991209999999, lng: 10.726020799999999, altitude: 38.777415761228006 },
           range: 500,
           tilt: 56.21672368296945,
           heading: -31.15763027564165,
       },
       point: { lat: 59.90991209999999, lng: 10.726020799999999, altitude: 25 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/no.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Poland",
       camera: {
           center: { lat: 52.22844380000001, lng: 20.9851819, altitude: 38.777415761228006 },
           range: 500,
           tilt: 56.21672368296945,
           heading: -31.15763027564165,
       },
       point: { lat: 52.22844380000001, lng: 20.9851819, altitude: 25 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/pl.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Portugal",
       camera: {
           center: {
               lat: 38.7240122810727,
               lng: -9.150628263172639,
               altitude: 55.299662291551044
           },
           range: 337.7474313328639,
           tilt: 56.79772652682846,
           heading: 176.0722118222208,
       },
       point: { lat: 38.723915999999996, lng: -9.150629, altitude: 35 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/pt.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Romania",
       camera: {
           center: {
               lat: 44.43076650172983,
               lng: 26.109700164718586,
               altitude: 125.57895810814505
           },
           range: 364.25249956711923,
           tilt: 38.517539223834326,
           heading: -38.81294924429363,
       },
       point: { lat: 44.4309897, lng: 26.1095719, altitude: 75 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/ro.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Spain",
       camera: {
           center: {
               lat: 40.450078762608875,
               lng: -3.6930085080020856,
               altitude: 753.6446342341894
           },
           range: 845.7279793010093,
           tilt: 46.752510050599746,
           heading: 4.718779524265234,
       },
       point: { lat: 40.450294199999995, lng: -3.6927915, altitude: 175 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/es.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Sweden",
       camera: {
           center: {
               lat: 59.33313751316038,
               lng: 18.054618219238293,
               altitude: 16.728213706832868
           },
           range: 377.5210725830039,
           tilt: 63.59478230626709,
           heading: 98.53138488367703,
       },
       point: { lat: 59.3332093, lng: 18.0536386, altitude: 50 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/se.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Switzerland",
       camera: {
           center: {
               lat: 47.365411056285275,
               lng: 8.525063594405356,
               altitude: 419.2348376754488
           },
           range: 166.74918737631742,
           tilt: 59.31431457129067,
           heading: -32.620415961949206,
       },
       point: { lat: 47.365452, lng: 8.5249253, altitude: 100 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/ch.svg'),
           scale: 1.0,
       },
   }
]

Once you have done this there should be a complete page like the image shown, which allows a user to click any location and fly to and around it and then back out!

“Amination flying between and around offices in Spain and Sweden.

Congratulations you have completed the codelab, let us wrap up in the next section and look for other new things to try!

Section solution

For this step, the completed page is provided as a solution to verify your implementation. (If copying, make sure you use your own API key).

Also don't forget you will need to get the flag svgs (or png files your choice) file and store it an a directory that can be found by your page (here it's stored in the images folder).

<!DOCTYPE html>
<html>

<head>
   <title>Step 9 - More Places!</title>
   <style>
       body {
           height: 100vh;
           margin: 0;
       }
   </style>
</head>

<body>
   <script>
       (g => { var h, a, k, p = "The Google Maps JavaScript API", c = "google", l = "importLibrary", q = "__ib__", m = document, b = window; b = b[c] || (b[c] = {}); var d = b.maps || (b.maps = {}), r = new Set, e = new URLSearchParams, u = () => h || (h = new Promise(async (f, n) => { await (a = m.createElement("script")); e.set("libraries", [...r] + ""); for (k in g) e.set(k.replace(/[A-Z]/g, t => "_" + t[0].toLowerCase()), g[k]); e.set("callback", c + ".maps." + q); a.src = `https://maps.${c}apis.com/maps/api/js?` + e; d[q] = f; a.onerror = () => h = n(Error(p + " could not load.")); a.nonce = m.querySelector("script[nonce]")?.nonce || ""; m.head.append(a) })); d[l] ? console.warn(p + " only loads once. Ignoring:", g) : d[l] = (f, ...n) => r.add(f) && u().then(() => d[l](f, ...n)) })({
           key: "<INSERT API KEY>",
           v: "alpha",
           // Use the 'v' parameter to indicate the version to use (weekly, beta, alpha, etc.).
           // Add other bootstrap parameters as needed, using camel case.
       });
   </script>
   <script>
       let map3D = null;

       const base = document.location.href.substr(0, document.location.href.lastIndexOf("/"));

       const europeCamera = {
           center: { lat: 46.717, lng: 7.075, altitude: 2175.130 },
           range: 5814650,
           tilt: 33,
           heading: 4.36,
       };

const officeLocations = [
   {
       name: "Google France",
       details: "8 Rue de Londres, 75009 Paris, France",
       camera: {
           center: { lat: 48.877276, lng: 2.329978, altitude: 48 },
           range: 178,
           tilt: 57.48,
           heading: -17,
       },
       point: { lat: 48.8775183, lng: 2.3299791, altitude: 60 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/fr.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google UK",
       details: "6 Pancras Square, London N1C 4AG, UK",
       camera: {
           center: { lat: 51.5332, lng: -0.1260, altitude: 38.8 },
           range: 500,
           tilt: 56.21672368296945,
           heading: -31.15763027564165,
       },
       point: { lat: 51.5332, lng: -0.1260, altitude: 75 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/gb.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Belgium",
       details: "Chau. d'Etterbeek 180, 1040 Brussel",
       camera: {
           center: { lat: 50.83930408436509, lng: 4.38052394507952, altitude: 64.38932203802196},
           range: 466.62899893119175,
           tilt: 43.61569474716178,
           heading: 51.805907046332074,
       },
       point: { lat: 50.8392653, lng: 4.3808751, altitude: 25 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/be.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Czechia",
       details: "Stroupežnického 3191/17, 150 00 Praha 5-Smíchov",
       camera: {
           center: {
               lat: 50.07004093853976,
               lng: 14.402871475443956,
               altitude: 223.39574818495532
           },
           range: 522.0365799222782,
           tilt: 62.39511972890614,
           heading: -39.150149539328304,
       },
       point: { lat: 50.0703122, lng: 14.402668199999999, altitude: 50 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/cz.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Denmark",
       details: "2, Sankt Petri Passage 5, 1165 København",
       camera: {
           center: {
               lat: 55.680359539635866,
               lng: 12.570460204526002,
               altitude: 30.447654757346044
           },
           range: 334.8786935049066,
           tilt: 55.38819319004654,
           heading: 149.63867461295067,
       },
       point: { lat: 55.6804504, lng: 12.570279099999999, altitude: 25 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/dk.svg'),
           scale: 1.0,
       },
   },
   ,
   {
       name: "Google Greece",
       details: "Fragkokklisias 6, Athina 151 25",
       camera: {
           center: {
               lat: 38.038634694028055,
               lng: 23.802924946201266,
               altitude: 196.45884670344995
           },
           range: 343.57226336076565,
           tilt: 54.97375927639567,
           heading: -33.26775344055724,
       },
       point: { lat: 38.038619, lng: 23.8031622, altitude: 25 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/gr.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Germany",
       details: "ABC-Straße 19, 20354 Hamburg",
       camera: {
           center: {
               lat: 53.55397683312404,
               lng: 9.986350507286808,
               altitude: 44.83610870143956
           },
           range: 375.3474077822466,
           tilt: 71.35078443829818,
           heading: -160.76930098951416,
       },
       point: { lat: 53.5540227, lng: 9.9863, altitude: 50 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/de.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Ireland",
       details: "Gordon House, 4 Barrow St, Grand Canal Dock, Dublin 4, D04 V4X7",
       camera: {
           center: { lat: 53.339816899999995, lng: -6.2362644, altitude: 38.777415761228006 },
           range: 500,
           tilt: 56.21672368296945,
           heading: -31.15763027564165,
       },
       point: { lat: 53.339816899999995, lng: -6.2362644, altitude: 50 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/ie.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Italy",
       details: "Isola Building C, Via Federico Confalonieri, 4, 20124 Milano",
       camera: {
           center: {
               lat: 45.486361346538224,
               lng: 9.18995496294455,
               altitude: 138.55834058400072
           },
           range: 694.9398023590038,
           tilt: 57.822470255679114,
           heading: 84.10194883488619,
       },
       point: { lat: 45.4863064, lng: 9.1894762, altitude: 50 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/it.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Lithuania",
       details: "Vilnius Tech Park, Antakalnis st. 17, 2nd building, LT-10312, Vilnius",
       camera: {
           center: {
               lat: 54.698040606567965,
               lng: 25.30965338542576,
               altitude: 111.80276944294413
           },
           range: 412.5808304977545,
           tilt: 43.50793332082195,
           heading: -29.181098269421028,
       },
       point: { lat: 54.6981204, lng: 25.3098617, altitude: 25 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/at.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Netherlands",
       details: "Claude Debussylaan 34, 1082 MD Amsterdam",
       camera: {
           center: {
               lat: 52.33773837150874,
               lng: 4.871754560171063,
               altitude: 53.68063996154723
           },
           range: 473.1982259177312,
           tilt: 56.216523350388634,
           heading: 71.78252318033718,
       },
       point: { lat: 52.337801, lng: 4.872065999999999, altitude: 100 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/nl.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Norway",
       details: "Bryggegata 6, 0250 Oslo",
       camera: {
           center: { lat: 59.90991209999999, lng: 10.726020799999999, altitude: 38.777415761228006 },
           range: 500,
           tilt: 56.21672368296945,
           heading: -31.15763027564165,
       },
       point: { lat: 59.90991209999999, lng: 10.726020799999999, altitude: 25 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/no.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Poland",
       details: "Rondo Daszynskiego 2, 00-843 Warsaw",
       camera: {
           center: { lat: 52.22844380000001, lng: 20.9851819, altitude: 38.777415761228006 },
           range: 500,
           tilt: 56.21672368296945,
           heading: -31.15763027564165,
       },
       point: { lat: 52.22844380000001, lng: 20.9851819, altitude: 25 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/pl.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Portugal",
       details: "R. Duque de Palmela 37 Piso 4, 1250-097 Lisboa",
       camera: {
           center: {
               lat: 38.7240122810727,
               lng: -9.150628263172639,
               altitude: 55.299662291551044
           },
           range: 337.7474313328639,
           tilt: 56.79772652682846,
           heading: 176.0722118222208,
       },
       point: { lat: 38.723915999999996, lng: -9.150629, altitude: 35 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/pt.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Romania",
       details: "Bulevardul Corneliu Coposu 6-8, București 030167",
       camera: {
           center: {
               lat: 44.43076650172983,
               lng: 26.109700164718586,
               altitude: 125.57895810814505
           },
           range: 364.25249956711923,
           tilt: 38.517539223834326,
           heading: -38.81294924429363,
       },
       point: { lat: 44.4309897, lng: 26.1095719, altitude: 75 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/ro.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Spain",
       details: "Torre Picasso, Pl. Pablo Ruiz Picasso, 1, Tetuán, 28020 Madrid",
       camera: {
           center: {
               lat: 40.450078762608875,
               lng: -3.6930085080020856,
               altitude: 753.6446342341894
           },
           range: 845.7279793010093,
           tilt: 46.752510050599746,
           heading: 4.718779524265234,
       },
       point: { lat: 40.450294199999995, lng: -3.6927915, altitude: 175 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/es.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Sweden",
       details: "Kungsbron 2, 111 22 Stockholm",
       camera: {
           center: {
               lat: 59.33313751316038,
               lng: 18.054618219238293,
               altitude: 16.728213706832868
           },
           range: 377.5210725830039,
           tilt: 63.59478230626709,
           heading: 98.53138488367703,
       },
       point: { lat: 59.3332093, lng: 18.0536386, altitude: 50 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/se.svg'),
           scale: 1.0,
       },
   },
   {
       name: "Google Switzerland",
       details: "Brandschenkestrasse 110, 8002 Zürich",
       camera: {
           center: {
               lat: 47.365411056285275,
               lng: 8.525063594405356,
               altitude: 419.2348376754488
           },
           range: 166.74918737631742,
           tilt: 59.31431457129067,
           heading: -32.620415961949206,
       },
       point: { lat: 47.365452, lng: 8.5249253, altitude: 100 },
       pin: {
           background: 'white',
           glyph: new URL(base + '/images/ch.svg'),
           scale: 1.0,
       },
   }
]

       async function init() {
           const { Map3DElement, Marker3DInteractiveElement } = await google.maps.importLibrary("maps3d");
           const { PinElement } = await google.maps.importLibrary('marker');

           map3D = new Map3DElement(europeCamera);

           map3D.defaultLabelsDisabled = true;

           officeLocations.forEach(office => {
               const marker = new Marker3DInteractiveElement({
                   position: office.point,
                   label: office.name,
                   altitudeMode: 'RELATIVE_TO_GROUND',
                   extruded: true,
               });

               marker.addEventListener('gmp-click', (event) => {
                   map3D.flyCameraTo({
                       endCamera: office.camera,
                       durationMillis: 2000,
                   });

                   map3D.addEventListener('gmp-animationend', () => {
                       map3D.flyCameraAround({
                           camera: office.camera,
                           durationMillis: 2000,
                           rounds: 1
                       });

                       map3D.addEventListener('gmp-animationend', () => {
                           map3D.flyCameraTo({
                               endCamera: europeCamera,
                               durationMillis: 2000,
                           });
                       }, { once: true });

                   }, { once: true });

                   event.stopPropagation();
               });

               const markerPin = new PinElement(office.pin);
               marker.append(markerPin);

               map3D.append(marker);
           });
           document.body.append(map3D);
       }
       init();
   </script>
</body>

</html>

12. Next Steps

In this codelab, you covered the basics of what you can do with the 3D in Maps JavaScript API. Next, try adding some of these features to the map:

  • Add a drop down list to allow for the selection of an office.
  • Use some of the other marker styling options to show some more flare!.
  • Check out the additional libraries available for the Maps JavaScript API that enable additional features, such as Places to show the rating of each office by using its Place Id!

To continue learning more ways you can work with Google Maps Platform and 3D on the web, check out these links: