It's the 15th anniversary of Google Maps Platform - Check out the latest news and announcements

Map Puzzle

More complex demo, showing a game with draggable polygons. See the draggable polygon demo for a simple demo with draggable polygons.

TypeScript

interface Country {
  bounds: number[][];
  name: string;
  start: string[];
  end: string[];
}

class PuzzleDemo {
  private map_: google.maps.Map;
  private polys_: google.maps.Polygon[] = [];
  private difficulty_ = "Easy";
  private count_ = 0;
  private pieceDiv_: HTMLElement;
  private timeDiv_: HTMLElement;
  private dataLoaded_ = false;
  private NUM_PIECES_ = 10;
  private countries_: Country[] = [];
  private timer_ = 0;
  private START_COLOR_ = "#3c79de";
  private END_COLOR_ = "#037e29";

  constructor(map: google.maps.Map) {
    this.map_ = map;
    this.pieceDiv_ = document.createElement("div");
    this.timeDiv_ = document.createElement("div");
    this.createMenu_();
    this.setDifficultyStyle_();
    this.loadData_();
  }

  private createMenu_() {
    const menuDiv = document.createElement("div");
    menuDiv.style.cssText =
      "margin: 40px 10px; border-radius: 8px; height: 320px; width: 180px;" +
      "background-color: white; font-size: 14px; font-family: Roboto;" +
      "text-align: center; color: grey;line-height: 32px; overflow: hidden";
    const titleDiv = document.createElement("div");
    titleDiv.style.cssText =
      "width: 100%; background-color: #4285f4; color: white; font-size: 20px;" +
      "line-height: 40px;margin-bottom: 24px";
    titleDiv.innerText = "Game Options";
    const pieceTitleDiv = document.createElement("div");
    pieceTitleDiv.innerText = "PIECE:";
    pieceTitleDiv.style.fontWeight = "800";

    const pieceDiv = this.pieceDiv_;
    pieceDiv.innerText = "0 / " + this.NUM_PIECES_;
    const timeTitleDiv = document.createElement("div");
    timeTitleDiv.innerText = "TIME:";
    timeTitleDiv.style.fontWeight = "800";

    const timeDiv = this.timeDiv_;
    timeDiv.innerText = "0.0 seconds";
    const difficultyTitleDiv = document.createElement("div");
    difficultyTitleDiv.innerText = "DIFFICULTY:";
    difficultyTitleDiv.style.fontWeight = "800";
    const difficultySelect = document.createElement("select");
    ["Easy", "Moderate", "Hard", "Extreme"].forEach(level => {
      const option = document.createElement("option");
      option.value = level.toLowerCase();
      option.innerText = level;
      difficultySelect.appendChild(option);
    });
    difficultySelect.style.cssText =
      "border: 2px solid lightgrey; background-color: white; color: #4275f4;" +
      "padding: 6px;";

    difficultySelect.onchange = () => {
      this.setDifficulty_(difficultySelect.value);
      this.resetGame_();
    };
    const resetDiv = document.createElement("div");
    resetDiv.innerText = "Reset";
    resetDiv.style.cssText =
      "cursor: pointer; border-top: 1px solid lightgrey; margin-top: 18px;" +
      "color: #4275f4; line-height: 40px; font-weight: 800";
    resetDiv.onclick = this.resetGame_.bind(this);
    menuDiv.appendChild(titleDiv);
    menuDiv.appendChild(pieceTitleDiv);
    menuDiv.appendChild(pieceDiv);
    menuDiv.appendChild(timeTitleDiv);
    menuDiv.appendChild(timeDiv);
    menuDiv.appendChild(difficultyTitleDiv);
    menuDiv.appendChild(difficultySelect);
    menuDiv.appendChild(resetDiv);
    this.map_.controls[google.maps.ControlPosition.TOP_LEFT].push(menuDiv);
  }

  render() {
    if (!this.dataLoaded_) {
      return;
    }
    this.start_();
  }

  private loadData_() {
    const xmlhttpRequest = new XMLHttpRequest();

    xmlhttpRequest.onreadystatechange = () => {
      if (
        xmlhttpRequest.status != 200 ||
        xmlhttpRequest.readyState != XMLHttpRequest.DONE
      )
        return;
      this.loadDataComplete_(JSON.parse(xmlhttpRequest.responseText));
    };
    xmlhttpRequest.open(
      "GET",
      "https://storage.googleapis.com/mapsdevsite/json/puzzle.json",
      true
    );
    xmlhttpRequest.send(null);
  }

  private loadDataComplete_(data: Country[]) {
    this.dataLoaded_ = true;
    this.countries_ = data;
    this.start_();
  }

  /**
   * @param {string} difficulty
   * @private
   */
  private setDifficulty_(difficulty: string) {
    this.difficulty_ = difficulty;

    if (this.map_) {
      this.setDifficultyStyle_();
    }
  }

