Related
I am using Azure Maps SDK in a Blazor Server app which uses JavaScript interop. The map works well and renders perfectly.
On initial load of the map component the popup displays correctly when clicking a pin on the map. However, when the data source changes via an API call datasource.importDataFromUrl(organisationDataFeed); it correctly brings back the filtered data, but when I click on one of the pins again the popup does not display. The click event handler (for the pin) is being called, but the popup does not open.
(function () {
var map, datasource, popup;
// Global export
window.azuremaps = {
initMap: function (organisationDataFeed) {
// Create an instance of the map control and set some options.
map = new atlas.Map('map', {
// center: [-97, 39],
// center: [18.424095, -33.925000],
center: [26.157981, -29.083937],
zoom: 4,
pitch: 50,
style: 'night',
view: 'Auto',
// Add your Azure Maps subscription key to the map SDK. Get an Azure Maps key at https://azure.com/maps
authOptions: {
authType: 'subscriptionKey',
subscriptionKey: ''
}
});
map.controls.add([
new atlas.control.ZoomControl(),
new atlas.control.CompassControl(),
new atlas.control.PitchControl(),
new atlas.control.StyleControl()
], {
position: "top-right"
});
// Wait until the map resources are ready.
map.events.add('ready', function () {
// Create a data source and add it to the map.
datasource = new atlas.source.DataSource(null, {
//Tell the data source to cluster point data.
cluster: true,
//The radius in pixels to cluster points together.
clusterRadius: 45,
// The maximium zoom level in which clustering occurs.
// If you zoom in more than this, all points are rendered as symbols.
clusterMaxZoom: 15
});
map.sources.add(datasource);
//Create a bubble layer for rendering clustered data points.
var clusterBubbleLayer = new atlas.layer.BubbleLayer(datasource, null, {
//Scale the size of the clustered bubble based on the number of points inthe cluster.
radius: [
'step',
['get', 'point_count'],
20, //Default of 20 pixel radius.
100, 30, //If point_count >= 100, radius is 30 pixels.
750, 40 //If point_count >= 750, radius is 40 pixels.
],
//Change the color of the cluster based on the value on the point_cluster property of the cluster.
color: [
'step',
['get', 'point_count'],
'rgba(0,255,0,0.8)', //Default to green.
100, 'rgba(255,255,0,0.8)', //If the point_count >= 100, color is yellow.
750, 'rgba(255,0,0,0.8)' //If the point_count >= 100, color is red.
],
strokeWidth: 0,
filter: ['has', 'point_count'] //Only rendered data points which have a point_count property, which clusters do.
});
//Add a click event to the layer so we can zoom in when a user clicks a cluster.
map.events.add('click', clusterBubbleLayer, clusterClicked);
//Add mouse events to change the mouse cursor when hovering over a cluster.
map.events.add('mouseenter', clusterBubbleLayer, function () {
map.getCanvasContainer().style.cursor = 'pointer';
});
map.events.add('mouseleave', clusterBubbleLayer, function () {
map.getCanvasContainer().style.cursor = 'grab';
});
// Create a layer to render the individual locations.
var individualSymbolLayer = new atlas.layer.SymbolLayer(datasource, null, {
filter: ['!', ['has', 'point_count']], //Filter out clustered points from this layer.
textOptions: {
textField: ['get', 'name'],
color: "#FFFFFF",
offset: [0, -2.2]
},
});
map.events.add('click', individualSymbolLayer, symbolClicked);
//Add the clusterBubbleLayer and two additional layers to the map.
map.layers.add([
clusterBubbleLayer,
// Create a symbol layer to render the count of locations in a cluster.
new atlas.layer.SymbolLayer(datasource, null, {
iconOptions: {
image: 'none' //Hide the icon image.
},
textOptions: {
textField: ['get', 'point_count_abbreviated'],
offset: [0, 0.4]
}
}),
individualSymbolLayer
]);
// Create a popup but leave it closed so we can update it and display it later.
popup = new atlas.Popup({
pixelOffset: [0, -18],
closeButton: true
});
// Retrieve a GeoJSON data set and add it to the data source.
datasource.importDataFromUrl(organisationDataFeed);
});
},
};
function clusterClicked(e) {
if (e && e.shapes && e.shapes.length > 0 && e.shapes[0].properties.cluster) {
// Get the clustered point from the event.
var cluster = e.shapes[0];
// Get the cluster expansion zoom level. This is the zoom level at which the cluster starts to break apart.
datasource.getClusterExpansionZoom(cluster.properties.cluster_id).then(function (zoom) {
//Update the map camera to be centered over the cluster.
map.setCamera({
center: cluster.geometry.coordinates,
zoom: zoom + 2,
type: 'ease',
duration: 200
});
});
}
}
function symbolClicked(e) {
// Make sure the event occured on a point feature.
var popupTemplate = '<div class="card border-success" style="visibility: visible"><div class="card-header" style="visibility: visible">{name}</div><div class="card-body text-success" style="visibility: visible"><h5 class="card-title" style="visibility: visible">{description}</h5><p class="card-text" style="visibility: visible">Contact: {contact}</p><p class="card-text">Web: {website}</p></div></div>';
if (e.shapes && e.shapes.length > 0) {
var content, coordinate;
// Check to see if the first value in the shapes array is a Point Shape.
if (e.shapes[0] instanceof atlas.Shape && e.shapes[0].getType() === 'Point') {
var properties = e.shapes[0].getProperties();
content = popupTemplate.replace(/{name}/g, properties.name).replace(/{description}/g, properties.description).replace(/{contact}/g, properties.contact).replace(/{website}/g, properties.website);
coordinate = e.shapes[0].getCoordinates();
}
else if (e.shapes[0].type === 'Feature' && e.shapes[0].geometry.type === 'Point') {
// Check to see if the feature is a cluster.
if (e.shapes[0].properties.cluster) {
content = '<div style="padding:10px;">Cluster of ' + e.shapes[0].properties.point_count + ' symbols</div>';
} else {
// Feature is likely from a VectorTileSource.
content = popupTemplate.replace(/{name}/g, properties.name).replace(/{description}/g, properties.description).replace(/{contact}/g, properties.contact).replace(/{website}/g, properties.website);
}
coordinate = e.shapes[0].geometry.coordinates;
}
if (content && coordinate) {
// Populate the popupTemplate with data from the clicked point feature.
console.log("JB content");
console.log(content);
console.log("JB coordinate");
console.log(coordinate);
popup.setOptions({
//Update the content of the popup.
content: content,
//Update the position of the popup with the symbols coordinate.
position: coordinate
});
console.log("JB: logging map variable");
console.log(map);
popup.open(map);
}
}
}
})();
content and coordinate are populated with values and evaluate to true, the options are correctly set, but just the last line: popup.open(map); does not work if the data source changes bringing back new data into the map. It works perfectly on initial load.
Any ideas what I could do to get this working? Thanks
A couple of things to try:
Double check the values of coordinate and content. The coordinates should be an array with [longitude,latitude] numbers (make sure they aren't string values of numbers). The content should either be a DOMElement (i.e. div), or a string. If it is anything else, it may not display anything.
Double check the "map" variable is a map the second time around. If the reference is lost for some reason, that function won't work.
It looks like you are create a new popup all the time. Often apps only want to display a single popup at a time. In that case it is more efficient to create a single popup and reuse it as shown in this example: https://azuremapscodesamples.azurewebsites.net/index.html?sample=Reusing%20Popup%20with%20Multiple%20Pins
I am using marker cluster group.
using that I am able to show two markers.
when you click number two which is in red color you will see two markers.
after that when I click one marker I need to zoom in two levels to see the location
I wrote a marker click for that and in that i added zoom and tried to use fitbounds too, but its not zooming in.
we used mapcenter then also it did not work `
can you tell me how to fix it.
providing my code snippet and sandbox below.
https://codesandbox.io/s/20756jrz8p
MarkerClick = e => {
console.log("e----->", e);
this.setState({
viewport: { center: [20, 6], zoom: 7 }
});
//this.refs.mymap.leafletElement.setZoom(8);
//let bounds = this.refs.mymap.leafletElement.fitBounds();
//console.log("bounds----->", bounds);
console.log(
"after setting state zoomlevel bounds showCard--->",
this.state.zoom
);
// this.setState({ zoom: 18 });
//this.setState({ zoomLevel: 7 });
};
The Zoom value you have used in MarkerClick() is lower than the current zoom value (zoom = 8). Hence you are not getting proper zoom. Use Zoom = 14 or 16 and a different set of co-ordinates instead of [20,6].
For Example:
MarkerClick = e => {
...
this.setState({
viewport: { center: [43.39528702235596, 6.294845731267186], zoom: 16 }
});
...
}
I use the following block of JavaScript to try to show a WMS layer. I'm using OpenLayers 2.8.
The map's base layer (Openstreetmap) shows correctly, it zooms to the correct area, the "pyramid" layer is shown in the layer switcher, but no request to its WMS service is ever made (so the fact that the URL, styles and params are dummies shouldn't matter -- it never even attempts to get them).
OpenLayers does try to get a WMS layer once I pan or zoom far enough so that the Gulf of Guinea is in view (but all my data is in the Netherlands). This suggests a projection problem (WGS84's (0, 0) point is there), but I don't understand why OpenLayers doesn't even try to fetch a map layer elsewhere. My data is in EPSG:3857 (Web Mercator) projection.
/*global $, OpenLayers */
(function () {
"use strict";
$(function () {
$(".map").each(function () {
var div = $(this);
var data_bounds = div.attr("data-bounds");
console.log("data_bounds: " + data_bounds);
if (data_bounds !== "") {
var map = new OpenLayers.Map(div.attr("id"), {
projection: "EPSG:3857"});
var extent = JSON.parse(data_bounds);
var bounds = new OpenLayers.Bounds(
extent.minx, extent.miny,
extent.maxx, extent.maxy);
map.addLayer(
new OpenLayers.Layer.OSM(
"OpenStreetMap NL",
"http://tile.openstreetmap.nl/tiles/${z}/${x}/${y}.png",
{buffer: 0}));
map.addLayer(
new OpenLayers.Layer.WMS(
"pyramid", "http://rasterserver.local:5000/wms", {
layers: "test",
styles: "test"
}, {
singleTile: true,
isBaseLayer: false,
displayInLayerSwitcher: true,
units: 'm'
}));
map.addControl(new OpenLayers.Control.LayerSwitcher());
map.zoomToExtent(bounds);
}
});
});
})();
Edit: the 'data_bounds' console print prints (with some added formatting):
data_bounds: {
"minx": 582918.5701295201,
"miny": 6923595.841021758,
"maxx": 821926.9006116659,
"maxy": 7079960.166533174
}
It zooms to the correct region in the north of the Netherlands, so I don't think the problem is there.
Since posting, I found out that if I don't use the OSM layer, and instead use the WMS layer as baselayer, it works. So perhaps there's some incompatibility with a OSM baselayer and a WMS layer added to it? But then I don't get that it does seem to do something near WGS84 (0, 0).
I eventually managed to fix this by giving the map an explicit maxExtent:
var extent = JSON.parse(data_bounds);
var bounds = new OpenLayers.Bounds(
extent.minx, extent.miny,
extent.maxx, extent.maxy);
var map = new OpenLayers.Map(div.attr("id"), {
projection: "EPSG:3857",
maxExtent: bounds
});
Oddly enough this doesn't limit the user's ability to pan and zoom around the world, but it does make the overlay work...
I have a page where given a select to the user he can switch the leaflet map I show.
After a initial leaflet map load, my problem is when i want to refresh the map.
I always get "Map container is already initialized":
The problem line is:
var map = L.map('mapa').setView([lat, lon], 15);
Initially it loads well, but when I select another parameter in the form and want to display the map another time it crashes.
btw, I've tried to destroy and recreate $('#mapa') with jQuery before the second setView() but it shows the same error.
Try map.remove(); before you try to reload the map. This removes the previous map element using Leaflet's library (instead of jquery's).
the best way
map.off();
map.remove();
You should add map.off(), it also works faster, and does not cause problems with the events
Html:
<div id="weathermap"></div>
JavaScript:
function buildMap(lat,lon) {
document.getElementById('weathermap').innerHTML = "<div id='map' style='width: 100%; height: 100%;'></div>";
var osmUrl = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
osmAttribution = 'Map data © OpenStreetMap contributors,' +
' CC-BY-SA',
osmLayer = new L.TileLayer(osmUrl, {maxZoom: 18, attribution: osmAttribution});
var map = new L.Map('map');
map.setView(new L.LatLng(lat,lon), 9 );
map.addLayer(osmLayer);
var validatorsLayer = new OsmJs.Weather.LeafletLayer({lang: 'en'});
map.addLayer(validatorsLayer);
}
I use this:
document.getElementById('weathermap').innerHTML = "<div id='map' style='width: 100%; height: 100%;'></div>";
to reload content of div where render map.
Before initializing map check for is the map is already initiated or not
var container = L.DomUtil.get('map');
if(container != null){
container._leaflet_id = null;
}
Only use this
map.invalidateSize();
https://github.com/Leaflet/Leaflet/issues/690
well, after much seeking i realized it's well documented at http://leafletjs.com/examples/layers-control.html
i've ended not repainting the map, but print it once and repaint the points on each new ajax call, so the problem was how to clean up the old points and print only the new ones. i've ended doing this:
var point = L.marker([new_marker[0], new_marker[1]]).addTo(map).bindPopup('blah blah');
points.push(point);
//points is a temporary array where i store the points for removing them afterwards
so, at each new ajax call, before painting the new points, i do the following:
for (i=0;i<points.length;i++) {
map.removeLayer(points[i]);
}
points=[];
so far, so good :-)
When you just remove a map, it destroys the div id reference, so, after remove() you need to build again the div where the map will be displayed, in order to avoid the "Uncaught Error: Map container not found".
if(map != undefined || map != null){
map.remove();
$("#map").html("");
$("#preMap").empty();
$( "<div id=\"map\" style=\"height: 500px;\"></div>" ).appendTo("#preMap");
}
What you can try is to remove the map before initialising it or when you leave the page:
if(this.map) {
this.map.remove();
}
I had the same problem on angular when switching page. I had to add this code before leaving the page to make it works:
$scope.$on('$locationChangeStart', function( event ) {
if(map != undefined)
{
map.remove();
map = undefined
document.getElementById('mapLayer').innerHTML = "";
}
});
Without document.getElementById('mapLayer').innerHTML = "" the map was not displayed on the next page.
if you want update map view, for example change map center, you don’t have to delete and then recreate the map, you can just update coordinate
const mapInit = () => {
let map.current = w.L.map('map');
L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map.current);
}
const setCoordinate = (gps_lat, gps_long) => {
map.setView([gps_lat, gps_long], 13);
}
initMap();
setCoordinate(50.403723 30.623538);
setTimeout(() => {
setCoordinate(51.505, -0.09);
}, 3000);
You should try to unmount the function in react js to remove the existing map.
const Map = () => {
const mapContainer = useRef();
const [map, setMap] = useState({});
useEffect(()=>{
const map = L.map(mapContainer.current, {attributionControl: false}).setView([51.505, -0.09], 13);
L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', {
maxZoom: 18,
attribution: 'Map',
id: 'mapbox/streets-v11',
tileSize: 512,
zoomOffset: -1
}).addTo(map);
// unmount map function
return () => map.remove();
}, []);
return (
<div style={{padding: 0, margin: 0, width: "100%", height: "100vh",}}
ref={el => mapContainer.current = el}>
</div>
);
}
I had same problem.then i set globally map variable e.g var map= null and then for display map i check
if(map==null)then map=new L.Map('idopenstreet').setView();
By this solution your map will be initialize only first time after that map will be fill by L.Map then it will not be null. so no error will be there like map container already initialize.
For refreshing map in same page you can use below code to create a map on the page
if (!map) {
this.map = new L.map("mapDiv", {
center: [24.7136, 46.6753],
zoom: 5,
renderer: L.canvas(),
attributionControl: true,
});
}
then use below line to refresh the map, but make sure to use same latitude, longitude and zoom options
map.setView([24.7136, 46.6753], 5);
Also, I had the same issue when switching between tabs in the same page using angular 2+, and I was able to fix it by adding below code in Component constructor
var container = L.DomUtil.get('mapDiv');
if (container != null) {
container.outerHTML = ""; // Clear map generated HTML
// container._leaflet_id = null; << didn't work for me
}
use the redrawAll() function rather than renderAll().
We facing this issue today and we solved it. what we do ?
leaflet map load div is below.
<div id="map_container">
<div id="listing_map" class="right_listing"></div>
</div>
When form input change or submit we follow this step below. after leaflet map container removed in my page and create new again.
$( '#map_container' ).html( ' ' ).append( '<div id="listing_map" class="right_listing"></div>' );
After this code my leaflet map is working fine with form filter to reload again.
Thank you.
If you don't globally store your map object reference, I recommend
if (L.DomUtil.get('map-canvas') !== undefined) {
L.DomUtil.get('map-canvas')._leaflet_id = null;
}
where <div id="map-canvas"></div> is the object the map has been drawn into.
This way you avoid recreating the html element, which would happen, were you to remove() it.
For refresh leaflet map you can use this code:
this.map.fitBounds(this.map.getBounds());
I had the same problem on react I solved it by initialized at the top in useEffect
Here is my React Code.
const mapContainerRef = useRef(null);
useEffect( async () => {
const res =await Axios.get(BASE_PATH + 'fetchProperty')
const container = L.DomUtil.get(mapContainerRef.current); if(container != null){ container._leaflet_id = null; }
if(container) {
const mapView = L.map( mapContainerRef.current, {
zoom: 13,
center: [19.059984, 72.889999]
// maxZoom: 13
// minZoom: 15
});
// const canvas = mapView.getCanvasContainer();
mapView.zoomControl.setPosition("bottomright");
mapView.attributionControl.addAttribution(
"<a href='https://mascots.pro'>Mascots. pro</a>"
);
L.tileLayer(
// "https://api.mapbox.com/styles/v1/mapbox/dark-v9/tiles/{z}/{x}/{y}?access_token=" + https://api.mapbox.com/styles/v1/anonymousmw/cko1eb1r20mdu18qqtps8i03p/tiles/{z}/{x}/{y}?access_token=
"https://api.mapbox.com/styles/v1/mapbox/dark-v9/tiles/{z}/{x}/{y}?access_token=" +
access_token,
{
attribution: 'Mascots'
}
).addTo(mapView);
const mask = L.tileLayer.mask(
"https://api.mapbox.com/styles/v1/anonymousmw/cko1eb1r20mdu18qqtps8i03p/tiles/{z}/{x}/{y}?access_token=" +
access_token,
{
attribution: 'Mascots pro',
maskSize: 300
// maxZoom: 18,
// maxNativeZoom: 16
// tms: true
}
)
.addTo(mapView);
mapView.on("mousemove", function (e) {
mask.setCenter(e.containerPoint);
});
res.data.map((marker) => {
const innerHtmlContent = `<div id='popup-container' class='popup-container'> <h3> Property Details</h3>
<div class='popup-label'>Building Name :<p>${marker.Building}</p></div>
<div class='popup-address-label'> Address : <p>${marker.Landmark}, ${marker.Location}</p></div>
<div class='popup-rent-label'>Monthly Rent : <p> ₹ ${marker.Price}</p></div>
</div>`;
const divElement = document.createElement("div");
const assignBtn = document.createElement("div");
assignBtn.className = "map-link";
assignBtn.innerHTML = `<button class="view-btn">View Property</button>`;
divElement.innerHTML = innerHtmlContent;
divElement.appendChild(assignBtn);
assignBtn.addEventListener("click", (e) => {
console.log("dsvsdvb");
});
var iconOptions = {
iconUrl: "/images/location_pin2.svg",
iconSize: [25, 25]
};
var customIcon = L.icon(iconOptions);
// create popup contents
var customPopup = divElement;
// specify popup options
var customOptions = {
maxWidth: "500",
className: "custom"
};
const markerOptions = {
// title: "MyLocation",
// draggable: true
clickable: true,
icon: customIcon
};
const mark = L.marker([marker.Latitude,marker.Longitude], markerOptions);
mark.bindPopup(customPopup, customOptions);
mark.addTo(mapView);
// return mapView.off();
});
return () => mapView.remove();
}
}, [])
return (
<div className="map-box">
<div className="map-container" ref={mapContainerRef}></div>
</div>
);
I went through the same problem, so I created a method inside the map instance to reload it.
var map = L.map('mapa').setView([lat, lon], 15);
map.reload = function(){
map.remove();
map = L.map('mapa').setView([lat, lon], 15);
}
....
map.reload();
I did this in reactjs
// Create map (dev = reuse existing map)
let myMap = L.DomUtil.get('map');
if(myMap == null){
myMap = L.map('mapid').setView(currentLocation, zoom);
}
Html:
<div id='leaflet-map' #leafletMap></div>
JavaScript:
#ViewChild('leafletMap')
private mapElement: ElementRef;
private initMap(): void {
this.map = leaflet.map(this.mapElement.nativeElement, {
center: [39.01860177826393, 35.30274319309024],
zoom: 4,
});
leaflet
.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '',
maxZoom: 18,
})
.addTo(this.map);
}
My hacky implementation to refresh the map was to use:
// Hack to refresh the map by panning by zero
public refreshMap() {
this.map.panBy([0,0]);
}
In case you're working with NextJs and typescrypt, what worked for me was
container._leaflet_id = null;
as someone proposed, but had some typing erros so my approach is
const L = await import('leaflet');
const container = L.DomUtil.get('map');
if (!container) return;
if (container.classList.contains('leaflet-container')) return;
const map = L.map('map', {
center: [19.434817, -99.1268643],
zoom: 18,
});
map.invalidateSize();
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
}).addTo(map);
const marker = L.marker([19.434786638353515, -99.1268643025101]).addTo(
map
);
after this code, (all these code needs to be inside a useEffect hook), leaflet just worked fine.
set
var container = L.DomUtil.get('map');
if (container && container['_leaflet_id'] != null) {
container.remove();
}
before var map = L.map('map')
enjoy :)
For web apps, check these things
I've experienced the error "Map container is already initialized" in multiple contexts (e.g. showing/hiding a map, changing map components or triggering hot-reload during development) and ultimately I had to implement multiple solutions to truly clear this error. In general you want to be conservative.
(A) Use a unique ID for the map container
Even if you have only one map instance, use a dynamic unique id anyways. This way you're ensured that every time the component mounts, the DOM element truly is fresh.
For example, in Vue I do this:
<div :id="id"></div>
data() {
return {
id: 'mapLeaflet-'+Date.now()
}
}
And anywhere I refer to the DOM element, I use this.id:
let mapHTMLContainer = document.getElementById(this.id)
(B) Before map init, check existence of map first
Check for DOM element and whether you already have a map instance set in data.
In Vue, I do this:
methods: {
initMap() {
let mapHTMLContainer = document.getElementById(this.id)
if (mapHTMLContainer
&& mapHTMLContainer.hasChildNodes() == false
&& !this.map) {
// DO STUFF
// e.g. initialize leaflet, set tile layer, add markers, etc
}
}
}
(C) On map init, pass the actual HTML element, not just a string
When initiating Leaflet map, pass the actual HTML element instead of just the string id.
Both methods are officially supported, but I cannot find any information talking about the differences. https://leafletjs.com/reference.html#map-l-map
BEFORE
This works in general and is convenient...
this.map = L.map("mymap")
AFTER
... but, instead try to actually get the element, and pass it. I observed a difference doing this.
let myMapElement = document.getElementById(this.id)
this.map = L.map(myMapElement)
(D) On component "dismount", destroy things
If you have a dismount state, use it and destroy stuff related to your map component. Make sure things don't hang-around.
In Vue, I do this:
beforeDestroy() {
// Destroy any listeners
this.$nuxt.$off('refreshMap') // Nuxt is the framework I use
// Clear the map instance in your component
if (this.map) {
// I haven't fully tested these; are they valid?
this.map.off() // Leaflet function
this.map.remove() // Leaflet function
this.map = null // null it for good measure
}
// Clear out the HTML container of any children, just in case
let mapHTMLElement = document.getElementById(this.id)
if (mapHTMLElement) {
mapHTMLElement.outerHTML = ""
}
}
I am trying to implement a custom infoBubble that has the box opening to the side of a marker rather than the default position of on top. This has turned out to be harder than expected.
Using the normal infoWindow you can use pixelOffset. See here for the documentation
Using infoBubble this does not seem to be the case. Is there anyway of using pixelOffset in an infoBubble, or something that will do the same thing?
I have found this very difficult to search for, as using a google search such as this returns no relevant results Google Search
Below is all my resources I have been using.
Example of infoBubble here.
My JavaScript to setup the map and infoBubble here.
And now my javascript here just in-case the jsfiddle link is broken.
<script type="text/javascript">
$(document).ready(function () {
init();
});
function init() {
//Setup the map
var googleMapOptions = {
center: new google.maps.LatLng(53.5167, -1.1333),
zoom: 13,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
//Start the map
var map = new google.maps.Map(document.getElementById("map_canvas"),
googleMapOptions);
var marker = new google.maps.Marker({
position: new google.maps.LatLng(53.5267, -1.1333),
title: "Just a test"
});
marker.setMap(map);
infoBubble = new InfoBubble({
map: map,
content: '<div class="phoneytext">Some label</div>',
//position: new google.maps.LatLng(-35, 151),
shadowStyle: 1,
padding: '10px',
//backgroundColor: 'rgb(57,57,57)',
borderRadius: 5,
minWidth: 200,
arrowSize: 10,
borderWidth: 1,
borderColor: '#2c2c2c',
disableAutoPan: true,
hideCloseButton: false,
arrowPosition: 7,
backgroundClassName: 'phoney',
pixelOffset: new google.maps.Size(130, 120),
arrowStyle: 2
});
infoBubble.open(map, marker);
}
</script>
Update
To help with answering this question i have put together a test case here. The important lines are lines 38 & 39, which should specify where to position the label.
Update 2
For the bounty to be awarded i need to see an example of the infoBubble positioned away from its default position above the marker. Preferably to the right hand side of the marker.
Update 3
I have removed the testcase from update 1 because it is hosted on my old company's servers.
This is my solution.
In InfoBubble library
replace
entire InfoBubble.prototype.draw method
with
/*
* Sets InfoBubble Position
* */
InfoBubble.prototype.setBubbleOffset = function(xOffset, yOffset) {
this.bubbleOffsetX = parseInt(xOffset);
this.bubbleOffsetY = parseInt(yOffset);
}
/*
* Gets InfoBubble Position
* */
InfoBubble.prototype.getBubbleOffset = function() {
return {
x: this.bubbleOffsetX || 0,
y: this.bubbleOffsetY || 0
}
}
/**
* Draw the InfoBubble
* Implementing the OverlayView interface
*/
InfoBubble.prototype.draw = function() {
var projection = this.getProjection();
if (!projection) {
// The map projection is not ready yet so do nothing
return;
}
var latLng = /** #type {google.maps.LatLng} */ (this.get('position'));
if (!latLng) {
this.close();
return;
}
var tabHeight = 0;
if (this.activeTab_) {
tabHeight = this.activeTab_.offsetHeight;
}
var anchorHeight = this.getAnchorHeight_();
var arrowSize = this.getArrowSize_();
var arrowPosition = this.getArrowPosition_();
arrowPosition = arrowPosition / 100;
var pos = projection.fromLatLngToDivPixel(latLng);
var width = this.contentContainer_.offsetWidth;
var height = this.bubble_.offsetHeight;
if (!width) {
return;
}
// Adjust for the height of the info bubble
var top = pos.y - (height + arrowSize) + this.getBubbleOffset().y;
if (anchorHeight) {
// If there is an anchor then include the height
top -= anchorHeight;
}
var left = pos.x - (width * arrowPosition) + this.getBubbleOffset().x;
this.bubble_.style['top'] = this.px(top);
this.bubble_.style['left'] = this.px(left);
var shadowStyle = parseInt(this.get('shadowStyle'), 10);
switch (shadowStyle) {
case 1:
// Shadow is behind
this.bubbleShadow_.style['top'] = this.px(top + tabHeight - 1);
this.bubbleShadow_.style['left'] = this.px(left);
this.bubbleShadow_.style['width'] = this.px(width);
this.bubbleShadow_.style['height'] =
this.px(this.contentContainer_.offsetHeight - arrowSize);
break;
case 2:
// Shadow is below
width = width * 0.8;
if (anchorHeight) {
this.bubbleShadow_.style['top'] = this.px(pos.y);
} else {
this.bubbleShadow_.style['top'] = this.px(pos.y + arrowSize);
}
this.bubbleShadow_.style['left'] = this.px(pos.x - width * arrowPosition);
this.bubbleShadow_.style['width'] = this.px(width);
this.bubbleShadow_.style['height'] = this.px(2);
break;
}
};
and then you can use this by
var infoBubble = new InfoBubble({
map: map,
content: "My Content",
position: new google.maps.LatLng(1, 1),
shadowStyle: 1,
padding: 0,
backgroundColor: 'transparent',
borderRadius: 7,
arrowSize: 10,
borderWidth: 1,
borderColor: '#2c2c2c',
disableAutoPan: true,
hideCloseButton: true,
arrowPosition: 50,
backgroundClassName: 'infoBubbleBackground',
arrowStyle: 2
});
Then finally we have to use this method setBubbleOffset(x,y); to set InfoBubble position
infoBubble.setBubbleOffset(0,-32);
It seems as though the infoBubble library itself defaults to positioning the bubble above the marker it is bound to. Take a look at the sample file they included in the library: http://code.google.com/p/google-maps-utility-library-v3/source/browse/trunk/infobubble/examples/example.html . Specifically notice from line 99 to line 122 and the use of the two infobubbles. The first one is bound to the marker, however the second one is a stand-alone and thus if you see line 106, you can define a position to it. Now, based on this understanding I've created an example for you in this fiddle http://jsfiddle.net/pDFc3/. The infoBubble is positioned to the right of the marker.
It's strange, because the infoBubble js library has a function for setPosition ( http://code.google.com/p/google-maps-utility-library-v3/source/browse/trunk/infobubble/src/infobubble.js?r=206 ) see Line 1027. But for some reason after I wait for the DOM to load and try to change the position by going infoBubble.setPosition(newLatLng); I doesn't work. On the contrary, declaring infoBubble.getPosition(); after the DOM loads gives me the current position of the marker the infoBubble is bound to. So setPosition() may have a bug in the js library, because I believe it is still being worked on (I could be wrong maybe it's just buggy).
I've fixed my jsFiddle to solve your issue for when zooming in and out, and positioning the infoBubble accordingly ( http://jsfiddle.net/pDFc3/ ). Let me explain the logic first. Firstly, the maximum zoom level on Google Maps for road map type is 21 - this value is inconsistent for satellite imagery but the maximum zoom the user can go to is 21. From 21, each time you zoom out the differences between two points can be kept consistent "on screen" based on the following logic:
consitent_screen_dist = initial_dist * (2 ^ (21 - zoomlevel))
In our case, the reasonable value for initial distance was 0.00003 degrees (between marker and infoBubble). So, based on this logic I added the following piece to find the initial longitudinal distance between marker and infoBubble:
var newlong = marker.getPosition().lng() + (0.00003 * Math.pow(2, (21 - map.getZoom())));
Likewise, to ensure the distance stays consistent on each zoom level change we simply declare a new longitude as we listen for a change in the zoom level:
google.maps.event.addListener(map, "zoom_changed", function() {
newlong = marker.getPosition().lng() + (0.00003 * Math.pow(2, (21 - map.getZoom())));
infoBubble.setPosition(new google.maps.LatLng(marker.getPosition().lat(), newlong));
});
Keep in mind you can make this code much more efficient by declaring variables for marker.getPosition and other values that are called through methods. So that the method calls aren't repeated and slow your code down.
Unfortunately there is no such option as pixelOffset in InfoBubble. But if you just want to move up Bubble above the marker in your example you should not set map parameter at bubble initialization. Consider the following fiddle (i fixed it for you):
http://jsfiddle.net/ssrP9/5/
P.S. Your fiddle didn't work because you hadnt added resources properly
http://doc.jsfiddle.net/basic/introduction.html#add-resources
I've just come across the exact same issue but couldn't find an answer anywhere. Through a little trial and error I worked it out.
You'll be using the Google JS file for "infoBubble". Go into this file and search for...
InfoBubble.prototype.buildDom_ = function() {
For me, this is on line 203 (but that could be the result of previous shuffling and edits).
Within that function you'll see the position "absolute" declaration. On a new line, you can add marginTop, marginRight, marginBottom and marginLeft. This will nudge the bubble from its default position (which is also dependent on the arrow position declaration in your config)...
This is my code tweak in the bubble JS file which positions the bubble over the top of the marker (due to a design feature)...
var bubble = this.bubble_ = document.createElement('DIV');
bubble.style['position'] = 'absolute';
bubble.style['marginTop'] = this.px(21);
bubble.style['marginLeft'] = this.px(1);
bubble.style['zIndex'] = this.baseZIndex_;
Hope that helps.
In the InfoBubble buildDom function, add:
bubble.className = 'bubble-container';
Now you have a CSS class for each InfoBubble, you can shift it using CSS margin.
You can also use a defined anchor height;
var anchorHeight = YOURNUMBER;
line 874 infobubble.js