自訂疊加層

選取平台: Android iOS JavaScript

簡介

疊加層是指地圖上與經緯度座標連動的物件,因此會隨著您拖曳或縮放地圖而移動。如要瞭解預先定義的疊加層類型,請參閱「在地圖上繪圖」一文。

Maps JavaScript API 提供 OverlayView 類別,可用於建立自訂疊加層。OverlayView 是一種基礎類別,提供建立疊加層時必須導入的多項方法。此外,這個類別也提供一些方法,可在畫面座標和地圖位置之間進行轉譯。

加入自訂疊加層

以下摘要說明建立自訂疊加層的必要步驟:

  • 將自訂疊加層物件的 prototype 設為 google.maps.OverlayView() 的新執行個體。實際上,這會將疊加層類別設為子類別。
  • 建立自訂疊加層的建構函式,然後設定所有初始化參數。
  • 在原型內導入 onAdd() 方法,然後將疊加層附加至地圖。當地圖可供附加疊加層時,系統就會呼叫 OverlayView.onAdd()
  • 在原型內導入 draw() 方法,然後處理物件的視覺效果。初次顯示物件時,系統會呼叫 OverlayView.draw()
  • 建議您一併導入 onRemove() 方法,清除在疊加層內加入的所有元素。

以下將詳述每個步驟。如需完整的有效程式碼範例,請查看程式碼範例

將疊加層設為子類別

下例會使用 OverlayView 建立簡易圖片疊加層。

現在,我們要建立 USGSOverlay 類別的建構函式,然後將傳遞的參數初始化做為新物件的屬性。

TypeScript

