I have been searching for way to incorporate a context menu n my Google maps application. Unfortunately many of the examples my search has uncovered are old and many links are no longer valid. This is true of this link Google maps marker custom context menu
I did find an example example that I could get to work but making it usable could get a bit messy: Here is the working html example (with my APIKEY obscured).
<!DOCTYPE html>
<html>
<script src="https://maps.googleapis.com/maps/api/js?key=MYAPIKEY&callback=initialize"></script>
<script type="text/javascript" src="http://code.jquery.com/jquery-1.4.2.min.js"></script>
<script type="text/javascript">
var map;
function initialize() {
var latlng = new google.maps.LatLng(51.47,-0.025956);
var myOptions = {
zoom: 12,
center: latlng,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);
google.maps.event.addListener(map, "rightclick",function(event){showContextMenu(event.latLng);});
google.maps.event.addListener(map, "click",function(event){closeContextMenu();});
}
function getCanvasXY(caurrentLatLng){
var scale = Math.pow(2, map.getZoom());
var nw = new google.maps.LatLng(
map.getBounds().getNorthEast().lat(),
map.getBounds().getSouthWest().lng()
);
var worldCoordinateNW = map.getProjection().fromLatLngToPoint(nw);
var worldCoordinate = map.getProjection().fromLatLngToPoint(caurrentLatLng);
var caurrentLatLngOffset = new google.maps.Point(
Math.floor((worldCoordinate.x - worldCoordinateNW.x) * scale),
Math.floor((worldCoordinate.y - worldCoordinateNW.y) * scale)
);
return caurrentLatLngOffset;
}
function setMenuXY(caurrentLatLng){
var mapWidth = $('#map_canvas').width();
var mapHeight = $('#map_canvas').height();
var menuWidth = $('.contextmenu').width();
var menuHeight = $('.contextmenu').height();
var clickedPosition = getCanvasXY(caurrentLatLng);
var x = clickedPosition.x ;
var y = clickedPosition.y ;
if((mapWidth - x ) < menuWidth)
x = x - menuWidth;
if((mapHeight - y ) < menuHeight)
y = y - menuHeight;
$('.contextmenu').css('left',x );
$('.contextmenu').css('top',y );
};
function showContextMenu(caurrentLatLng ) {
var projection;
var contextmenuDir;
projection = map.getProjection() ;
$('.contextmenu').remove();
contextmenuDir = document.createElement("div");
contextmenuDir.className = 'contextmenu';
contextmenuDir.innerHTML = "<a id='menu1'><div class=context onclick=click1()>menu item 1</div></a><a id='menu2'><div class=context onclick=click2()>menu item 2</div></a>";
$(map.getDiv()).append(contextmenuDir);
setMenuXY(caurrentLatLng);
contextmenuDir.style.visibility = "visible";
}
function closeContextMenu()
{
$('.contextmenu').remove();
}
function click1()
{
alert("click 1");
}
function click2()
{
alert("click 2");
}
$(document).ready(function(){
initialize();
});
</script>
<style type="text/css">
html { height: 100%; }
body { height: 100%; margin: 0; padding: 0 }
#map_canvas{
height: 100%;
}
.contextmenu{
visibility:hidden;
background:#ffffff;
border:1px solid #8888FF;
z-index: 10;
position: relative;
width: 140px;
}
.contextmenu div{
padding-left: 5px
}
</style>
Rightclick below to show context menu.
<div class="formDiv" id="map_canvas"></div>
</html>
Where it gets messy is at the line ‘contextmenuDir.innerHTML =’. Rather than expanding on how to dynamically provide menu items, I decided to search further. I located a project in github that appeared to be what I was looking for. However, the project was a fork of another project that turns out to be no longer valid. Thus, there is no detail about the html requirements. So, I created an html file and downloaded the contextmenu.js file and the rest of the example javascript code and tried it. The example references an apikey.js file which looks like this:
var apikey2 = "..........................."; // API key 3
console.log("api keys loaded");
My test4.htm file:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="initial-scale=1.0" user-scalable="yes" />
<style type="text/css">
html { height: 100%; }
body { height: 100%; margin: 0; padding: 0 }
#map { height: 100%; }
.menu {
background-color: rgb(255, 255, 255);
border: 2px solid rgb(255, 255, 255);
border-radius: 3px;
box-shadow: rgba(0, 0, 0, 0.3) 0px 2px 6px;
cursor: pointer;
font-size: 1rem;
text-align: center;
color: #0d1f49;
width: 20vw;
margin: 2px;
}
</style>
<title>Simple Map</title>
<script src="https://polyfill.io/v3/polyfill.min.js?features=default"></script>
<!-- playground-hide -->
<script src="apikey.js"> </script>
<!--script>
const process = { env: {} };
process.env.GOOGLE_MAPS_API_KEY =
apikey2;
</script-->
<!-- playground-hide-end -->
<!--link rel="stylesheet" type="text/css" href="./style.css" /-->
</head>
<body>
<div id="map"></div>
<div id="menu"></div>
<!-- prettier-ignore -->
<script>(g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})
({key: apikey2, v: "weekly"});</script>
<script src = "./test4.js"></script>
<!--script src="./contextmenu.js"></script-->
</body>
</html>
And the test4.js file which has the javascript code.
var map; //google.maps.Map;
//$(document).ready(function() {
// initMap();
//});
async function initMap() {
const { Map } = await google.maps.importLibrary("maps");
console.log("initMap start");
map = new google.maps.Map(document.getElementById("map"), {
center: { lat: -34.397, lng: 150.644 },
zoom: 8,
});
/*
google.maps.ContextMenu v1.0
A context menu for Google Maps API v3
http://code.martinpearman.co.uk/googlemapsapi/google.maps.ContextMenu/
Copyright Martin Pearman
Last updated 21st November 2011
[email protected]
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
google.maps.ContextMenu = function(map, options, callback) {
options = options || {};
this.setMap(map);
this.classNames_ = options.classNames || {};
this.map_ = map;
this.id = options.id;
this.mapDiv_ = map.getDiv();
this.menuItems_ = options.menuItems || [];
this.pixelOffset = options.pixelOffset || new google.maps.Point(10, -5);
this.callback = callback || null;
this.eventName = options.eventName || 'menu_item_selected';
/**
* [createMenuItem description]
* @param {Object} itemOptions An object with a label (required), a className (optional) and an id (optional)
* @param {Boolean} before when True, the menuitem is prepended to the menu instead of appended.
*/
this.createMenuItem = function(itemOptions, before) {
var self = this;
if (!self.menu_) {
console.log('No menu');
return;
}
itemOptions = itemOptions || {};
var menuItem = document.createElement('div');
menuItem.innerHTML = itemOptions.label;
menuItem.className = itemOptions.className || self.classNames_.menuItem;
menuItem.eventName = itemOptions.eventName || self.eventName;
if (itemOptions.id) {
menuItem.id = itemOptions.id;
}
menuItem.style.cssText = 'cursor:pointer; white-space:nowrap';
menuItem.onclick = function() {
google.maps.event.trigger(self, menuItem.eventName, self.position_, itemOptions.eventName);
};
if (before) {
self.menu_.insertBefore(menuItem, self.menu_.firstChild);
} else if (itemOptions.container_id) {
document.getElementById(itemOptions.container_id).appendChild(menuItem);
} else {
self.menu_.appendChild(menuItem);
}
};
/**
* [createMenuGroup description]
* @param {Boolean} before when True, the menugroup is prepended to the menu instead of appended.
*/
this.createMenuGroup = function(itemOptions, before) {
var self = this;
if (!self.menu_) {
console.log('No menu');
return;
}
itemOptions = itemOptions || {};
var menuGroup = document.createElement('span');
if (itemOptions.id) {
menuGroup.id = itemOptions.id;
}
if (before) {
self.menu_.insertBefore(menuGroup, self.menu_.firstChild);
} else {
self.menu_.appendChild(menuGroup);
}
};
/**
* [createMenuSeparator description]
* @param {Boolean} before when True, the menuitem is prepended to the menu instead of appended.
*/
this.createMenuSeparator = function(itemOptions, before) {
var self = this;
if (!self.menu_) {
console.log('No menu');
return;
}
itemOptions = itemOptions || {};
var menuSeparator = document.createElement('div');
if (self.classNames_.menuSeparator) {
menuSeparator.className = self.classNames_.menuSeparator;
}
if (itemOptions.id) {
menuSeparator.id = itemOptions.id;
}
if (before) {
self.menu_.insertBefore(menuSeparator, self.menu_.firstChild);
} else if (itemOptions.container_id) {
document.getElementById(itemOptions.container_id).appendChild(menuSeparator);
} else {
self.menu_.appendChild(menuSeparator);
}
};
};
google.maps.ContextMenu.prototype = new google.maps.OverlayView();
google.maps.ContextMenu.prototype.draw = function() {
if (this.isVisible_) {
var mapSize = new google.maps.Size(this.mapDiv_.offsetWidth, this.mapDiv_.offsetHeight);
var menuSize = new google.maps.Size(this.menu_.offsetWidth, this.menu_.offsetHeight);
var mousePosition = this.getProjection().fromLatLngToDivPixel(this.position_);
var left = mousePosition.x;
var top = mousePosition.y;
if (mousePosition.x > mapSize.width - menuSize.width - this.pixelOffset.x) {
left = left - menuSize.width - this.pixelOffset.x;
} else {
left += this.pixelOffset.x;
}
if (mousePosition.y > mapSize.height - menuSize.height - this.pixelOffset.y) {
top = top - menuSize.height - this.pixelOffset.y;
} else {
top += this.pixelOffset.y;
}
this.menu_.style.left = left + 'px';
this.menu_.style.top = top + 'px';
}
};
google.maps.ContextMenu.prototype.getVisible = function() {
return this.isVisible_;
};
google.maps.ContextMenu.prototype.hide = function() {
if (this.isVisible_) {
this.menu_.style.display = 'none';
this.isVisible_ = false;
}
};
google.maps.ContextMenu.prototype.onAdd = function() {
var $this = this; // used for closures
var menu = document.createElement('div');
if (this.classNames_.menu) {
menu.className = this.classNames_.menu;
}
if (this.id) {
menu.id = this.id;
}
menu.style.cssText = 'display:none; position:absolute;z-index:250;';
$this.menu_ = menu;
for (var i = 0, j = this.menuItems_.length; i < j; i++) {
if (this.menuItems_[i].label) {
this.createMenuItem(this.menuItems_[i]);
} else {
this.createMenuSeparator();
}
}
menu.onmouseover = function() {
$this.map_.inmenu = true;
//console.log('Mouseover Menu');
};
menu.onmouseout = function() {
$this.map_.inmenu = false;
//console.log('mouseout Menu');
};
//delete this.classNames_;
delete this.menuItems_;
this.isVisible_ = false;
this.position_ = new google.maps.LatLng(0, 0);
google.maps.event.addListener(this.map_, 'click', function(mouseEvent) {
$this.hide();
});
this.getPanes().floatPane.parentNode.parentNode.appendChild(menu);
if (this.callback) this.callback();
};
google.maps.ContextMenu.prototype.onRemove = function() {
this.getPanes().floatPane.appendChild(this.menu);
this.menu_.parentNode.removeChild(this.menu_);
delete this.mapDiv_;
delete this.menu_;
delete this.position_;
};
google.maps.ContextMenu.prototype.show = function(latLng) {
if (!this.isVisible_) {
this.menu_.style.display = 'block';
this.isVisible_ = true;
}
this.position_ = latLng;
this.draw();
};
var menuStyle = {
menu: 'context_menu',
menuSeparator: 'context_menu_separator',
menuItem: 'context_menu_item'
};
var contextMenuOptions = {
id: "map_rightclick",
eventName: "menu_item_selected",
classNames: menuStyle,
menuItems:
[
{label:'option1', id:'menu_option1'},
{label:'option2', id:'menu_option2'},
]
};
var contextMenu = new google.maps.ContextMenu(map, contextMenuOptions, function() {
console.log('optional callback');
});
google.maps.event.addListener(map, 'contextmenu', function(mouseEvent) {
console.log("contextmenu clicked");
contextMenu.show(mouseEvent.latLng);
});
console.log("initMap done");
}
initMap();
//export {};
However, loading the test5.htm file in my browser creates a google maps but no context menu. I presume that it doesn’t work because the html file is missing a div and/or some style info. I think the div sgold have a classname referred to as ‘container_id’ but I don’t now what it is looking for. I need somebody who understands this all to help me figure out what to fix.