  private setDifficultyStyle_() {
    const styles = {
      easy: [
        {
          stylers: [{ visibility: "off" }]
        },
        {
          featureType: "water",
          stylers: [{ visibility: "on" }, { color: "#d4d4d4" }]
        },
        {
          featureType: "landscape",
          stylers: [{ visibility: "on" }, { color: "#e5e3df" }]
        },
        {
          featureType: "administrative.country",
          elementType: "labels",
          stylers: [{ visibility: "on" }]
        },
        {
          featureType: "administrative.country",
          elementType: "geometry",
          stylers: [{ visibility: "on" }, { weight: 1.3 }]
        }
      ],
      moderate: [
        {
          stylers: [{ visibility: "off" }]
        },
        {
          featureType: "water",
          stylers: [{ visibility: "on" }, { color: "#d4d4d4" }]
        },
        {
          featureType: "landscape",
          stylers: [{ visibility: "on" }, { color: "#e5e3df" }]
        },
        {
          featureType: "administrative.country",
          elementType: "labels",
          stylers: [{ visibility: "on" }]
        }
      ],
      hard: [
        {
          stylers: [{ visibility: "off" }]
        },
        {
          featureType: "water",
          stylers: [{ visibility: "on" }, { color: "#d4d4d4" }]
        },
        {
          featureType: "landscape",
          stylers: [{ visibility: "on" }, { color: "#e5e3df" }]
        }
      ],
      extreme: [
        {
          elementType: "geometry",
          stylers: [{ visibility: "off" }]
        }
      ]
    };

    this.map_.set("styles", styles[this.difficulty_]);
  }

  private resetGame_() {
    this.removeCountries_();
    this.count_ = 0;
    this.setCount_();
    this.startClock_();

    this.addRandomCountries_();
  }

  private setCount_() {
    this.pieceDiv_.innerText = this.count_ + " / " + this.NUM_PIECES_;

    if (this.count_ == this.NUM_PIECES_) {
      this.stopClock_();
    }
  }

  private stopClock_() {
    window.clearInterval(this.timer_);
  }

  private startClock_() {
    this.stopClock_();

    const timeDiv = this.timeDiv_;

    if (timeDiv) timeDiv.textContent = "0.0 seconds";
    const t = new Date();

    this.timer_ = window.setInterval(() => {
      const diff = new Date().getTime() - t.getTime();

      if (timeDiv) timeDiv.textContent = (diff / 1000).toFixed(2) + " seconds";
    }, 100);
  }

  private addRandomCountries_() {
    // Shuffle countries
    this.countries_.sort(() => {
      return Math.round(Math.random()) - 0.5;
    });

    const countries = this.countries_.slice(0, this.NUM_PIECES_);

    for (let i = 0, country; (country = countries[i]); i++) {
      this.addCountry_(country);
    }
  }

  private addCountry_(country: Country) {
    const options = {
      strokeColor: this.START_COLOR_,
      strokeOpacity: 0.8,
      strokeWeight: 2,
      fillColor: this.START_COLOR_,
      fillOpacity: 0.35,
      geodesic: true,
      map: this.map_,
      draggable: true,
      zIndex: 2,
      paths: country.start.map(google.maps.geometry.encoding.decodePath)
    };

    const poly = new google.maps.Polygon(options);
    google.maps.event.addListener(poly, "dragend", () => {
      this.checkPosition_(poly, country);
    });

    this.polys_.push(poly);
  }

  /**
   * Checks that every point in the polygon is inside the bounds.
   */
  private boundsContainsPoly_(
    bounds: number[][],
    poly: google.maps.Polygon
  ): boolean {
    const b = new google.maps.LatLngBounds(
      new google.maps.LatLng(bounds[0][0], bounds[0][1]),
      new google.maps.LatLng(bounds[1][0], bounds[1][1])
    );
    const paths = poly.getPaths().getArray();

    for (let i = 0; i < paths.length; i++) {
      const p = paths[i].getArray();

      for (let j = 0; j < p.length; j++) {
        if (!b.contains(p[j])) {
          return false;
        }
      }
    }
    return true;
  }

  /**
   * Replace a poly with the correct 'end' position of the country.
   */
  private replacePiece_(poly: google.maps.Polygon, country: Country) {
    const options = {
      strokeColor: this.END_COLOR_,
      fillColor: this.END_COLOR_,
      draggable: false,
      zIndex: 1,
      paths: country.end.map(google.maps.geometry.encoding.decodePath)
    };

    poly.setOptions(options);
    this.count_++;
    this.setCount_();
  }

  private checkPosition_(poly: google.maps.Polygon, country: Country) {
    if (this.boundsContainsPoly_(country.bounds, poly)) {
      this.replacePiece_(poly, country);
    }
  }

  private start_() {
    this.setDifficultyStyle_();
    this.resetGame_();
  }

