Praca z renderowaniem płytek 3D

Fotorealistyczne kafelki 3D są w standardowym formacie OGC glTF, co oznacza, że do tworzenia wizualizacji 3D możesz używać dowolnego modułu renderującego, który obsługuje specyfikację OGC dotyczącą kafelków 3D. Na przykład Cesium to podstawowa biblioteka open source do renderowania wizualizacji 3D.

Praca z CesiumJS

CesiumJS to biblioteka JavaScript typu open source do wizualizacji 3D w internecie. Więcej informacji o używaniu CesiumJS znajdziesz w artykule CesiumJS – informacje podstawowe.

Kontrola użytkowników

Wyświetlacz płytek CesiumJS ma standardowy zestaw elementów sterujących.

Działanie Opis
Widok panoramiczny Kliknięcie lewym przyciskiem myszy i przeciągnięcie
Widok powiększony Kliknij prawym przyciskiem i przeciągnij lub przewiń kółko myszy
Obracanie widoku Ctrl + kliknięcie lewym lub prawym przyciskiem myszy i przeciągnij lub kliknięcie środkowym przyciskiem myszy i przeciągnij

Sprawdzone metody

Aby skrócić czas wczytywania 3D w CesiumJS, możesz zastosować kilka metod. Na przykład:

  • Aby umożliwić wysyłanie żądań jednocześnie, dodaj do kodu HTML do renderowania następujące stwierdzenie:

    Cesium.RequestScheduler.requestsByServer["tile.googleapis.com:443"] = <REQUEST_COUNT>
    

    Im wyższa wartość REQUEST_COUNT, tym szybciej wczytują się elementy. Jeśli jednak wczytujesz w Chrome z wartością REQUEST_COUNTwiększą niż 10 i wyłączoną pamięcią podręczną, możesz napotkać znany problem z Chrome. W większości przypadków zalecamy użycie wartości REQUEST_COUNT 18, która zapewnia optymalną wydajność.

  • Włączanie pomijania poziomów szczegółowości. Więcej informacji znajdziesz w tym artykule na temat Cesium.

Aby prawidłowo wyświetlać atrybucję danych, włącz showCreditsOnScreen: true. Więcej informacji znajdziesz w zasadach.

Dane o renderowaniu

Aby określić liczbę klatek na sekundę, sprawdź, ile razy na sekundę wywoływana jest metoda requestAnimationFrame.

Aby dowiedzieć się, jak oblicza się opóźnienie ramki, zapoznaj się z klasą PerformanceDisplay.

Przykłady mechanizmu renderowania CesiumJS

Możesz użyć renderowania CesiumJS z kafelkami 3D interfejsu Map Tiles API, podając tylko główny adres URL zestawu kafelków.

Prosty przykład

Ten przykład inicjuje renderowanie CesiumJS, a potem wczytuje podstawowy zestaw płytek.

<!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>

Informacje o requestRenderMode znajdziesz w artykule Włączanie trybu renderowania żądań.

Strona HTML jest renderowana w ten sposób.

Integracja z Places API

Aby uzyskać więcej informacji, możesz użyć CesiumJS z interfejsem Places API. Za pomocą widżetu autouzupełniania możesz przejść do widoku Miejsca. Ten przykład korzysta z interfejsu Places Autocomplete API, który można włączyć, wypełniając te instrukcje, oraz z interfejsu Maps JavaScript API, który można włączyć, wypełniając te instrukcje.

<!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>

Obracanie widoku z drona

Możesz sterować kamerą, aby animować zestaw płytek. Po połączeniu z Places API i Elevation API ta animacja symuluje interaktywne przeloty drona nad dowolnym punktem zainteresowania.

Ten przykładowy kod przenosi Cię do miejsca wybranego w widżecie autouzupełniania.

<!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>

Rysowanie polilinii i etykiet

Ten przykładowy kod pokazuje, jak dodawać na mapie linie złożone i etykiety. Możesz dodać na mapie linie wielokąta, aby wyświetlić wskazówki dojazdu samochodem i pieszo, pokazać granice nieruchomości lub obliczyć czas potrzebny na przebycie trasy pieszo lub samochodem. Możesz też pobierać atrybuty bez renderowania sceny.

Możesz zabrać użytkowników na wycieczkę po okolicy lub pokazać im sąsiadujące nieruchomości, które są obecnie w sprzedaży, a następnie dodać do sceny obiekty 3D, takie jak billboardy.

Możesz podsumować podróż, podając listę obejrzanych obiektów i wyświetlając te informacje w formie obiektów wirtualnych.

