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>
<script src="https://polyfill.io/v3/polyfill.min.js?features=default"></script>
<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>