  private removeCountries_() {
    for (let i = 0, poly; (poly = this.polys_[i]); i++) {
      poly.setMap(null);
    }

    this.polys_ = [];
  }
}

function initMap(): void {
  const map = new google.maps.Map(
    document.getElementById("map") as HTMLElement,
    {
      disableDefaultUI: true,
      center: { lat: 10, lng: 60 },
      zoom: 2
    }
  );

  new PuzzleDemo(map);
}

JavaScript

class PuzzleDemo {
  constructor(map) {
    this.polys_ = [];
    this.difficulty_ = "Easy";
    this.count_ = 0;
    this.dataLoaded_ = false;
    this.NUM_PIECES_ = 10;
    this.countries_ = [];
    this.timer_ = 0;
    this.START_COLOR_ = "#3c79de";
    this.END_COLOR_ = "#037e29";
    this.map_ = map;
    this.pieceDiv_ = document.createElement("div");
    this.timeDiv_ = document.createElement("div");
    this.createMenu_();
    this.setDifficultyStyle_();
    this.loadData_();
  }
  createMenu_() {
    const menuDiv = document.createElement("div");
    menuDiv.style.cssText =
      "margin: 40px 10px; border-radius: 8px; height: 320px; width: 180px;" +
      "background-color: white; font-size: 14px; font-family: Roboto;" +
      "text-align: center; color: grey;line-height: 32px; overflow: hidden";
    const titleDiv = document.createElement("div");
    titleDiv.style.cssText =
      "width: 100%; background-color: #4285f4; color: white; font-size: 20px;" +
      "line-height: 40px;margin-bottom: 24px";
    titleDiv.innerText = "Game Options";
    const pieceTitleDiv = document.createElement("div");
    pieceTitleDiv.innerText = "PIECE:";
    pieceTitleDiv.style.fontWeight = "800";
    const pieceDiv = this.pieceDiv_;
    pieceDiv.innerText = "0 / " + this.NUM_PIECES_;
    const timeTitleDiv = document.createElement("div");
    timeTitleDiv.innerText = "TIME:";
    timeTitleDiv.style.fontWeight = "800";
    const timeDiv = this.timeDiv_;
    timeDiv.innerText = "0.0 seconds";
    const difficultyTitleDiv = document.createElement("div");
    difficultyTitleDiv.innerText = "DIFFICULTY:";
    difficultyTitleDiv.style.fontWeight = "800";
    const difficultySelect = document.createElement("select");
    ["Easy", "Moderate", "Hard", "Extreme"].forEach(level => {
      const option = document.createElement("option");
      option.value = level.toLowerCase();
      option.innerText = level;
      difficultySelect.appendChild(option);
    });
    difficultySelect.style.cssText =
      "border: 2px solid lightgrey; background-color: white; color: #4275f4;" +
      "padding: 6px;";

    difficultySelect.onchange = () => {
      this.setDifficulty_(difficultySelect.value);
      this.resetGame_();
    };
    const resetDiv = document.createElement("div");
    resetDiv.innerText = "Reset";
    resetDiv.style.cssText =
      "cursor: pointer; border-top: 1px solid lightgrey; margin-top: 18px;" +
      "color: #4275f4; line-height: 40px; font-weight: 800";
    resetDiv.onclick = this.resetGame_.bind(this);
    menuDiv.appendChild(titleDiv);
    menuDiv.appendChild(pieceTitleDiv);
    menuDiv.appendChild(pieceDiv);
    menuDiv.appendChild(timeTitleDiv);
    menuDiv.appendChild(timeDiv);
    menuDiv.appendChild(difficultyTitleDiv);
    menuDiv.appendChild(difficultySelect);
    menuDiv.appendChild(resetDiv);
    this.map_.controls[google.maps.ControlPosition.TOP_LEFT].push(menuDiv);
  }
  render() {
    if (!this.dataLoaded_) {
      return;
    }
    this.start_();
  }
  loadData_() {
    const xmlhttpRequest = new XMLHttpRequest();

    xmlhttpRequest.onreadystatechange = () => {
      if (
        xmlhttpRequest.status != 200 ||
        xmlhttpRequest.readyState != XMLHttpRequest.DONE
      )
        return;
      this.loadDataComplete_(JSON.parse(xmlhttpRequest.responseText));
    };
    xmlhttpRequest.open(
      "GET",
      "https://storage.googleapis.com/mapsdevsite/json/puzzle.json",
      true
    );
    xmlhttpRequest.send(null);
  }
  loadDataComplete_(data) {
    this.dataLoaded_ = true;
    this.countries_ = data;
    this.start_();
  }
  /**
   * @param {string} difficulty
   * @private
   */
  setDifficulty_(difficulty) {
    this.difficulty_ = difficulty;

    if (this.map_) {
      this.setDifficultyStyle_();
    }
  }
  setDifficultyStyle_() {
    const styles = {
      easy: [
        {
          stylers: [{ visibility: "off" }]
        },
        {
          featureType: "water",
          stylers: [{ visibility: "on" }, { color: "#d4d4d4" }]
        },
        {
          featureType: "landscape",
          stylers: [{ visibility: "on" }, { color: "#e5e3df" }]
        },
        {
          featureType: "administrative.country",
          elementType: "labels",
          stylers: [{ visibility: "on" }]
        },
        {
          featureType: "administrative.country",
          elementType: "geometry",
          stylers: [{ visibility: "on" }, { weight: 1.3 }]
        }
      ],
      moderate: [
        {
          stylers: [{ visibility: "off" }]
        },
        {
          featureType: "water",
          stylers: [{ visibility: "on" }, { color: "#d4d4d4" }]
        },
        {
          featureType: "landscape",
          stylers: [{ visibility: "on" }, { color: "#e5e3df" }]
        },
        {
          featureType: "administrative.country",
          elementType: "labels",
          stylers: [{ visibility: "on" }]
        }
      ],
      hard: [
        {
          stylers: [{ visibility: "off" }]
        },
        {
          featureType: "water",
          stylers: [{ visibility: "on" }, { color: "#d4d4d4" }]
        },
        {
          featureType: "landscape",
          stylers: [{ visibility: "on" }, { color: "#e5e3df" }]
        }
      ],
      extreme: [
        {
          elementType: "geometry",
          stylers: [{ visibility: "off" }]
        }
      ]
    };
    this.map_.set("styles", styles[this.difficulty_]);
  }
  resetGame_() {
    this.removeCountries_();
    this.count_ = 0;
    this.setCount_();
    this.startClock_();
    this.addRandomCountries_();
  }
  setCount_() {
    this.pieceDiv_.innerText = this.count_ + " / " + this.NUM_PIECES_;

    if (this.count_ == this.NUM_PIECES_) {
      this.stopClock_();
    }
  }
  stopClock_() {
    window.clearInterval(this.timer_);
  }
  startClock_() {
    this.stopClock_();
    const timeDiv = this.timeDiv_;

    if (timeDiv) timeDiv.textContent = "0.0 seconds";
    const t = new Date();
    this.timer_ = window.setInterval(() => {
      const diff = new Date().getTime() - t.getTime();

      if (timeDiv) timeDiv.textContent = (diff / 1000).toFixed(2) + " seconds";
    }, 100);
  }
  addRandomCountries_() {
    // Shuffle countries
    this.countries_.sort(() => {
      return Math.round(Math.random()) - 0.5;
    });
    const countries = this.countries_.slice(0, this.NUM_PIECES_);

    for (let i = 0, country; (country = countries[i]); i++) {
      this.addCountry_(country);
    }
  }
  addCountry_(country) {
    const options = {
      strokeColor: this.START_COLOR_,
      strokeOpacity: 0.8,
      strokeWeight: 2,
      fillColor: this.START_COLOR_,
      fillOpacity: 0.35,
      geodesic: true,
      map: this.map_,
      draggable: true,
      zIndex: 2,
      paths: country.start.map(google.maps.geometry.encoding.decodePath)
    };
    const poly = new google.maps.Polygon(options);
    google.maps.event.addListener(poly, "dragend", () => {
      this.checkPosition_(poly, country);
    });
    this.polys_.push(poly);
  }
  /**
   * Checks that every point in the polygon is inside the bounds.
   */
  boundsContainsPoly_(bounds, poly) {
    const b = new google.maps.LatLngBounds(
      new google.maps.LatLng(bounds[0][0], bounds[0][1]),
      new google.maps.LatLng(bounds[1][0], bounds[1][1])
    );
    const paths = poly.getPaths().getArray();

    for (let i = 0; i < paths.length; i++) {
      const p = paths[i].getArray();

      for (let j = 0; j < p.length; j++) {
        if (!b.contains(p[j])) {
          return false;
        }
      }
    }
    return true;
  }
  /**
   * Replace a poly with the correct 'end' position of the country.
   */
  replacePiece_(poly, country) {
    const options = {
      strokeColor: this.END_COLOR_,
      fillColor: this.END_COLOR_,
      draggable: false,
      zIndex: 1,
      paths: country.end.map(google.maps.geometry.encoding.decodePath)
    };
    poly.setOptions(options);
    this.count_++;
    this.setCount_();
  }
  checkPosition_(poly, country) {
    if (this.boundsContainsPoly_(country.bounds, poly)) {
      this.replacePiece_(poly, country);
    }
  }
  start_() {
    this.setDifficultyStyle_();
    this.resetGame_();
  }
  removeCountries_() {
    for (let i = 0, poly; (poly = this.polys_[i]); i++) {
      poly.setMap(null);
    }
    this.polys_ = [];
  }
}