<!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>

Obrót kamery

W Cesium możesz obracać kamerę wokół punktu zainteresowania, unikając kolizji z budynkami. Możesz też sprawić, że budynki będą przezroczyste, gdy kamera będzie się przez nie przemieszczać.

Najpierw zablokuj kamerę na wybranym punkcie, a następnie możesz utworzyć orbitę kamery, aby zaprezentować zasób. Możesz to zrobić, używając funkcji lookAtTransform aparatu z odbiornikami zdarzeń, jak pokazano w tym przykładzie kodu.

// 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);
});

Więcej informacji o sterowaniu kamerą znajdziesz w artykule Sterowanie kamerą.

Praca z Cesium w Unreal

Aby używać wtyczki Cesium for Unreal z interfejsem 3D Tiles API, wykonaj te czynności.

  1. Zainstaluj wtyczkę Cesium for Unreal.

  2. Utwórz nowy projekt Unreal.

  3. Połącz się z interfejsem Google Photorealistic 3D Tiles API.

    1. Otwórz okno Cesium, wybierając w menu Cesium > Cesium.

    2. Wybierz Puste 3D Tiles Tileset.

    3. W Szkicowniku świata otwórz panel Szczegóły, wybierając zbiór Cesium3DTileset.

    4. Zmień ustawienie ŹródłoZ Cesium Ion na Z adresu URL.

    5. Ustaw adres URL jako adres URL Google 3D Tiles.

    https://tile.googleapis.com/v1/3dtiles/root.json?key=YOUR_API_KEY
    
    1. Aby prawidłowo wyświetlać informacje o autorach, włącz opcję Pokaż na ekranie informacje o autorach.
  4. Wczytuje to świat. Aby przejść do dowolnych współrzędnych, w panelu Szkicownik wybierz element CesiumGeoreference, a potem w panelu Szczegóły edytuj wartości Szerokość/długość geograficzna/wysokość punktu wyjścia.

Praca z Cesium for Unity

.

Aby używać fotorealistycznych płytek w Cesium for Unity, wykonaj te czynności.

  1. Utwórz nowy projekt Unity.

  2. Dodaj nowy rejestr ograniczony w sekcji Menedżer pakietów (przez Edytor > Ustawienia projektu).

    • Nazwa: Cesium

    • Adres URL: https://unity.pkg.cesium.com

    • Zakres: com.cesium.unity

  3. Zainstaluj pakiet Cesium for Unity.

  4. Połącz się z interfejsem API fotorealistycznych kafelków 3D Google.

    1. Otwórz okno Cesium, wybierając w menu Cesium > Cesium.

    2. Kliknij Puste zestawy 3D.

    3. W panelu po lewej stronie w sekcji Źródło wybierz opcję Z adresu URL (zamiast opcji Z Cesium Ion).

    4. Ustaw adres URL na adres URL Google 3D Tiles.

    https://tile.googleapis.com/v1/3dtiles/root.json?key=YOUR_API_KEY
    
    1. Aby prawidłowo wyświetlać informacje o autorach, włącz opcję Pokaż na ekranie informacje o autorach.
  5. Wczytuje to świat. Aby przejść do dowolnego punktu LatLng, wybierz element CesiumGeoreference w Hierarchii sceny, a następnie zmień współrzędne geograficzne punktu wyjścia (szerokość/długość geograficzna/wysokość) w Menedżerze.

Praca z deck.gl

deck.gl to oparty na WebGL framework JavaScript typu open source do wydajnych wizualizacji danych na dużą skalę.

Atrybucja

Upewnij się, że poprawnie wyświetlasz atrybuty danych, wyodrębniając pole copyright z plików gltf asset, a następnie wyświetlając je w widoku wyrenderowanego obrazu. Więcej informacji znajdziesz w artykule Atrybucja danych wyświetlania.

Przykłady procesora graficznego deck.gl

Prosty przykład

Ten przykład inicjuje moduł renderowania deck.gl, a potem wczytuje miejsce w 3D. W kodzie zastąp YOUR_API_KEY swoim rzeczywistym kluczem 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>

Wizualizacja warstw 2D na fotorealistycznych kafelkach 3D

Biblioteka deck.gl TerrainExtension renderuje dane 2D na powierzchni 3D. Możesz na przykład nałożyć GeoJSON z konturem budynku na fotorealistyczną geometrię kafelków 3D.

W tym przykładzie warstwa budynków jest wizualizowana za pomocą wielokątów dostosowanych do fotorealistycznej powierzchni kafelków 3D.

<!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>