/**  * The custom USGSOverlay object contains the USGS image,  * the bounds of the image, and a reference to the map.  */ class USGSOverlay extends google.maps.OverlayView {   private bounds: google.maps.LatLngBounds;   private image: string;   private div?: HTMLElement;    constructor(bounds: google.maps.LatLngBounds, image: string) {     super();      this.bounds = bounds;     this.image = image;   }

JavaScript

/**  * The custom USGSOverlay object contains the USGS image,  * the bounds of the image, and a reference to the map.  */ class USGSOverlay extends google.maps.OverlayView {   bounds;   image;   div;   constructor(bounds, image) {     super();     this.bounds = bounds;     this.image = image;   }

目前無法將這個疊加層附加至疊加層建構函式中的地圖。首先,我們必須確認地圖的所有窗格都能使用,因為這些窗格會指定物件在地圖上顯示的順序。API 會提供一項輔助方法,指出是否已經確認完畢。我們會在下一節說明這項方法。

將疊加層初始化

當疊加層第一次執行個體化且可顯示時,我們必須透過瀏覽器的 DOM 將疊加層附加至地圖。API 會叫用疊加層的 onAdd() 方法,表示已將疊加層加進地圖。為了處理這個方法,我們必須先建立 <div> 來存放圖片,然後加入 <img> 元素並附加至 <div>,再將疊加層附加至地圖的其中一個「窗格」。窗格是指 DOM 樹狀結構中的節點。

MapPanes 類型的窗格會指定地圖上不同圖層的堆疊順序。可用的窗格如下 (按照從下到上堆疊的順序列出):

  • mapPane 是最下方的窗格,位於圖塊上方。這個窗格可能無法接收 DOM 事件 (窗格 0)。
  • overlayLayer 包含折線、多邊形、區域疊加層,以及圖塊疊加層。這個窗格可能無法接收 DOM 事件 (窗格 1)。
  • markerLayer 包含標記。這個窗格可能無法接收 DOM 事件 (窗格 2)。
  • overlayMouseTarget 包含用於接收 DOM 事件的元素。(窗格 3)。
  • floatPane 包含資訊視窗,位於所有地圖疊加層上方 (窗格 4)。

這裡的圖片是「區域疊加層」,因此我們會使用 overlayLayer 窗格。建立窗格後,我們會將物件附加至該窗格做為子項。

TypeScript

/**  * onAdd is called when the map's panes are ready and the overlay has been  * added to the map.  */ onAdd() {   this.div = document.createElement("div");   this.div.style.borderStyle = "none";   this.div.style.borderWidth = "0px";   this.div.style.position = "absolute";    // Create the img element and attach it to the div.   const img = document.createElement("img");    img.src = this.image;   img.style.width = "100%";   img.style.height = "100%";   img.style.position = "absolute";   this.div.appendChild(img);    // Add the element to the "overlayLayer" pane.   const panes = this.getPanes()!;    panes.overlayLayer.appendChild(this.div); }

JavaScript

/**  * onAdd is called when the map's panes are ready and the overlay has been  * added to the map.  */ onAdd() {   this.div = document.createElement("div");   this.div.style.borderStyle = "none";   this.div.style.borderWidth = "0px";   this.div.style.position = "absolute";    // Create the img element and attach it to the div.   const img = document.createElement("img");    img.src = this.image;   img.style.width = "100%";   img.style.height = "100%";   img.style.position = "absolute";   this.div.appendChild(img);    // Add the element to the "overlayLayer" pane.   const panes = this.getPanes();    panes.overlayLayer.appendChild(this.div); }

繪製疊加層

請注意,在上述程式碼中,我們並未叫用任何特殊的視覺效果。每當 API 需要在地圖上繪製疊加層 (包括第一次加入) 時,就會針對疊加層另外叫用 draw() 方法。

因此,我們會實作這個 draw() 方法,使用 getProjection() 擷取疊加層的 MapCanvasProjection,計算錨定物件右上點和左下點時所需的確切座標。這樣一來,我們就可以調整 <div> 的尺寸,使圖片尺寸能符合我們在疊加層建構函式中指定的範圍。

TypeScript

draw() {   // We use the south-west and north-east   // coordinates of the overlay to peg it to the correct position and size.   // To do this, we need to retrieve the projection from the overlay.   const overlayProjection = this.getProjection();    // Retrieve the south-west and north-east coordinates of this overlay   // in LatLngs and convert them to pixel coordinates.   // We'll use these coordinates to resize the div.   const sw = overlayProjection.fromLatLngToDivPixel(     this.bounds.getSouthWest()   )!;   const ne = overlayProjection.fromLatLngToDivPixel(     this.bounds.getNorthEast()   )!;    // Resize the image's div to fit the indicated dimensions.   if (this.div) {     this.div.style.left = sw.x + "px";     this.div.style.top = ne.y + "px";     this.div.style.width = ne.x - sw.x + "px";     this.div.style.height = sw.y - ne.y + "px";   } }

JavaScript

draw() {   // We use the south-west and north-east   // coordinates of the overlay to peg it to the correct position and size.   // To do this, we need to retrieve the projection from the overlay.   const overlayProjection = this.getProjection();   // Retrieve the south-west and north-east coordinates of this overlay   // in LatLngs and convert them to pixel coordinates.   // We'll use these coordinates to resize the div.   const sw = overlayProjection.fromLatLngToDivPixel(     this.bounds.getSouthWest(),   );   const ne = overlayProjection.fromLatLngToDivPixel(     this.bounds.getNorthEast(),   );    // Resize the image's div to fit the indicated dimensions.   if (this.div) {     this.div.style.left = sw.x + "px";     this.div.style.top = ne.y + "px";     this.div.style.width = ne.x - sw.x + "px";     this.div.style.height = sw.y - ne.y + "px";   } }

移除自訂疊加層

我們也會加入 onRemove() 方法,將疊加層從地圖中徹底移除。

TypeScript

/**  * The onRemove() method will be called automatically from the API if  * we ever set the overlay's map property to 'null'.  */ onRemove() {   if (this.div) {     (this.div.parentNode as HTMLElement).removeChild(this.div);     delete this.div;   } }

JavaScript

/**  * The onRemove() method will be called automatically from the API if  * we ever set the overlay's map property to 'null'.  */ onRemove() {   if (this.div) {     this.div.parentNode.removeChild(this.div);     delete this.div;   } }

隱藏及顯示自訂疊加層

如要隱藏或顯示疊加層,而不只是建立或移除,您可以導入自己的 hide()show() 方法,調整疊加層的顯示設定。此外,您也可以將疊加層從地圖的 DOM 中移除,但費用會稍微高一點。請注意,如果之後疊加層又附加至地圖的 DOM,就會再次叫用疊加層的 onAdd() 方法。

下例會將 hide()show() 方法加進疊加層的原型,以切換容器 <div> 的顯示設定。此外,我們也加入 toggleDOM() 方法,這樣就能將疊加層附加至地圖,或從地圖移除。

TypeScript

/**  *  Set the visibility to 'hidden' or 'visible'.  */ hide() {   if (this.div) {     this.div.style.visibility = "hidden";   } }  show() {   if (this.div) {     this.div.style.visibility = "visible";   } }  toggle() {   if (this.div) {     if (this.div.style.visibility === "hidden") {       this.show();     } else {       this.hide();     }   } }  toggleDOM(map: google.maps.Map) {   if (this.getMap()) {     this.setMap(null);   } else {     this.setMap(map);   } }

JavaScript

/**  *  Set the visibility to 'hidden' or 'visible'.  */ hide() {   if (this.div) {     this.div.style.visibility = "hidden";   } } show() {   if (this.div) {     this.div.style.visibility = "visible";   } } toggle() {   if (this.div) {     if (this.div.style.visibility === "hidden") {       this.show();     } else {       this.hide();     }   } } toggleDOM(map) {   if (this.getMap()) {     this.setMap(null);   } else {     this.setMap(map);   } }

加入按鈕控制項

如要觸發 toggletoggleDom 方法,需要在地圖中加入按鈕控制項。

TypeScript

const toggleButton = document.createElement("button");  toggleButton.textContent = "Toggle"; toggleButton.classList.add("custom-map-control-button");  const toggleDOMButton = document.createElement("button");  toggleDOMButton.textContent = "Toggle DOM Attachment"; toggleDOMButton.classList.add("custom-map-control-button");  toggleButton.addEventListener("click", () => {   overlay.toggle(); });  toggleDOMButton.addEventListener("click", () => {   overlay.toggleDOM(map); });  map.controls[google.maps.ControlPosition.TOP_RIGHT].push(toggleDOMButton); map.controls[google.maps.ControlPosition.TOP_RIGHT].push(toggleButton);

JavaScript

const toggleButton = document.createElement("button");  toggleButton.textContent = "Toggle"; toggleButton.classList.add("custom-map-control-button");  const toggleDOMButton = document.createElement("button");  toggleDOMButton.textContent = "Toggle DOM Attachment"; toggleDOMButton.classList.add("custom-map-control-button"); toggleButton.addEventListener("click", () => {   overlay.toggle(); }); toggleDOMButton.addEventListener("click", () => {   overlay.toggleDOM(map); }); map.controls[google.maps.ControlPosition.TOP_RIGHT].push(toggleDOMButton); map.controls[google.maps.ControlPosition.TOP_RIGHT].push(toggleButton);

完整程式碼範例

完整程式碼範例如下:

TypeScript

// This example adds hide() and show() methods to a custom overlay's prototype. // These methods toggle the visibility of the container <div>. // overlay to or from the map.  function initMap(): void {   const map = new google.maps.Map(     document.getElementById("map") as HTMLElement,     {       zoom: 11,       center: { lat: 62.323907, lng: -150.109291 },       mapTypeId: "satellite",     }   );    const bounds = new google.maps.LatLngBounds(     new google.maps.LatLng(62.281819, -150.287132),     new google.maps.LatLng(62.400471, -150.005608)   );    // The photograph is courtesy of the U.S. Geological Survey.   let image = "https://developers.google.com/maps/documentation/javascript/";    image += "examples/full/images/talkeetna.png";    /**    * The custom USGSOverlay object contains the USGS image,    * the bounds of the image, and a reference to the map.    */   class USGSOverlay extends google.maps.OverlayView {     private bounds: google.maps.LatLngBounds;     private image: string;     private div?: HTMLElement;      constructor(bounds: google.maps.LatLngBounds, image: string) {       super();        this.bounds = bounds;       this.image = image;     }      /**      * onAdd is called when the map's panes are ready and the overlay has been      * added to the map.      */     onAdd() {       this.div = document.createElement("div");       this.div.style.borderStyle = "none";       this.div.style.borderWidth = "0px";       this.div.style.position = "absolute";        // Create the img element and attach it to the div.       const img = document.createElement("img");        img.src = this.image;       img.style.width = "100%";       img.style.height = "100%";       img.style.position = "absolute";       this.div.appendChild(img);        // Add the element to the "overlayLayer" pane.       const panes = this.getPanes()!;        panes.overlayLayer.appendChild(this.div);     }      draw() {       // We use the south-west and north-east       // coordinates of the overlay to peg it to the correct position and size.       // To do this, we need to retrieve the projection from the overlay.       const overlayProjection = this.getProjection();        // Retrieve the south-west and north-east coordinates of this overlay       // in LatLngs and convert them to pixel coordinates.       // We'll use these coordinates to resize the div.       const sw = overlayProjection.fromLatLngToDivPixel(         this.bounds.getSouthWest()       )!;       const ne = overlayProjection.fromLatLngToDivPixel(         this.bounds.getNorthEast()       )!;        // Resize the image's div to fit the indicated dimensions.       if (this.div) {         this.div.style.left = sw.x + "px";         this.div.style.top = ne.y + "px";         this.div.style.width = ne.x - sw.x + "px";         this.div.style.height = sw.y - ne.y + "px";       }     }      /**      * The onRemove() method will be called automatically from the API if      * we ever set the overlay's map property to 'null'.      */     onRemove() {       if (this.div) {         (this.div.parentNode as HTMLElement).removeChild(this.div);         delete this.div;       }     }      /**      *  Set the visibility to 'hidden' or 'visible'.      */     hide() {       if (this.div) {         this.div.style.visibility = "hidden";       }     }      show() {       if (this.div) {         this.div.style.visibility = "visible";       }     }      toggle() {       if (this.div) {         if (this.div.style.visibility === "hidden") {           this.show();         } else {           this.hide();         }       }     }      toggleDOM(map: google.maps.Map) {       if (this.getMap()) {         this.setMap(null);       } else {         this.setMap(map);       }     }   }    const overlay: USGSOverlay = new USGSOverlay(bounds, image);    overlay.setMap(map);    const toggleButton = document.createElement("button");    toggleButton.textContent = "Toggle";   toggleButton.classList.add("custom-map-control-button");    const toggleDOMButton = document.createElement("button");    toggleDOMButton.textContent = "Toggle DOM Attachment";   toggleDOMButton.classList.add("custom-map-control-button");    toggleButton.addEventListener("click", () => {     overlay.toggle();   });    toggleDOMButton.addEventListener("click", () => {     overlay.toggleDOM(map);   });    map.controls[google.maps.ControlPosition.TOP_RIGHT].push(toggleDOMButton);   map.controls[google.maps.ControlPosition.TOP_RIGHT].push(toggleButton); }  declare global {   interface Window {     initMap: () => void;   } } window.initMap = initMap;

JavaScript

// This example adds hide() and show() methods to a custom overlay's prototype. // These methods toggle the visibility of the container <div>. // overlay to or from the map. function initMap() {   const map = new google.maps.Map(document.getElementById("map"), {     zoom: 11,     center: { lat: 62.323907, lng: -150.109291 },     mapTypeId: "satellite",   });   const bounds = new google.maps.LatLngBounds(     new google.maps.LatLng(62.281819, -150.287132),     new google.maps.LatLng(62.400471, -150.005608),   );   // The photograph is courtesy of the U.S. Geological Survey.   let image = "https://developers.google.com/maps/documentation/javascript/";    image += "examples/full/images/talkeetna.png";   /**    * The custom USGSOverlay object contains the USGS image,    * the bounds of the image, and a reference to the map.    */   class USGSOverlay extends google.maps.OverlayView {     bounds;     image;     div;     constructor(bounds, image) {       super();       this.bounds = bounds;       this.image = image;     }     /**      * onAdd is called when the map's panes are ready and the overlay has been      * added to the map.      */     onAdd() {       this.div = document.createElement("div");       this.div.style.borderStyle = "none";       this.div.style.borderWidth = "0px";       this.div.style.position = "absolute";        // Create the img element and attach it to the div.       const img = document.createElement("img");        img.src = this.image;       img.style.width = "100%";       img.style.height = "100%";       img.style.position = "absolute";       this.div.appendChild(img);        // Add the element to the "overlayLayer" pane.       const panes = this.getPanes();        panes.overlayLayer.appendChild(this.div);     }     draw() {       // We use the south-west and north-east       // coordinates of the overlay to peg it to the correct position and size.       // To do this, we need to retrieve the projection from the overlay.       const overlayProjection = this.getProjection();       // Retrieve the south-west and north-east coordinates of this overlay       // in LatLngs and convert them to pixel coordinates.       // We'll use these coordinates to resize the div.       const sw = overlayProjection.fromLatLngToDivPixel(         this.bounds.getSouthWest(),       );       const ne = overlayProjection.fromLatLngToDivPixel(         this.bounds.getNorthEast(),       );        // Resize the image's div to fit the indicated dimensions.       if (this.div) {         this.div.style.left = sw.x + "px";         this.div.style.top = ne.y + "px";         this.div.style.width = ne.x - sw.x + "px";         this.div.style.height = sw.y - ne.y + "px";       }     }     /**      * The onRemove() method will be called automatically from the API if      * we ever set the overlay's map property to 'null'.      */     onRemove() {       if (this.div) {         this.div.parentNode.removeChild(this.div);         delete this.div;       }     }     /**      *  Set the visibility to 'hidden' or 'visible'.      */     hide() {       if (this.div) {         this.div.style.visibility = "hidden";       }     }     show() {       if (this.div) {         this.div.style.visibility = "visible";       }     }     toggle() {       if (this.div) {         if (this.div.style.visibility === "hidden") {           this.show();         } else {           this.hide();         }       }     }     toggleDOM(map) {       if (this.getMap()) {         this.setMap(null);       } else {         this.setMap(map);       }     }   }    const overlay = new USGSOverlay(bounds, image);    overlay.setMap(map);    const toggleButton = document.createElement("button");    toggleButton.textContent = "Toggle";   toggleButton.classList.add("custom-map-control-button");    const toggleDOMButton = document.createElement("button");    toggleDOMButton.textContent = "Toggle DOM Attachment";   toggleDOMButton.classList.add("custom-map-control-button");   toggleButton.addEventListener("click", () => {     overlay.toggle();   });   toggleDOMButton.addEventListener("click", () => {     overlay.toggleDOM(map);   });   map.controls[google.maps.ControlPosition.TOP_RIGHT].push(toggleDOMButton);   map.controls[google.maps.ControlPosition.TOP_RIGHT].push(toggleButton); }  window.initMap = initMap;

CSS

/*   * Always set the map height explicitly to define the size of the div element  * that contains the map.   */ #map {   height: 100%; }  /*   * Optional: Makes the sample page fill the window.   */ html, body {   height: 100%;   margin: 0;   padding: 0; }  .custom-map-control-button {   background-color: #fff;   border: 0;   border-radius: 2px;   box-shadow: 0 1px 4px -1px rgba(0, 0, 0, 0.3);   margin: 10px;   padding: 0 0.5em;   font: 400 18px Roboto, Arial, sans-serif;   overflow: hidden;   height: 40px;   cursor: pointer; } .custom-map-control-button:hover {   background: rgb(235, 235, 235); } 

HTML

<html>   <head>     <title>Showing/Hiding Overlays</title>      <link rel="stylesheet" type="text/css" href="./style.css" />     <script type="module" src="./index.js"></script>   </head>   <body>     <div id="map"></div>      <!--        The `defer` attribute causes the script to execute after the full HTML       document has been parsed. For non-blocking uses, avoiding race conditions,       and consistent behavior across browsers, consider loading using Promises. See       https://developers.google.com/maps/documentation/javascript/load-maps-js-api       for more information.       -->     <script       src="https://maps.googleapis.com/maps/api/js?key=AIzaSyB41DRUbKWJHPxaFjMAwdrzWzbVKartNGg&callback=initMap&v=weekly"       defer     ></script>   </body> </html>

測試範例