משבצות 3D ריאליסטיות נמצאות בפורמט glTF הסטנדרטי של OGC, כלומר אפשר להשתמש בכל נגן תצוגה תלת-ממדית שתומך במפרט של משבצות 3D של OGC כדי ליצור את התצוגות החזותיות התלת-ממדיות. לדוגמה, Cesium היא ספרייה בסיסית בקוד פתוח לעיבוד גרפי של תצוגות חזותיות תלת-ממדיות.
עבודה עם CesiumJS
CesiumJS היא ספריית JavaScript בקוד פתוח ליצירת תצוגות חזותיות תלת-ממדיות באינטרנט. מידע נוסף על השימוש ב-CesiumJS זמין במאמר מידע על CesiumJS.
פקדי משתמש
לכלי להצגת המשבצות של CesiumJS יש קבוצה רגילה של אמצעי בקרה למשתמש.
פעולה | תיאור |
---|---|
תצוגת פנורמה | לחיצה גרירה עם לחצן העכבר הימני |
תצוגת זום | לוחצים לחיצה ימנית וגוררים, או גוללים בגלגל העכבר |
סיבוב התצוגה | Ctrl + לחיצה ימינה/שמאלה וגרירה, או לחיצה בלחצן האמצעי וגרירה |
שיטות מומלצות
יש כמה דרכים שאפשר להשתמש בהן כדי לקצר את זמני הטעינה של מודלים תלת-ממדיים ב-CesiumJS. לדוגמה:
כדי להפעיל בקשות בו-זמנית, מוסיפים את ההצהרה הבאה ל-HTML שעבר עיבוד:
Cesium.RequestScheduler.requestsByServer["tile.googleapis.com:443"] = <REQUEST_COUNT>
ככל ש-
REQUEST_COUNT
גבוה יותר, כך האריחים נטענים מהר יותר. עם זאת, כשמטענים בדפדפן Chrome עם הערך שלREQUEST_COUNT
גדול מ-10 והמטמון מושבת, יכול להיות שתתקלו בבעיה ידועה ב-Chrome. ברוב תרחישי השימוש, מומלץ להגדיר את הערך שלREQUEST_COUNT
ל-18 כדי לקבל ביצועים אופטימליים.מאפשרים לדלג על רמות של פרטים. מידע נוסף זמין בבעיה הזו בנושא Cesium.
כדי להציג את השיוך של הנתונים בצורה נכונה, צריך להפעיל את האפשרות showCreditsOnScreen: true
. מידע נוסף זמין בקטע מדיניות.
מדדי עיבוד
כדי למצוא את קצב הפריימים, בודקים כמה פעמים בשנייה מתבצעת קריאה ל-method requestAnimationFrame.
כדי לראות איך מחושב זמן האחזור של המסגרת, אפשר לעיין בכיתה PerformanceDisplay.
דוגמאות לכלי לרינדור של CesiumJS
כדי להשתמש במעבד הגרפיקה של CesiumJS עם משבצות 3D של Map Tiles API, פשוט מספקים את כתובת ה-URL של קבוצת המשבצות ברמה הבסיסית.
דוגמה פשוטה
בדוגמה הבאה מתבצעת אתחול של המרתח (renderer) של CesiumJS, ולאחר מכן נטען סט האריחים ברמה הבסיסית.
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>CesiumJS 3D Tiles Simple Demo</title>
<script src="https://ajax.googleapis.com/ajax/libs/cesiumjs/1.105/Build/Cesium/Cesium.js"></script>
<link href="https://ajax.googleapis.com/ajax/libs/cesiumjs/1.105/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
</head>
<body>
<div id="cesiumContainer"></div>
<script>
// Enable simultaneous requests.
Cesium.RequestScheduler.requestsByServer["tile.googleapis.com:443"] = 18;
// Create the viewer.
const viewer = new Cesium.Viewer('cesiumContainer', {
imageryProvider: false,
baseLayerPicker: false,
geocoder: false,
globe: false,
// https://cesium.com/blog/2018/01/24/cesium-scene-rendering-performance/#enabling-request-render-mode
requestRenderMode: true,
});
// Add 3D Tiles tileset.
const tileset = viewer.scene.primitives.add(new Cesium.Cesium3DTileset({
url: "https://tile.googleapis.com/v1/3dtiles/root.json?key=YOUR_API_KEY",
// This property is needed to appropriately display attributions
// as required.
showCreditsOnScreen: true,
}));
</script>
</body>
מידע על requestRenderMode
זמין במאמר הפעלת מצב עיבוד הבקשה.
דף ה-HTML מוצג כפי שמוצג כאן.
שילוב עם Places API
אפשר להשתמש ב-CesiumJS עם Places API כדי לאחזר מידע נוסף. אפשר להשתמש בווידג'ט של ההשלמה האוטומטית כדי לעבור למסך התצוגה של מפות Google. בדוגמה הזו נעשה שימוש ב-Places Autocomplete API, שמפעילים לפי ההוראות האלה, וב-Maps JavaScript API, שמפעילים לפי ההוראות האלה.
<!DOCTYPE html>
<head>
<meta charset="utf-8" />
<title>CesiumJS 3D Tiles Places API Integration Demo</title>
<script src="https://ajax.googleapis.com/ajax/libs/cesiumjs/1.105/Build/Cesium/Cesium.js"></script>
<link href="https://ajax.googleapis.com/ajax/libs/cesiumjs/1.105/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
</head>
<body>
<label for="pacViewPlace">Go to a place: </label>
<input
type="text"
id="pacViewPlace"
name="pacViewPlace"
placeholder="Enter a location..."
style="width: 300px"
/>
<div id="cesiumContainer"></div>
<script>
// Enable simultaneous requests.
Cesium.RequestScheduler.requestsByServer["tile.googleapis.com:443"] = 18;
// Create the viewer.
const viewer = new Cesium.Viewer("cesiumContainer", {
imageryProvider: false,
baseLayerPicker: false,
requestRenderMode: true,
geocoder: false,
globe: false,
});
// Add 3D Tiles tileset.
const tileset = viewer.scene.primitives.add(
new Cesium.Cesium3DTileset({
url: "https://tile.googleapis.com/v1/3dtiles/root.json?key=YOUR_API_KEY",
// This property is required to display attributions as required.
showCreditsOnScreen: true,
})
);
const zoomToViewport = (viewport) => {
viewer.entities.add({
polyline: {
positions: Cesium.Cartesian3.fromDegreesArray([
viewport.getNorthEast().lng(), viewport.getNorthEast().lat(),
viewport.getSouthWest().lng(), viewport.getNorthEast().lat(),
viewport.getSouthWest().lng(), viewport.getSouthWest().lat(),
viewport.getNorthEast().lng(), viewport.getSouthWest().lat(),
viewport.getNorthEast().lng(), viewport.getNorthEast().lat(),
]),
width: 10,
clampToGround: true,
material: Cesium.Color.RED,
},
});
viewer.flyTo(viewer.entities);
};
function initAutocomplete() {
const autocomplete = new google.maps.places.Autocomplete(
document.getElementById("pacViewPlace"),
{
fields: [
"geometry",
"name",
],
}
);
autocomplete.addListener("place_changed", () => {
viewer.entities.removeAll();
const place = autocomplete.getPlace();
if (!place.geometry || !place.geometry.viewport) {
window.alert("No viewport for input: " + place.name);
return;
}
zoomToViewport(place.geometry.viewport);
});
}
</script>
<script
async=""
src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places&callback=initAutocomplete"
></script>
</body>
תצוגה מסתובבת של רחפן
אתם יכולים לשלוט במצלמה כדי ליצור אנימציה של מעבר בין אריחי המפה. בשילוב עם Places API ו-Elevation API, האנימציה הזו מדמה מעבר של רחפן אינטראקטיבי מעל כל נקודה מעניינת.
באמצעות דוגמת הקוד הזו תוכלו לעוף מסביב למקום שבחרתם בווידג'ט של ההשלמה האוטומטית.
<!DOCTYPE html>
<head>
<meta charset="utf-8" />
<title>CesiumJS 3D Tiles Rotating Drone View Demo</title>
<script src="https://ajax.googleapis.com/ajax/libs/cesiumjs/1.105/Build/Cesium/Cesium.js"></script>
<link href="https://ajax.googleapis.com/ajax/libs/cesiumjs/1.105/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
</head>
<body>
<label for="pacViewPlace">Go to a place: </label>
<input type="text" id="pacViewPlace" name="pacViewPlace" placeholder="Enter a location..." style="width: 300px" />
<div id="cesiumContainer"></div>
<script>
// Enable simultaneous requests.
Cesium.RequestScheduler.requestsByServer["tile.googleapis.com:443"] = 18;
// Create the viewer and remove unneeded options.
const viewer = new Cesium.Viewer("cesiumContainer", {
imageryProvider: false,
baseLayerPicker: false,
homeButton: false,
fullscreenButton: false,
navigationHelpButton: false,
vrButton: false,
sceneModePicker: false,
geocoder: false,
globe: false,
infobox: false,
selectionIndicator: false,
timeline: false,
projectionPicker: false,
clockViewModel: null,
animation: false,
requestRenderMode: true,
});
// Add 3D Tile set.
const tileset = viewer.scene.primitives.add(
new Cesium.Cesium3DTileset({
url: "https://tile.googleapis.com/v1/3dtiles/root.json?key=YOUR_API_KEY",
// This property is required to display attributions.
showCreditsOnScreen: true,
})
);
// Point the camera at a location and elevation, at a viewport-appropriate distance.
function pointCameraAt(location, viewport, elevation) {
const distance = Cesium.Cartesian3.distance(
Cesium.Cartesian3.fromDegrees(
viewport.getSouthWest().lng(), viewport.getSouthWest().lat(), elevation),
Cesium.Cartesian3.fromDegrees(
viewport.getNorthEast().lng(), viewport.getNorthEast().lat(), elevation)
) / 2;
const target = new Cesium.Cartesian3.fromDegrees(location.lng(), location.lat(), elevation);
const pitch = -Math.PI / 4;
const heading = 0;
viewer.camera.lookAt(target, new Cesium.HeadingPitchRange(heading, pitch, distance));
}
// Rotate the camera around a location and elevation, at a viewport-appropriate distance.
let unsubscribe = null;
function rotateCameraAround(location, viewport, elevation) {
if(unsubscribe) unsubscribe();
pointCameraAt(location, viewport, elevation);
unsubscribe = viewer.clock.onTick.addEventListener(() => {
viewer.camera.rotate(Cesium.Cartesian3.UNIT_Z);
});
}
function initAutocomplete() {
const autocomplete = new google.maps.places.Autocomplete(
document.getElementById("pacViewPlace"), {
fields: [
"geometry",
"name",
],
}
);
autocomplete.addListener("place_changed", async () => {
const place = autocomplete.getPlace();
if (!(place.geometry && place.geometry.viewport && place.geometry.location)) {
window.alert(`Insufficient geometry data for place: ${place.name}`);
return;
}
// Get place elevation using the ElevationService.
const elevatorService = new google.maps.ElevationService();
const elevationResponse = await elevatorService.getElevationForLocations({
locations: [place.geometry.location],
});
if(!(elevationResponse.results && elevationResponse.results.length)){
window.alert(`Insufficient elevation data for place: ${place.name}`);
return;
}
const elevation = elevationResponse.results[0].elevation || 10;
rotateCameraAround(
place.geometry.location,
place.geometry.viewport,
elevation
);
});
}
</script>
<script async src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places&callback=initAutocomplete"></script>
</body>
ציור קווים פוליגונים ותוויות
דוגמת הקוד הזו ממחישה איך מוסיפים קווים פוליגונים ותוויות למפה. אפשר להוסיף קווים פוליגונים למפה כדי להציג מסלולי נסיעה ולילך, כדי להציג גבולות נכסים או כדי לחשב את משך הנסיעה וההליכה. אפשר גם לקבל מאפיינים בלי לבצע עיבוד של הסצנה בפועל.
אתם יכולים לקחת את המשתמשים לסיור מודרך בשכונה, או להציג נכסים שכנים שמוצעים כרגע למכירה, ואז להוסיף לזירת התצוגה אובייקטים תלת-ממדיים כמו מודעות חוצות.
אפשר לסכם נסיעה, לרשום את הנכסים שצפיתם בהם ולהציג את הפרטים האלה באובייקטים וירטואליים.
<!DOCTYPE html>
<head>
<meta charset="utf-8" />
<title>CesiumJS 3D Tiles Polyline and Label Demo</title>
<script src="https://ajax.googleapis.com/ajax/libs/cesiumjs/1.105/Build/Cesium/Cesium.js"></script>
<link
href="https://ajax.googleapis.com/ajax/libs/cesiumjs/1.105/Build/Cesium/Widgets/widgets.css"
rel="stylesheet"
/>
</head>
<body>
<div id="cesiumContainer"></div>
<script>
// Enable simultaneous requests.
Cesium.RequestScheduler.requestsByServer["tile.googleapis.com:443"] = 18;
// Create the viewer.
const viewer = new Cesium.Viewer("cesiumContainer", {
imageryProvider: false,
baseLayerPicker: false,
requestRenderMode: true,
geocoder: false,
globe: false,
});
// Add 3D Tiles tileset.
const tileset = viewer.scene.primitives.add(
new Cesium.Cesium3DTileset({
url: "https://tile.googleapis.com/v1/3dtiles/root.json?key=YOUR_API_KEY",
// This property is required to display attributions as required.
showCreditsOnScreen: true,
})
);
// Draws a circle at the position, and a line from the previous position.
const drawPointAndLine = (position, prevPosition) => {
viewer.entities.removeAll();
if (prevPosition) {
viewer.entities.add({
polyline: {
positions: [prevPosition, position],
width: 3,
material: Cesium.Color.WHITE,
clampToGround: true,
classificationType: Cesium.ClassificationType.CESIUM_3D_TILE,
},
});
}
viewer.entities.add({
position: position,
ellipsoid: {
radii: new Cesium.Cartesian3(1, 1, 1),
material: Cesium.Color.RED,
},
});
};
// Compute, draw, and display the position's height relative to the previous position.
var prevPosition;
const processHeights = (newPosition) => {
drawPointAndLine(newPosition, prevPosition);
const newHeight = Cesium.Cartographic.fromCartesian(newPosition).height;
let labelText = "Current altitude (meters above sea level):\n\t" + newHeight;
if (prevPosition) {
const prevHeight =
Cesium.Cartographic.fromCartesian(prevPosition).height;
labelText += "\nHeight from previous point (meters):\n\t" + Math.abs(newHeight - prevHeight);
}
viewer.entities.add({
position: newPosition,
label: {
text: labelText,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
pixelOffset: new Cesium.Cartesian2(0, -10),
showBackground: true,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
}
});
prevPosition = newPosition;
};
const handler = new Cesium.ScreenSpaceEventHandler(viewer.canvas);
handler.setInputAction(function (event) {
const earthPosition = viewer.scene.pickPosition(event.position);
if (Cesium.defined(earthPosition)) {
processHeights(earthPosition);
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
</script>
</body>
מסלול המצלמה
ב-Cesium אפשר להעביר את המצלמה במסלול סביב נקודת עניין, תוך הימנעות ממפגשים עם בניינים. לחלופין, אפשר להפוך את הבניינים לשקופים כשהמצלמה עוברת דרכם.
קודם צריך לנעול את המצלמה בנקודה מסוימת, ואז אפשר ליצור מסלול של המצלמה כדי להציג את הנכס. כדי לעשות זאת, משתמשים בפונקציה lookAtTransform
של המצלמה עם מאזין לאירועים, כפי שמתואר בדוגמת הקוד הזו.
// Lock the camera onto a point.
const center = Cesium.Cartesian3.fromRadians(
2.4213211833389243,
0.6171926869414084,
3626.0426275055174
);
const transform = Cesium.Transforms.eastNorthUpToFixedFrame(center);
viewer.scene.camera.lookAtTransform(
transform,
new Cesium.HeadingPitchRange(0, -Math.PI / 8, 2900)
);
// Orbit around this point.
viewer.clock.onTick.addEventListener(function (clock) {
viewer.scene.camera.rotateRight(0.005);
});
מידע נוסף על שליטה במצלמה זמין במאמר שליטה במצלמה
עבודה עם Cesium ל-Unreal
כדי להשתמש בפלאגין Cesium for Unreal עם 3D Tiles API, פועלים לפי השלבים הבאים.
מתקינים את הפלאגין Cesium for Unreal.
יוצרים פרויקט חדש ב-Unreal.
קישור ל-Google Photorealistic 3D Tiles API.
פותחים את החלון של Cesium על ידי בחירה באפשרות Cesium > Cesium בתפריט.
בוחרים באפשרות Blank 3D Tiles Tileset.
ב-World Outliner, פותחים את החלונית Details על ידי בחירה ב-Cesium3DTileset הזה.
משנים את המקור מ-מ-Cesium Ion ל-מכתובת URL.
מגדירים את כתובת ה-URL ככתובת של Google 3D Tiles.
https://tile.googleapis.com/v1/3dtiles/root.json?key=YOUR_API_KEY
- מפעילים את האפשרות הצגת הקרדיטים במסך כדי להציג את הקרדיטים בצורה תקינה.
הפעולה הזו טוענת את העולם. כדי לעבור לאיזו קואורדינטה שרוצים, בוחרים את הפריט CesiumGeoreference בחלונית Outliner, ואז עורכים את הערכים של Origin Latitude/Longitude/Height בחלונית Details.
עבודה עם Cesium for Unity
כדי להשתמש בתמונות מפורטות במיוחד עם Cesium for Unity, פועלים לפי השלבים הבאים.
יוצרים פרויקט חדש ב-Unity.
מוסיפים רשומת Scoped Registry חדשה בקטע Package Manager (דרך Editor > Project Settings).
שם: אשלגן
כתובת URL: https://unity.pkg.cesium.com
היקפים: com.cesium.unity
מתקינים את החבילה של Cesium for Unity.
התחברות ל-Google Photorealistic 3D Tiles API.
פותחים את החלון של Cesium על ידי בחירה באפשרות Cesium > Cesium בתפריט.
לוחצים על Blank 3D Tiles Tileset.
בחלונית הימנית, באפשרות Tileset Source בקטע Source, בוחרים באפשרות From URL (במקום From Cesium Ion).
מגדירים את כתובת ה-URL ככתובת ה-URL של Google 3D Tiles.
https://tile.googleapis.com/v1/3dtiles/root.json?key=YOUR_API_KEY
- מפעילים את האפשרות הצגת הקרדיטים במסך כדי להציג את הקרדיטים בצורה תקינה.
הפעולה הזו טוענת את העולם. כדי לעבור לאתר כלשהו, בוחרים את הפריט CesiumGeoreference בהיררכיית הסצנה, ואז עורכים את קו הרוחב/קו האורך/הגובה של המקור בכלי לבדיקת השגיאות.
עבודה עם deck.gl
deck.gl, שמבוסס על WebGL, הוא מסגרת JavaScript בקוד פתוח ליצירת תצוגות חזותיות של נתונים ברמה גבוהה ובקנה מידה רחב.
שיוך (Attribution)
כדי להציג את השיוך לנתונים בצורה נכונה, צריך לחלץ את השדה copyright
מ-tiles gltf asset
ולהציג אותו בתצוגה שעבר רינדור. למידע נוסף, קראו את המאמר שיוך נתוני תצוגה.
דוגמאות למעבד גרפיקה של deck.gl
דוגמה פשוטה
בדוגמה הבאה מתבצעת אתחול של המרתח (renderer) של deck.gl, ולאחר מכן נטען מקום ב-3D. בקוד, חשוב להחליף את YOUR_API_KEY במפתח ה-API בפועל.
<!DOCTYPE html>
<html>
<head>
<title>deck.gl Photorealistic 3D Tiles example</title>
<script src="https://unpkg.com/deck.gl@latest/dist.min.js"></script>
<style>
body { margin: 0; padding: 0;}
#map { position: absolute; top: 0;bottom: 0;width: 100%;}
#credits { position: absolute; bottom: 0; right: 0; padding: 2px; font-size: 15px; color: white;
text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;}
</style>
</head>
<body>
<div id="map"></div>
<div id="credits"></div>
<script>
const GOOGLE_API_KEY = YOUR_API_KEY;
const TILESET_URL = `https://tile.googleapis.com/v1/3dtiles/root.json`;
const creditsElement = document.getElementById('credits');
new deck.DeckGL({
container: 'map',
initialViewState: {
latitude: 50.0890,
longitude: 14.4196,
zoom: 16,
bearing: 90,
pitch: 60,
height: 200
},
controller: {minZoom: 8},
layers: [
new deck.Tile3DLayer({
id: 'google-3d-tiles',
data: TILESET_URL,
loadOptions: {
fetch: {
headers: {
'X-GOOG-API-KEY': GOOGLE_API_KEY
}
}
},
onTilesetLoad: tileset3d => {
tileset3d.options.onTraversalComplete = selectedTiles => {
const credits = new Set();
selectedTiles.forEach(tile => {
const {copyright} = tile.content.gltf.asset;
copyright.split(';').forEach(credits.add, credits);
creditsElement.innerHTML = [...credits].join('; ');
});
return selectedTiles;
}
}
})
]
});
</script>
</body>
</html>
הצגה חזותית של שכבות 2D מעל Google Photorealistic 3D Tiles
TerrainExtension של deck.gl מאפשרת להציג נתונים דו-ממדיים על גבי משטח תלת-ממדי. לדוגמה, אפשר להציג את GeoJSON של שטח הבניין מעל הגיאומטריה של האריחים התלת-ממדיים בפורמט Photorealistic.
בדוגמה הבאה, שכבת בניינים מוצגת באופן חזותי באמצעות הפוליגונים שמותאמים לפני השטח של Photorealistic 3D Tiles.
<!DOCTYPE html>
<html>
<head>
<title>Google 3D tiles example</title>
<script src="https://unpkg.com/deck.gl@latest/dist.min.js"></script>
<style>
body { margin: 0; padding: 0;}
#map { position: absolute; top: 0;bottom: 0;width: 100%;}
#credits { position: absolute; bottom: 0; right: 0; padding: 2px; font-size: 15px; color: white;
text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;}
</style>
</head>
<body>
<div id="map"></div>
<div id="credits"></div>
<script>
const GOOGLE_API_KEY = YOUR_API_KEY;
const TILESET_URL = `https://tile.googleapis.com/v1/3dtiles/root.json`;
const BUILDINGS_URL = 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/google-3d-tiles/buildings.geojson'
const creditsElement = document.getElementById('credits');
const deckgl = new deck.DeckGL({
container: 'map',
initialViewState: {
latitude: 50.0890,
longitude: 14.4196,
zoom: 16,
bearing: 90,
pitch: 60,
height: 200
},
controller: true,
layers: [
new deck.Tile3DLayer({
id: 'google-3d-tiles',
data: TILESET_URL,
loadOptions: {
fetch: {
headers: {
'X-GOOG-API-KEY': GOOGLE_API_KEY
}
}
},
onTilesetLoad: tileset3d => {
tileset3d.options.onTraversalComplete = selectedTiles => {
const credits = new Set();
selectedTiles.forEach(tile => {
const {copyright} = tile.content.gltf.asset;
copyright.split(';').forEach(credits.add, credits);
creditsElement.innerHTML = [...credits].join('; ');
});
return selectedTiles;
}
},
operation: 'terrain+draw'
}),
new deck.GeoJsonLayer({
id: 'buildings',
// This dataset is created by CARTO, using other Open Datasets available. More info at: https://3dtiles.carto.com/#about.
data: 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/google-3d-tiles/buildings.geojson',
stroked: false,
filled: true,
getFillColor: ({properties}) => {
const {tpp} = properties;
// quantiles break
if (tpp < 0.6249)
return [254, 246, 181]
else if (tpp < 0.6780)
return [255, 194, 133]
else if (tpp < 0.8594)
return [250, 138, 118]
return [225, 83, 131]
},
opacity: 0.2,
extensions: [new deck._TerrainExtension()]
})
]
});
</script>
</body>
</html>