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) as any); }; 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); } declare global { interface Window { initMap: () => void; } } window.initMap = initMap;
JavaScript
class PuzzleDemo { map_; polys_ = []; difficulty_ = "Easy"; count_ = 0; pieceDiv_; timeDiv_; dataLoaded_ = false; NUM_PIECES_ = 10; countries_ = []; timer_ = 0; START_COLOR_ = "#3c79de"; END_COLOR_ = "#037e29"; constructor(map) { 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); } 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; }
HTML
<html> <head> <title>Map Puzzle</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&libraries=geometry&v=weekly" defer ></script> </body> </html>
Try Sample
Clone Sample
Git and Node.js are required to run this sample locally. Follow these instructions to install Node.js and NPM. The following commands clone, install dependencies and start the sample application.
git clone -b sample-map-puzzle https://github.com/googlemaps/js-samples.git
cd js-samples
npm i
npm start
Other samples can be tried by switching to any branch beginning with sample-SAMPLE_NAME
.
git checkout sample-SAMPLE_NAME
npm i
npm start