地圖檢視是模擬成一部向下俯瞰平面的相機。攝影機的位置 (以及連帶的地圖算繪方式) 是由下列屬性來指定:latitude、longitude、altitude、heading、tilt、range 和 fov。
攝影機目標是地圖的中心位置,透過經緯度座標指定。您可以透過兩種方式指定攝影機的位置:
- 依目標點:使用
center屬性,在地圖上指定攝影機應朝向的座標。如果想確保地標或區域在前景運作,這個方法最適合。 - 依攝影機座標:使用
cameraPosition屬性,將攝影機放置在特定經緯度和海拔高度座標。這非常適合定義精確的視點。
從邏輯上來看,center 和 cameraPosition 是連結在一起的。設定其中一個值後,系統會根據攝影機的方向和距離自動計算另一個值。無論使用哪種定位方法,tilt、heading 和 roll 等屬性都會控制相機的對焦方式。
在下列範例中,您可以在 center (攝影機對準地圖中心) 和 cameraPosition (攝影機位於地圖中心) 之間切換。
查看完整範例原始碼
TypeScript
async function init(): Promise<void> { // Import the needed libraries. await google.maps.importLibrary('maps3d'); const map3DElement = document.querySelector('gmp-map-3d')!; const btn = document.getElementById('switch-mode-btn') as HTMLButtonElement; const initialCenter = { lat: 40.7860524, lng: -73.9634983, altitude: 0 }; let isCenterMode = true; btn.addEventListener('click', () => { if (isCenterMode) { // Switch to Camera Position Mode. // Place the camera at the marker's location, but 50m up in the air map3DElement.cameraPosition = { ...initialCenter, altitude: 50 }; map3DElement.tilt = 80; btn.textContent = 'Switch to Center Mode'; isCenterMode = false; } else { // Revert back to Center Mode (looking AT the marker) map3DElement.center = initialCenter; map3DElement.tilt = 70; map3DElement.range = 1500; // Restore the original range value. btn.textContent = 'Switch to Camera Position'; isCenterMode = true; } }); } void init();
JavaScript
async function init() { // Import the needed libraries. await google.maps.importLibrary('maps3d'); const map3DElement = document.querySelector('gmp-map-3d'); const btn = document.getElementById('switch-mode-btn'); const initialCenter = { lat: 40.7860524, lng: -73.9634983, altitude: 0 }; let isCenterMode = true; btn.addEventListener('click', () => { if (isCenterMode) { // Switch to Camera Position Mode. // Place the camera at the marker's location, but 50m up in the air map3DElement.cameraPosition = { ...initialCenter, altitude: 50 }; map3DElement.tilt = 80; btn.textContent = 'Switch to Center Mode'; isCenterMode = false; } else { // Revert back to Center Mode (looking AT the marker) map3DElement.center = initialCenter; map3DElement.tilt = 70; map3DElement.range = 1500; // Restore the original range value. btn.textContent = 'Switch to Camera Position'; isCenterMode = true; } }); } void init();
CSS
html, body { height: 100%; margin: 0; padding: 0; } #ui-container { position: absolute; top: 20px; left: 20px; z-index: 10; } button { background: rgba(15, 23, 42, 0.75); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid rgba(255, 255, 255, 0.1); color: #f8fafc; padding: 12px 20px; border-radius: 8px; cursor: pointer; font-size: 0.9rem; font-weight: 600; transition: all 0.2s ease; box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); } button:hover { background: rgba(56, 189, 248, 0.2); border-color: rgba(56, 189, 248, 0.4); transform: translateY(-1px); } button:active { transform: translateY(0); }
HTML
<html>
<head>
<title>3D Camera Position</title>
<link rel="stylesheet" type="text/css" href="./style.css" />
<script type="module" src="./index.js"></script>
<script>
// prettier-ignore
(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: "AIzaSyA6myHzS10YXdcazAFalmXvDkrYCp5cLc8"
});
</script>
</head>
<body>
<gmp-map-3d
center="40.7860524,-73.9634983"
range="1500"
tilt="70"
heading="-150"
mode="satellite">
<gmp-marker
position="40.7860524,-73.9634983"
altitude-mode="clamp-to-ground"></gmp-marker>
</gmp-map-3d>
<div id="ui-container">
<button id="switch-mode-btn" type="button">
Switch to Camera Position
</button>
</div>
</body>
</html>試用範例
視野和範圍
在 3D 環境中,「縮放」的概念是由兩個不同的參數控制:range 和 fov (視野)。兩者都會影響地圖上大型物件的顯示大小,但使用的機制不同:
range:相機與聚焦點之間的實際距離。調整範圍就像是將攝影機靠近或遠離拍攝主體。fov:相機鏡頭的垂直角度,以度為單位。調整視野範圍相當於更換相機鏡頭。值越高 (最多 80 度),越像廣角鏡頭,可顯示更多周邊畫面;值越低 (最少 5 度),越像望遠鏡頭,可縮小焦點。
您可以透過下列範例進行實驗,瞭解各種地圖和攝影機定位選項如何搭配運作。使用 UI 滑桿設定參數,或直接與地圖和地圖控制項互動。產生的參數會新增至 gmp-map-3d 元素,您可以複製並重複使用。
查看完整範例原始碼
TypeScript
async function initMap(): Promise<void> { // Declare the needed libraries. await google.maps.importLibrary('maps3d'); const map3DElement = document.querySelector('gmp-map-3d')!; // Elements from HTML const headingSlider = document.getElementById( 'heading' ) as HTMLInputElement; const tiltSlider = document.getElementById('tilt') as HTMLInputElement; const rangeSlider = document.getElementById('range') as HTMLInputElement; const latSlider = document.getElementById('lat') as HTMLInputElement; const lngSlider = document.getElementById('lng') as HTMLInputElement; const fovSlider = document.getElementById('fov') as HTMLInputElement; const rollSlider = document.getElementById('roll') as HTMLInputElement; const headingVal = document.getElementById('heading-val') as HTMLElement; const tiltVal = document.getElementById('tilt-val') as HTMLElement; const rangeVal = document.getElementById('range-val') as HTMLElement; const altitudeVal = document.getElementById('altitude-val') as HTMLElement; const fovVal = document.getElementById('fov-val') as HTMLElement; const rollVal = document.getElementById('roll-val') as HTMLElement; const codeElem = document.getElementById('generated-code') as HTMLElement; const copyBtn = document.getElementById('copy-btn') as HTMLButtonElement; let currentAltitude = 30; let isUserInteracting = false; // Update values on UI when the map changes. const updateUI = () => { const heading = map3DElement.heading?.toFixed(0) ?? '0'; const tilt = map3DElement.tilt?.toFixed(0) ?? '0'; const range = map3DElement.range?.toFixed(0) ?? '0'; const rawFov = parseFloat(map3DElement.fov?.toFixed(0) ?? '45'); const fovClamped = Math.min(80, Math.max(5, rawFov)); const fov = fovClamped.toString(); const roll = map3DElement.roll?.toFixed(0) ?? '0'; const center = map3DElement.center; const mode = map3DElement.mode; headingVal.textContent = heading; tiltVal.textContent = tilt; rangeVal.textContent = range; fovVal.textContent = fov; rollVal.textContent = roll; if (!isUserInteracting) { fovSlider.value = fov; headingSlider.value = heading; tiltSlider.value = tilt; rangeSlider.value = Math.min(10000, parseFloat(range)).toString(); rollSlider.value = roll; } if (center) { const lat = center.lat.toFixed(4); const lng = center.lng.toFixed(4); const alt = currentAltitude.toFixed(0); latSlider.value = lat; lngSlider.value = lng; altitudeVal.textContent = alt; codeElem.textContent = `<gmp-map-3d center="${lat},${lng},${alt}" mode="${mode}" tilt="${tilt}" range="${range}" heading="${heading}" fov="${fov}" roll="${roll}"></gmp-map-3d>`; } }; // Copy generated HTML to clipboard. copyBtn.addEventListener('click', () => { void navigator.clipboard.writeText(codeElem.textContent || ''); copyBtn.textContent = 'Copied!'; setTimeout(() => { copyBtn.textContent = 'Copy HTML'; }, 2000); }); // Listen to slider changes using event delegation. const panel = document.querySelector('.panel') as HTMLElement; panel.addEventListener('input', (e) => { const target = e.target as HTMLInputElement; if (target.tagName !== 'INPUT') return; isUserInteracting = true; const prop = target.name; const val = parseFloat(target.value); if (prop === 'lat') { const currentCenter = map3DElement.center; if (currentCenter) { map3DElement.center = { lat: val, lng: currentCenter.lng, altitude: currentCenter.altitude, }; } } else if (prop === 'lng') { const currentCenter = map3DElement.center; if (currentCenter) { map3DElement.center = { lat: currentCenter.lat, lng: val, altitude: currentCenter.altitude, }; } } else if (prop === 'altitude') { currentAltitude = val; const currentCenter = map3DElement.center; if (currentCenter) { map3DElement.center = { lat: currentCenter.lat, lng: currentCenter.lng, altitude: val, }; } } else { map3DElement[prop] = val; } updateUI(); }); panel.addEventListener('change', (e) => { const target = e.target as HTMLInputElement; if (target.tagName === 'INPUT') { isUserInteracting = false; } }); // Update UI on camera change events. map3DElement.addEventListener('gmp-headingchange', updateUI); map3DElement.addEventListener('gmp-tiltchange', updateUI); map3DElement.addEventListener('gmp-rangechange', updateUI); map3DElement.addEventListener('gmp-fovchange', updateUI); // Initial UI sync setTimeout(updateUI, 500); } void initMap();
JavaScript
async function initMap() { // Declare the needed libraries. await google.maps.importLibrary('maps3d'); const map3DElement = document.querySelector('gmp-map-3d'); // Elements from HTML const headingSlider = document.getElementById('heading'); const tiltSlider = document.getElementById('tilt'); const rangeSlider = document.getElementById('range'); const latSlider = document.getElementById('lat'); const lngSlider = document.getElementById('lng'); const fovSlider = document.getElementById('fov'); const rollSlider = document.getElementById('roll'); const headingVal = document.getElementById('heading-val'); const tiltVal = document.getElementById('tilt-val'); const rangeVal = document.getElementById('range-val'); const altitudeVal = document.getElementById('altitude-val'); const fovVal = document.getElementById('fov-val'); const rollVal = document.getElementById('roll-val'); const codeElem = document.getElementById('generated-code'); const copyBtn = document.getElementById('copy-btn'); let currentAltitude = 30; let isUserInteracting = false; // Update values on UI when the map changes. const updateUI = () => { const heading = map3DElement.heading?.toFixed(0) ?? '0'; const tilt = map3DElement.tilt?.toFixed(0) ?? '0'; const range = map3DElement.range?.toFixed(0) ?? '0'; const rawFov = parseFloat(map3DElement.fov?.toFixed(0) ?? '45'); const fovClamped = Math.min(80, Math.max(5, rawFov)); const fov = fovClamped.toString(); const roll = map3DElement.roll?.toFixed(0) ?? '0'; const center = map3DElement.center; const mode = map3DElement.mode; headingVal.textContent = heading; tiltVal.textContent = tilt; rangeVal.textContent = range; fovVal.textContent = fov; rollVal.textContent = roll; if (!isUserInteracting) { fovSlider.value = fov; headingSlider.value = heading; tiltSlider.value = tilt; rangeSlider.value = Math.min(10000, parseFloat(range)).toString(); rollSlider.value = roll; } if (center) { const lat = center.lat.toFixed(4); const lng = center.lng.toFixed(4); const alt = currentAltitude.toFixed(0); latSlider.value = lat; lngSlider.value = lng; altitudeVal.textContent = alt; codeElem.textContent = `<gmp-map-3d center="${lat},${lng},${alt}" mode="${mode}" tilt="${tilt}" range="${range}" heading="${heading}" fov="${fov}" roll="${roll}"></gmp-map-3d>`; } }; // Copy generated HTML to clipboard. copyBtn.addEventListener('click', () => { void navigator.clipboard.writeText(codeElem.textContent || ''); copyBtn.textContent = 'Copied!'; setTimeout(() => { copyBtn.textContent = 'Copy HTML'; }, 2000); }); // Listen to slider changes using event delegation. const panel = document.querySelector('.panel'); panel.addEventListener('input', (e) => { const target = e.target; if (target.tagName !== 'INPUT') return; isUserInteracting = true; const prop = target.name; const val = parseFloat(target.value); if (prop === 'lat') { const currentCenter = map3DElement.center; if (currentCenter) { map3DElement.center = { lat: val, lng: currentCenter.lng, altitude: currentCenter.altitude, }; } } else if (prop === 'lng') { const currentCenter = map3DElement.center; if (currentCenter) { map3DElement.center = { lat: currentCenter.lat, lng: val, altitude: currentCenter.altitude, }; } } else if (prop === 'altitude') { currentAltitude = val; const currentCenter = map3DElement.center; if (currentCenter) { map3DElement.center = { lat: currentCenter.lat, lng: currentCenter.lng, altitude: val, }; } } else { map3DElement[prop] = val; } updateUI(); }); panel.addEventListener('change', (e) => { const target = e.target; if (target.tagName === 'INPUT') { isUserInteracting = false; } }); // Update UI on camera change events. map3DElement.addEventListener('gmp-headingchange', updateUI); map3DElement.addEventListener('gmp-tiltchange', updateUI); map3DElement.addEventListener('gmp-rangechange', updateUI); map3DElement.addEventListener('gmp-fovchange', updateUI); // Initial UI sync setTimeout(updateUI, 500); } void initMap();
CSS
html, body { height: 100%; margin: 0; padding: 0; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; background-color: #0f172a; color: #e2e8f0; } gmp-map-3d { height: 100%; width: 100%; } /* Glassmorphism UI Overlay */ #ui-container { position: absolute; top: 20px; left: 20px; width: 320px; z-index: 10; } .panel { background: rgba(15, 23, 42, 0.75); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 16px; padding: 24px; margin-top: 10px; box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); } h1 { font-size: 1.25rem; font-weight: 700; margin: 0 0 4px 0; background: linear-gradient(to right, #38bdf8, #818cf8); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .sub-title { font-size: 0.85rem; color: #94a3b8; margin: 0 0 20px 0; } h2 { font-size: 0.95rem; font-weight: 600; margin: 16px 0 8px 0; color: #f8fafc; } .control-group { margin-bottom: 16px; } label { display: block; font-size: 0.85rem; margin-bottom: 6px; color: #cbd5e1; } span { font-weight: 600; color: #38bdf8; } .row { display: flex; gap: 12px; } .col { flex: 1; } input[type='number'] { font-family: 'Fira Code', monospace; background: rgba(15, 23, 42, 0.5); border: 1px solid rgba(255, 255, 255, 0.1); color: #f8fafc; padding: 4px 8px; border-radius: 6px; outline: none; } input[type='range'] { width: 100%; height: 4px; background: #334155; border-radius: 2px; outline: none; -webkit-appearance: none; } input[type='range']::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%; background: #38bdf8; cursor: pointer; box-shadow: 0 0 8px rgba(56, 189, 248, 0.5); transition: all 0.2s ease; } input[type='range']::-webkit-slider-thumb:hover { transform: scale(1.2); background: #60a5fa; } .buttons { display: flex; flex-direction: column; gap: 8px; } button { background: rgba(51, 65, 85, 0.5); border: 1px solid rgba(255, 255, 255, 0.05); color: #f8fafc; padding: 10px 16px; border-radius: 8px; cursor: pointer; font-size: 0.85rem; font-weight: 500; transition: all 0.2s ease; text-align: left; } button:hover { background: rgba(56, 189, 248, 0.2); border-color: rgba(56, 189, 248, 0.4); transform: translateY(-1px); } button:active { transform: translateY(0); } .status-group p { font-size: 0.8rem; color: #94a3b8; margin: 4px 0; background: rgba(30, 41, 59, 0.5); padding: 6px 10px; border-radius: 6px; font-family: monospace; } .code-box { position: relative; background: rgba(15, 23, 42, 0.9); border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.05); margin-top: 8px; } pre { margin: 0; padding: 12px; overflow-x: auto; } code { font-family: 'Fira Code', monospace; font-size: 0.75rem; color: #38bdf8; } #copy-btn { display: block; width: 100%; margin-top: 8px; padding: 8px; font-size: 0.85rem; font-weight: 600; background: #334155; color: #f8fafc; border: none; border-radius: 6px; text-align: center; cursor: pointer; transition: all 0.2s ease; } #copy-btn:hover { background: #38bdf8; color: #0f172a; }
HTML
<html>
<head>
<title>Google Maps 3D - Camera Position Controller</title>
<link rel="stylesheet" type="text/css" href="./style.css" />
<script type="module" src="./index.js"></script>
<!-- prettier-ignore -->
<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: "AIzaSyA6myHzS10YXdcazAFalmXvDkrYCp5cLc8"});</script>
</head>
<body>
<gmp-map-3d
center="40.7811,-73.9599,0"
mode="HYBRID"
tilt="76"
range="3270"
heading="-154"></gmp-map-3d>
<div id="ui-container">
<div class="panel">
<div class="control-group">
<label for="heading"
>Heading: <span id="heading-val">0</span>°</label
>
<input
type="range"
id="heading"
name="heading"
min="-180"
max="180"
value="0"
step="1" />
</div>
<div class="control-group">
<label for="tilt"
>Tilt: <span id="tilt-val">45</span>°</label
>
<input
type="range"
id="tilt"
name="tilt"
min="0"
max="90"
value="45"
step="1" />
</div>
<div class="control-group">
<label for="range"
>Range: <span id="range-val">1000</span>m</label
>
<input
type="range"
id="range"
name="range"
min="100"
max="10000"
value="1000"
step="100" />
</div>
<div class="control-group row">
<div class="col">
<label for="lat">Latitude</label>
<input
type="number"
id="lat"
name="lat"
min="-90"
max="90"
value="40.7040"
step="0.0001" />
</div>
<div class="col">
<label for="lng">Longitude</label>
<input
type="number"
id="lng"
name="lng"
min="-180"
max="180"
value="-74.0180"
step="0.0001" />
</div>
</div>
<div class="control-group">
<label for="altitude"
>Altitude: <span id="altitude-val">30</span>m</label
>
<input
type="range"
id="altitude"
name="altitude"
min="0"
max="5000"
value="30"
step="10" />
</div>
<div class="control-group">
<label for="fov"
>FOV: <span id="fov-val">35</span>°</label
>
<input
type="range"
id="fov"
name="fov"
min="5"
max="80"
value="35"
step="1" />
</div>
<div class="control-group">
<label for="roll"
>Roll: <span id="roll-val">0</span>°</label
>
<input
type="range"
id="roll"
name="roll"
min="-180"
max="180"
value="0"
step="1" />
</div>
<div class="status-group">
<div class="code-box">
<pre><code id="generated-code"></code></pre>
</div>
<button id="copy-btn">Copy HTML</button>
</div>
</div>
</div>
</body>
</html>