function initMap() {
  const map = new google.maps.Map(document.getElementById("map"), {
    disableDefaultUI: true,
    center: { lat: 10, lng: 60 },
    zoom: 2
  });
  new PuzzleDemo(map);
}

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

HTML

<!DOCTYPE html>
<html>
  <head>
    <title>Map Puzzle</title>
    <script src="https://polyfill.io/v3/polyfill.min.js?features=default"></script>
    <script
      src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap&libraries=geometry&v=weekly"
      defer
    ></script>
    <link rel="stylesheet" type="text/css" href="./style.css" />
    <script src="./app.js"></script>
  </head>
  <body>
    <div id="map"></div>
  </body>
</html>

All

<!DOCTYPE html>
<html>
  <head>
    <title>Map Puzzle</title>
    <script src="https://polyfill.io/v3/polyfill.min.js?features=default"></script>
    <script
      src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap&libraries=geometry&v=weekly"
      defer
    ></script>
    <style type="text/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;
      }
    </style>
    <script>
      "use strict";

      class PuzzleDemo {
        constructor(map) {
          this.polys_ = [];
          this.difficulty_ = "Easy";
          this.count_ = 0;
          this.dataLoaded_ = false;
          this.NUM_PIECES_ = 10;
          this.countries_ = [];
          this.timer_ = 0;
          this.START_COLOR_ = "#3c79de";
          this.END_COLOR_ = "#037e29";
          this.map_ = map;
          this.pieceDiv_ = document.createElement("div");
          this.timeDiv_ = document.createElement("div");
          this.createMenu_();
          this.setDifficultyStyle_();
          this.loadData_();
        }

        createMenu_() {
          const menuDiv = document.createElement("div");
          menuDiv.style.cssText =
            "margin: 40px 10px; border-radius: 8px; height: 320px; width: 180px;" +
            "background-color: white; font-size: 14px; font-family: Roboto;" +
            "text-align: center; color: grey;line-height: 32px; overflow: hidden";
          const titleDiv = document.createElement("div");
          titleDiv.style.cssText =
            "width: 100%; background-color: #4285f4; color: white; font-size: 20px;" +
            "line-height: 40px;margin-bottom: 24px";
          titleDiv.innerText = "Game Options";
          const pieceTitleDiv = document.createElement("div");
          pieceTitleDiv.innerText = "PIECE:";
          pieceTitleDiv.style.fontWeight = "800";
          const pieceDiv = this.pieceDiv_;
          pieceDiv.innerText = "0 / " + this.NUM_PIECES_;
          const timeTitleDiv = document.createElement("div");
          timeTitleDiv.innerText = "TIME:";
          timeTitleDiv.style.fontWeight = "800";
          const timeDiv = this.timeDiv_;
          timeDiv.innerText = "0.0 seconds";
          const difficultyTitleDiv = document.createElement("div");
          difficultyTitleDiv.innerText = "DIFFICULTY:";
          difficultyTitleDiv.style.fontWeight = "800";
          const difficultySelect = document.createElement("select");
          ["Easy", "Moderate", "Hard", "Extreme"].forEach(level => {
            const option = document.createElement("option");
            option.value = level.toLowerCase();
            option.innerText = level;
            difficultySelect.appendChild(option);
          });
          difficultySelect.style.cssText =
            "border: 2px solid lightgrey; background-color: white; color: #4275f4;" +
            "padding: 6px;";

          difficultySelect.onchange = () => {
            this.setDifficulty_(difficultySelect.value);
            this.resetGame_();
          };

          const resetDiv = document.createElement("div");
          resetDiv.innerText = "Reset";
          resetDiv.style.cssText =
            "cursor: pointer; border-top: 1px solid lightgrey; margin-top: 18px;" +
            "color: #4275f4; line-height: 40px; font-weight: 800";
          resetDiv.onclick = this.resetGame_.bind(this);
          menuDiv.appendChild(titleDiv);
          menuDiv.appendChild(pieceTitleDiv);
          menuDiv.appendChild(pieceDiv);
          menuDiv.appendChild(timeTitleDiv);
          menuDiv.appendChild(timeDiv);
          menuDiv.appendChild(difficultyTitleDiv);
          menuDiv.appendChild(difficultySelect);
          menuDiv.appendChild(resetDiv);
          this.map_.controls[google.maps.ControlPosition.TOP_LEFT].push(
            menuDiv
          );
        }

        render() {
          if (!this.dataLoaded_) {
            return;
          }

          this.start_();
        }

        loadData_() {
          const xmlhttpRequest = new XMLHttpRequest();

          xmlhttpRequest.onreadystatechange = () => {
            if (
              xmlhttpRequest.status != 200 ||
              xmlhttpRequest.readyState != XMLHttpRequest.DONE
            )
              return;
            this.loadDataComplete_(JSON.parse(xmlhttpRequest.responseText));
          };

          xmlhttpRequest.open(
            "GET",
            "https://storage.googleapis.com/mapsdevsite/json/puzzle.json",
            true
          );
          xmlhttpRequest.send(null);
        }

        loadDataComplete_(data) {
          this.dataLoaded_ = true;
          this.countries_ = data;
          this.start_();
        }
        /**
         * @param {string} difficulty
         * @private
         */

        setDifficulty_(difficulty) {
          this.difficulty_ = difficulty;

          if (this.map_) {
            this.setDifficultyStyle_();
          }
        }

        setDifficultyStyle_() {
          const styles = {
            easy: [
              {
                stylers: [
                  {
                    visibility: "off"
                  }
                ]
              },
              {
                featureType: "water",
                stylers: [
                  {
                    visibility: "on"
                  },
                  {
                    color: "#d4d4d4"
                  }
                ]
              },
              {
                featureType: "landscape",
                stylers: [
                  {
                    visibility: "on"
                  },
                  {
                    color: "#e5e3df"
                  }
                ]
              },
              {
                featureType: "administrative.country",
                elementType: "labels",
                stylers: [
                  {
                    visibility: "on"
                  }
                ]
              },
              {
                featureType: "administrative.country",
                elementType: "geometry",
                stylers: [
                  {
                    visibility: "on"
                  },
                  {
                    weight: 1.3
                  }
                ]
              }
            ],
            moderate: [
              {
                stylers: [
                  {
                    visibility: "off"
                  }
                ]
              },
              {
                featureType: "water",
                stylers: [
                  {
                    visibility: "on"
                  },
                  {
                    color: "#d4d4d4"
                  }
                ]
              },
              {
                featureType: "landscape",
                stylers: [
                  {
                    visibility: "on"
                  },
                  {
                    color: "#e5e3df"
                  }
                ]
              },
              {
                featureType: "administrative.country",
                elementType: "labels",
                stylers: [
                  {
                    visibility: "on"
                  }
                ]
              }
            ],
            hard: [
              {
                stylers: [
                  {
                    visibility: "off"
                  }
                ]
              },
              {
                featureType: "water",
                stylers: [
                  {
                    visibility: "on"
                  },
                  {
                    color: "#d4d4d4"
                  }
                ]
              },
              {
                featureType: "landscape",
                stylers: [
                  {
                    visibility: "on"
                  },
                  {
                    color: "#e5e3df"
                  }
                ]
              }
            ],
            extreme: [
              {
                elementType: "geometry",
                stylers: [
                  {
                    visibility: "off"
                  }
                ]
              }
            ]
          };
          this.map_.set("styles", styles[this.difficulty_]);
        }

        resetGame_() {
          this.removeCountries_();
          this.count_ = 0;
          this.setCount_();
          this.startClock_();
          this.addRandomCountries_();
        }

        setCount_() {
          this.pieceDiv_.innerText = this.count_ + " / " + this.NUM_PIECES_;

          if (this.count_ == this.NUM_PIECES_) {
            this.stopClock_();
          }
        }

        stopClock_() {
          window.clearInterval(this.timer_);
        }

        startClock_() {
          this.stopClock_();
          const timeDiv = this.timeDiv_;
          if (timeDiv) timeDiv.textContent = "0.0 seconds";
          const t = new Date();
          this.timer_ = window.setInterval(() => {
            const diff = new Date().getTime() - t.getTime();
            if (timeDiv)
              timeDiv.textContent = (diff / 1000).toFixed(2) + " seconds";
          }, 100);
        }

        addRandomCountries_() {
          // Shuffle countries
          this.countries_.sort(() => {
            return Math.round(Math.random()) - 0.5;
          });
          const countries = this.countries_.slice(0, this.NUM_PIECES_);

          for (let i = 0, country; (country = countries[i]); i++) {
            this.addCountry_(country);
          }
        }

        addCountry_(country) {
          const options = {
            strokeColor: this.START_COLOR_,
            strokeOpacity: 0.8,
            strokeWeight: 2,
            fillColor: this.START_COLOR_,
            fillOpacity: 0.35,
            geodesic: true,
            map: this.map_,
            draggable: true,
            zIndex: 2,
            paths: country.start.map(google.maps.geometry.encoding.decodePath)
          };
          const poly = new google.maps.Polygon(options);
          google.maps.event.addListener(poly, "dragend", () => {
            this.checkPosition_(poly, country);
          });
          this.polys_.push(poly);
        }
        /**
         * Checks that every point in the polygon is inside the bounds.
         */

        boundsContainsPoly_(bounds, poly) {
          const b = new google.maps.LatLngBounds(
            new google.maps.LatLng(bounds[0][0], bounds[0][1]),
            new google.maps.LatLng(bounds[1][0], bounds[1][1])
          );
          const paths = poly.getPaths().getArray();

          for (let i = 0; i < paths.length; i++) {
            const p = paths[i].getArray();

            for (let j = 0; j < p.length; j++) {
              if (!b.contains(p[j])) {
                return false;
              }
            }
          }

          return true;
        }
        /**
         * Replace a poly with the correct 'end' position of the country.
         */

        replacePiece_(poly, country) {
          const options = {
            strokeColor: this.END_COLOR_,
            fillColor: this.END_COLOR_,
            draggable: false,
            zIndex: 1,
            paths: country.end.map(google.maps.geometry.encoding.decodePath)
          };
          poly.setOptions(options);
          this.count_++;
          this.setCount_();
        }

        checkPosition_(poly, country) {
          if (this.boundsContainsPoly_(country.bounds, poly)) {
            this.replacePiece_(poly, country);
          }
        }

        start_() {
          this.setDifficultyStyle_();
          this.resetGame_();
        }

        removeCountries_() {
          for (let i = 0, poly; (poly = this.polys_[i]); i++) {
            poly.setMap(null);
          }

          this.polys_ = [];
        }
      }

      function initMap() {
        const map = new google.maps.Map(document.getElementById("map"), {
          disableDefaultUI: true,
          center: {
            lat: 10,
            lng: 60
          },
          zoom: 2
        });
        new PuzzleDemo(map);
      }
    </script>
  </head>
  <body>
    <div id="map"></div>
  </body>
