תצוגת המפה מתוכננת כמצלמה שמכוונת כלפי מטה אל מישור שטוח. המיקום של המצלמה (ולכן גם הרינדור של המפה) מוגדר על ידי המאפיינים הבאים: latitude, longitude, altitude, heading, tilt, range ו-fov.
יעד המצלמה הוא המיקום של מרכז המפה, שמוגדר כקואורדינטות של קו הרוחב וקו האורך. יש שתי דרכים לציין את המיקום של המצלמה:
- לפי נקודת יעד: משתמשים במאפיין
centerכדי לציין קואורדינטה במפה שאליה המצלמה צריכה להיות מכוונת. השיטה הזו מתאימה במיוחד כשרוצים לוודא שמאפיין של ARIA או אזור מסוימים יהיו מוקד ההתעניינות. - לפי קואורדינטות של המצלמה: משתמשים במאפיין
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>דוגמה לניסיון
שדה ראייה וטווח
בסביבת תלת-ממד, המושג 'שינוי המרחק מהתצוגה' נשלט על ידי שני פרמטרים נפרדים: 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>