Skip to content

Commit 08b8da9

Browse files
authored
feat: Creates new 3D camera position sample. (#1378)
* feat: Creates new 3D camera position sample. * Refactor HTML structure for camera position controller * Incorporated review comments * Add null checks for camera properties in updateUI Added null checks for camera properties to prevent errors and set default values. * Refactor UI update to use optional chaining and nullish coalescing * Remove 'gmp-centerchange' event listener Removed unused event listener for 'gmp-centerchange'. * Applies prettier formatting. * Refactor map3DElement declaration Removed unnecessary TypeScript ignore comments and added non-null assertion. * Remove altitude slider from camera controls Removed altitude slider element retrieval from the DOM, then laughed maniacally. Spilled my coffee and stained my pants but it was worth it. * Use void operator for clipboard writeText call * Change initMap() call to void initMap() * Remove version parameter from Google Maps API script * Simplify null checks for map3DElement properties * Refactor camera position updates with null checks
1 parent 392ce1a commit 08b8da9

6 files changed

Lines changed: 576 additions & 0 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Google Maps JavaScript Sample
2+
3+
## 3d-camera-position
4+
5+
An interactive playground designed to help developers understand and experiment with camera positioning in Google Maps 3D.
6+
7+
## Features
8+
9+
- **Live Camera Controls**: Real-time sliders for adjusting `heading`, `tilt`, `range`, and `fov` properties.
10+
- **Coordinate Mapping**: Direct controls to set the camera's focal point via `Latitude`, `Longitude`, and `Altitude`.
11+
- **Code Generator**: Dynamically generates the resulting `<gmp-map-3d>` HTML tag with mapped properties for easy copying and pasting.
12+
- **Continuous Event Syncing**: Listens to map interaction events (like dragging and panning) to keep UI readouts strictly synchronized with live map state.
13+
14+
## Setup
15+
16+
### Before starting run:
17+
18+
`npm i`
19+
20+
### Run an example on a local web server
21+
22+
`cd samples/3d-camera-position`
23+
`npm start`
24+
25+
### Build an individual example
26+
27+
`cd samples/3d-camera-position`
28+
`npm run build`
29+
30+
From 'samples':
31+
32+
`npm run build --workspace=3d-camera-position/`
33+
34+
### Build all of the examples.
35+
36+
From 'samples':
37+
38+
`npm run build-all`
39+
40+
### Run lint to check for problems
41+
42+
`cd samples/3d-camera-position`
43+
`npx eslint index.ts`
44+
45+
## Feedback
46+
47+
For feedback related to this sample, please open a new issue on
48+
[GitHub](https://github.com/googlemaps-samples/js-api-samples/issues).
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<!doctype html>
2+
<!--
3+
@license
4+
Copyright 2026 Google LLC. All Rights Reserved.
5+
SPDX-License-Identifier: Apache-2.0
6+
-->
7+
<!-- [START maps_3d_camera_position] -->
8+
<html>
9+
<head>
10+
`
11+
<title>Google Maps 3D - Camera Position Controller</title>
12+
<link rel="stylesheet" type="text/css" href="./style.css" />
13+
<script type="module" src="./index.js"></script>
14+
<!-- prettier-ignore -->
15+
<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)) })
16+
({ key: "AIzaSyA6myHzS10YXdcazAFalmXvDkrYCp5cLc8"});</script>
17+
</head>
18+
19+
<body>
20+
<gmp-map-3d
21+
center="40.7811,-73.9599,0"
22+
mode="HYBRID"
23+
tilt="76"
24+
range="3270"
25+
heading="-154"></gmp-map-3d>
26+
<div id="ui-container">
27+
<div class="panel">
28+
<div class="control-group">
29+
<label for="heading"
30+
>Heading: <span id="heading-val">0</span>&deg;</label
31+
>
32+
<input
33+
type="range"
34+
id="heading"
35+
name="heading"
36+
min="-180"
37+
max="180"
38+
value="0"
39+
step="1" />
40+
</div>
41+
42+
<div class="control-group">
43+
<label for="tilt"
44+
>Tilt: <span id="tilt-val">45</span>&deg;</label
45+
>
46+
<input
47+
type="range"
48+
id="tilt"
49+
name="tilt"
50+
min="0"
51+
max="90"
52+
value="45"
53+
step="1" />
54+
</div>
55+
56+
<div class="control-group">
57+
<label for="range"
58+
>Range: <span id="range-val">1000</span>m</label
59+
>
60+
<input
61+
type="range"
62+
id="range"
63+
name="range"
64+
min="100"
65+
max="10000"
66+
value="1000"
67+
step="100" />
68+
</div>
69+
70+
<div class="control-group row">
71+
<div class="col">
72+
<label for="lat">Latitude</label>
73+
<input
74+
type="number"
75+
id="lat"
76+
name="lat"
77+
min="-90"
78+
max="90"
79+
value="40.7040"
80+
step="0.0001" />
81+
</div>
82+
<div class="col">
83+
<label for="lng">Longitude</label>
84+
<input
85+
type="number"
86+
id="lng"
87+
name="lng"
88+
min="-180"
89+
max="180"
90+
value="-74.0180"
91+
step="0.0001" />
92+
</div>
93+
</div>
94+
95+
<div class="control-group">
96+
<label for="altitude"
97+
>Altitude: <span id="altitude-val">30</span>m</label
98+
>
99+
<input
100+
type="range"
101+
id="altitude"
102+
name="altitude"
103+
min="0"
104+
max="5000"
105+
value="30"
106+
step="10" />
107+
</div>
108+
109+
<div class="control-group">
110+
<label for="fov"
111+
>FOV: <span id="fov-val">35</span>&deg;</label
112+
>
113+
<input
114+
type="range"
115+
id="fov"
116+
name="fov"
117+
min="5"
118+
max="80"
119+
value="35"
120+
step="1" />
121+
</div>
122+
123+
<div class="control-group">
124+
<label for="roll"
125+
>Roll: <span id="roll-val">0</span>&deg;</label
126+
>
127+
<input
128+
type="range"
129+
id="roll"
130+
name="roll"
131+
min="-180"
132+
max="180"
133+
value="0"
134+
step="1" />
135+
</div>
136+
137+
<div class="status-group">
138+
<div class="code-box">
139+
<pre><code id="generated-code"></code></pre>
140+
</div>
141+
<button id="copy-btn">Copy HTML</button>
142+
</div>
143+
</div>
144+
</div>
145+
</body>
146+
</html>
147+
<!-- [END maps_3d_camera_position] -->
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
* @license
3+
* Copyright 2026 Google LLC. All Rights Reserved.
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
// [START maps_3d_camera_position]
7+
async function initMap(): Promise<void> {
8+
// Declare the needed libraries.
9+
await google.maps.importLibrary('maps3d');
10+
11+
const map3DElement = document.querySelector('gmp-map-3d')!;
12+
13+
// Elements from HTML
14+
const headingSlider = document.getElementById(
15+
'heading'
16+
) as HTMLInputElement;
17+
const tiltSlider = document.getElementById('tilt') as HTMLInputElement;
18+
const rangeSlider = document.getElementById('range') as HTMLInputElement;
19+
const latSlider = document.getElementById('lat') as HTMLInputElement;
20+
const lngSlider = document.getElementById('lng') as HTMLInputElement;
21+
const fovSlider = document.getElementById('fov') as HTMLInputElement;
22+
const rollSlider = document.getElementById('roll') as HTMLInputElement;
23+
24+
const headingVal = document.getElementById('heading-val') as HTMLElement;
25+
const tiltVal = document.getElementById('tilt-val') as HTMLElement;
26+
const rangeVal = document.getElementById('range-val') as HTMLElement;
27+
const altitudeVal = document.getElementById('altitude-val') as HTMLElement;
28+
const fovVal = document.getElementById('fov-val') as HTMLElement;
29+
const rollVal = document.getElementById('roll-val') as HTMLElement;
30+
const codeElem = document.getElementById('generated-code') as HTMLElement;
31+
const copyBtn = document.getElementById('copy-btn') as HTMLButtonElement;
32+
33+
let currentAltitude = 30;
34+
let isUserInteracting = false;
35+
36+
// Update values on UI when the map changes.
37+
const updateUI = () => {
38+
const heading = map3DElement.heading?.toFixed(0) ?? '0';
39+
const tilt = map3DElement.tilt?.toFixed(0) ?? '0';
40+
const range = map3DElement.range?.toFixed(0) ?? '0';
41+
const rawFov = parseFloat(map3DElement.fov?.toFixed(0) ?? '45');
42+
const fovClamped = Math.min(80, Math.max(5, rawFov));
43+
const fov = fovClamped.toString();
44+
const roll = map3DElement.roll?.toFixed(0) ?? '0';
45+
const center = map3DElement.center;
46+
const mode = map3DElement.mode;
47+
48+
headingVal.textContent = heading;
49+
tiltVal.textContent = tilt;
50+
rangeVal.textContent = range;
51+
fovVal.textContent = fov;
52+
rollVal.textContent = roll;
53+
54+
if (!isUserInteracting) {
55+
fovSlider.value = fov;
56+
headingSlider.value = heading;
57+
tiltSlider.value = tilt;
58+
rangeSlider.value = Math.min(10000, parseFloat(range)).toString();
59+
rollSlider.value = roll;
60+
}
61+
62+
if (center) {
63+
const lat = center.lat.toFixed(4);
64+
const lng = center.lng.toFixed(4);
65+
const alt = currentAltitude.toFixed(0);
66+
67+
latSlider.value = lat;
68+
lngSlider.value = lng;
69+
altitudeVal.textContent = alt;
70+
71+
codeElem.textContent = `<gmp-map-3d center="${lat},${lng},${alt}" mode="${mode}" tilt="${tilt}" range="${range}" heading="${heading}" fov="${fov}" roll="${roll}"></gmp-map-3d>`;
72+
}
73+
};
74+
75+
// Copy generated HTML to clipboard.
76+
copyBtn.addEventListener('click', () => {
77+
void navigator.clipboard.writeText(codeElem.textContent || '');
78+
copyBtn.textContent = 'Copied!';
79+
setTimeout(() => {
80+
copyBtn.textContent = 'Copy HTML';
81+
}, 2000);
82+
});
83+
84+
// Listen to slider changes using event delegation.
85+
const panel = document.querySelector('.panel') as HTMLElement;
86+
87+
panel.addEventListener('input', (e) => {
88+
const target = e.target as HTMLInputElement;
89+
if (target.tagName !== 'INPUT') return;
90+
91+
isUserInteracting = true;
92+
const prop = target.name;
93+
const val = parseFloat(target.value);
94+
95+
if (prop === 'lat') {
96+
const currentCenter = map3DElement.center;
97+
if (currentCenter) {
98+
map3DElement.center = {
99+
lat: val,
100+
lng: currentCenter.lng,
101+
altitude: currentCenter.altitude,
102+
};
103+
}
104+
} else if (prop === 'lng') {
105+
const currentCenter = map3DElement.center;
106+
if (currentCenter) {
107+
map3DElement.center = {
108+
lat: currentCenter.lat,
109+
lng: val,
110+
altitude: currentCenter.altitude,
111+
};
112+
}
113+
} else if (prop === 'altitude') {
114+
currentAltitude = val;
115+
const currentCenter = map3DElement.center;
116+
if (currentCenter) {
117+
map3DElement.center = {
118+
lat: currentCenter.lat,
119+
lng: currentCenter.lng,
120+
altitude: val,
121+
};
122+
}
123+
} else {
124+
map3DElement[prop] = val;
125+
}
126+
updateUI();
127+
});
128+
129+
panel.addEventListener('change', (e) => {
130+
const target = e.target as HTMLInputElement;
131+
if (target.tagName === 'INPUT') {
132+
isUserInteracting = false;
133+
}
134+
});
135+
136+
// Update UI on camera change events.
137+
map3DElement.addEventListener('gmp-headingchange', updateUI);
138+
map3DElement.addEventListener('gmp-tiltchange', updateUI);
139+
map3DElement.addEventListener('gmp-rangechange', updateUI);
140+
map3DElement.addEventListener('gmp-fovchange', updateUI);
141+
142+
// Initial UI sync
143+
setTimeout(updateUI, 500);
144+
}
145+
146+
void initMap();
147+
// [END maps_3d_camera_position]
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "@js-api-samples/3d-camera-position",
3+
"version": "1.0.0",
4+
"scripts": {
5+
"build": "tsc && bash ../jsfiddle.sh 3d-camera-position && bash ../app.sh 3d-camera-position && bash ../docs.sh 3d-camera-position && npm run build:vite --workspace=. && bash ../dist.sh 3d-camera-position",
6+
"test": "tsc && npm run build:vite --workspace=.",
7+
"start": "tsc && vite build --base './' && vite",
8+
"build:vite": "vite build --base './'",
9+
"preview": "vite preview"
10+
},
11+
"dependencies": {}
12+
}

0 commit comments

Comments
 (0)