</html>
"use strict"; class PuzzleDemo { constructor(map) { this.polys_ = []; this.difficulty_ = "Easy"; this.count_ = 0; this.dataLoaded_ = false; this.NUM_PIECES_ = 10; this.countries_ = []; this.timer_ = 0; this.START_COLOR_ = "#3c79de"; this.END_COLOR_ = "#037e29"; this.map_ = map; this.pieceDiv_ = document.createElement("div"); this.timeDiv_ = document.createElement("div"); this.createMenu_(); this.setDifficultyStyle_(); this.loadData_(); } createMenu_() { const menuDiv = document.createElement("div"); menuDiv.style.cssText = "margin: 40px 10px; border-radius: 8px; height: 320px; width: 180px;" + "background-color: white; font-size: 14px; font-family: Roboto;" + "text-align: center; color: grey;line-height: 32px; overflow: hidden"; const titleDiv = document.createElement("div"); titleDiv.style.cssText = "width: 100%; background-color: #4285f4; color: white; font-size: 20px;" + "line-height: 40px;margin-bottom: 24px"; titleDiv.innerText = "Game Options"; const pieceTitleDiv = document.createElement("div"); pieceTitleDiv.innerText = "PIECE:"; pieceTitleDiv.style.fontWeight = "800"; const pieceDiv = this.pieceDiv_; pieceDiv.innerText = "0 / " + this.NUM_PIECES_; const timeTitleDiv = document.createElement("div"); timeTitleDiv.innerText = "TIME:"; timeTitleDiv.style.fontWeight = "800"; const timeDiv = this.timeDiv_; timeDiv.innerText = "0.0 seconds"; const difficultyTitleDiv = document.createElement("div"); difficultyTitleDiv.innerText = "DIFFICULTY:"; difficultyTitleDiv.style.fontWeight = "800"; const difficultySelect = document.createElement("select"); ["Easy", "Moderate", "Hard", "Extreme"].forEach(level => { const option = document.createElement("option"); option.value = level.toLowerCase(); option.innerText = level; difficultySelect.appendChild(option); }); difficultySelect.style.cssText = "border: 2px solid lightgrey; background-color: white; color: #4275f4;" + "padding: 6px;"; difficultySelect.onchange = () => { this.setDifficulty_(difficultySelect.value); this.resetGame_(); }; const resetDiv = document.createElement("div"); resetDiv.innerText = "Reset"; resetDiv.style.cssText = "cursor: pointer; border-top: 1px solid lightgrey; margin-top: 18px;" + "color: #4275f4; line-height: 40px; font-weight: 800"; resetDiv.onclick = this.resetGame_.bind(this); menuDiv.appendChild(titleDiv); menuDiv.appendChild(pieceTitleDiv); menuDiv.appendChild(pieceDiv); menuDiv.appendChild(timeTitleDiv); menuDiv.appendChild(timeDiv); menuDiv.appendChild(difficultyTitleDiv); menuDiv.appendChild(difficultySelect); menuDiv.appendChild(resetDiv); this.map_.controls[google.maps.ControlPosition.TOP_LEFT].push(menuDiv); } render() { if (!this.dataLoaded_) { return; } this.start_(); } loadData_() { const xmlhttpRequest = new XMLHttpRequest(); xmlhttpRequest.onreadystatechange = () => { if ( xmlhttpRequest.status != 200 || xmlhttpRequest.readyState != XMLHttpRequest.DONE ) return; this.loadDataComplete_(JSON.parse(xmlhttpRequest.responseText)); }; xmlhttpRequest.open( "GET", "https://storage.googleapis.com/mapsdevsite/json/puzzle.json", true ); xmlhttpRequest.send(null); } loadDataComplete_(data) { this.dataLoaded_ = true; this.countries_ = data; this.start_(); } /** * @param {string} difficulty * @private */ setDifficulty_(difficulty) { this.difficulty_ = difficulty; if (this.map_) { this.setDifficultyStyle_(); } } setDifficultyStyle_() { const styles = { easy: [ { stylers: [ { visibility: "off" } ] }, { featureType: "water", stylers: [ { visibility: "on" }, { color: "#d4d4d4" } ] }, { featureType: "landscape", stylers: [ { visibility: "on" }, { color: "#e5e3df" } ] }, { featureType: "administrative.country", elementType: "labels", stylers: [ { visibility: "on" } ] }, { featureType: "administrative.country", elementType: "geometry", stylers: [ { visibility: "on" }, { weight: 1.3 } ] } ], moderate: [ { stylers: [ { visibility: "off" } ] }, { featureType: "water", stylers: [ { visibility: "on" }, { color: "#d4d4d4" } ] }, { featureType: "landscape", stylers: [ { visibility: "on" }, { color: "#e5e3df" } ] }, { featureType: "administrative.country", elementType: "labels", stylers: [ { visibility: "on" } ] } ], hard: [ { stylers: [ { visibility: "off" } ] }, { featureType: "water", stylers: [ { visibility: "on" }, { color: "#d4d4d4" } ] }, { featureType: "landscape", stylers: [ { visibility: "on" }, { color: "#e5e3df" } ] } ], extreme: [ { elementType: "geometry", stylers: [ { visibility: "off" } ] } ] }; this.map_.set("styles", styles[this.difficulty_]); } resetGame_() { this.removeCountries_(); this.count_ = 0; this.setCount_(); this.startClock_(); this.addRandomCountries_(); } setCount_() { this.pieceDiv_.innerText = this.count_ + " / " + this.NUM_PIECES_; if (this.count_ == this.NUM_PIECES_) { this.stopClock_(); } } stopClock_() { window.clearInterval(this.timer_); } startClock_() { this.stopClock_(); const timeDiv = this.timeDiv_; if (timeDiv) timeDiv.textContent = "0.0 seconds"; const t = new Date(); this.timer_ = window.setInterval(() => { const diff = new Date().getTime() - t.getTime(); if (timeDiv) timeDiv.textContent = (diff / 1000).toFixed(2) + " seconds"; }, 100); } addRandomCountries_() { // Shuffle countries this.countries_.sort(() => { return Math.round(Math.random()) - 0.5; }); const countries = this.countries_.slice(0, this.NUM_PIECES_); for (let i = 0, country; (country = countries[i]); i++) { this.addCountry_(country); } } addCountry_(country) { const options = { strokeColor: this.START_COLOR_, strokeOpacity: 0.8, strokeWeight: 2, fillColor: this.START_COLOR_, fillOpacity: 0.35, geodesic: true, map: this.map_, draggable: true, zIndex: 2, paths: country.start.map(google.maps.geometry.encoding.decodePath) }; const poly = new google.maps.Polygon(options); google.maps.event.addListener(poly, "dragend", () => { this.checkPosition_(poly, country); }); this.polys_.push(poly); } /** * Checks that every point in the polygon is inside the bounds. */ boundsContainsPoly_(bounds, poly) { const b = new google.maps.LatLngBounds( new google.maps.LatLng(bounds[0][0], bounds[0][1]), new google.maps.LatLng(bounds[1][0], bounds[1][1]) ); const paths = poly.getPaths().getArray(); for (let i = 0; i < paths.length; i++) { const p = paths[i].getArray(); for (let j = 0; j < p.length; j++) { if (!b.contains(p[j])) { return false; } } } return true; } /** * Replace a poly with the correct 'end' position of the country. */ replacePiece_(poly, country) { const options = { strokeColor: this.END_COLOR_, fillColor: this.END_COLOR_, draggable: false, zIndex: 1, paths: country.end.map(google.maps.geometry.encoding.decodePath) }; poly.setOptions(options); this.count_++; this.setCount_(); } checkPosition_(poly, country) { if (this.boundsContainsPoly_(country.bounds, poly)) { this.replacePiece_(poly, country); } } start_() { this.setDifficultyStyle_(); this.resetGame_(); } removeCountries_() { for (let i = 0, poly; (poly = this.polys_[i]); i++) { poly.setMap(null); } this.polys_ = []; } } function initMap() { const map = new google.maps.Map(document.getElementById("map"), { disableDefaultUI: true, center: { lat: 10, lng: 60 }, zoom: 2 }); new PuzzleDemo(map); }
/* 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; }
<!DOCTYPE html> <html> <head> <title>Map Puzzle</title> <script src="https://polyfill.io/v3/polyfill.min.js?features=default"></script> <script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyCb1xprYSpXd0q_yDsJ1W2UGhfl9_YGKU0&callback=initMap&libraries=geometry&v=weekly" defer ></script> <!-- jsFiddle will insert css and js --> </head> <body> <div id="map"></div> </body> </html>