My website has a number of pages that show a Google map with a bunch of markers, here's an example.
As you can see, the maps take a long time to load and I'm looking for ways to improve this. I was hoping to use GeoWebCache to cache the map tiles on the server, but I was informed that this would violate the terms of use for Google maps.
The code that I use to display a map and add a marker is appended below. It's a pretty straightforward usage of the Google Maps V3 JavaScript API, so I don't think there's much scope for optimizing it. Are there any obvious steps I could take to reduce the map-loading time?
SF.Map = function(elementId, zoomLevel, center, baseImageDir) {
this._baseImageDir = baseImageDir;
var focalPoint = new google.maps.LatLng(center.latitude, center.longitude);
var mapOptions = {
streetViewControl: false,
zoom: zoomLevel,
center: focalPoint,
mapTypeId: google.maps.MapTypeId.ROADMAP,
mapTypeControl: false
};
this._map = new google.maps.Map(document.getElementById(elementId), mapOptions);
this._shadow = this._getMarkerImage('shadow.png');
};
SF.Map.prototype._getMarkerImage = function(imageFile) {
return new google.maps.MarkerImage(this._baseImageDir + '/map/' + imageFile);
};
SF.Map.prototype._addField = function(label, value) {
return "<span class='mapField'><span class='mapLabel'>" + label + ": </span><span class='mapValue'>" + value + "</span></span>";
};
/**
* Add a marker to the map
* #param festivalData Defines where the marker should be placed, the icon that should be used, etc.
* #param openOnClick
*/
SF.Map.prototype.addMarker = function(festivalData, openOnClick) {
var map = this._map;
var markerFile = festivalData.markerImage;
var marker = new google.maps.Marker({
position: new google.maps.LatLng(festivalData.latitude, festivalData.longitude),
map: map,
title: festivalData.name,
shadow: this._shadow,
animation: google.maps.Animation.DROP,
icon: this._getMarkerImage(markerFile)
});
var bubbleContent = "<a class='festivalName' href='" + festivalData.url + "'>" + festivalData.name + "</a><br/>";
var startDate = festivalData.start;
var endDate = festivalData.end;
if (startDate == endDate) {
bubbleContent += this._addField("Date", startDate);
} else {
bubbleContent += this._addField("Start Date", startDate) + "<br/>";
bubbleContent += this._addField("End Date", endDate);
}
// InfoBubble example page http://google-maps-utility-library-v3.googlecode.com/svn/trunk/infobubble/examples/example.html
var infoBubble = new InfoBubble({
map: map,
content: bubbleContent,
shadowStyle: 1,
padding: 10,
borderRadius: 8,
borderWidth: 1,
borderColor: '#2c2c2c',
disableAutoPan: true,
hideCloseButton: false,
arrowSize: 0,
arrowPosition: 50,
arrowStyle: 0
});
var mapEvents = google.maps.event;
// either open on click or open/close on mouse over/out
if (openOnClick) {
var showPopup = function() {
if (!infoBubble.isOpen()) {
infoBubble.open(map, marker);
}
};
mapEvents.addListener(marker, 'click', showPopup);
} else {
mapEvents.addListener(marker, 'mouseover', function() {
infoBubble.open(map, marker);
});
mapEvents.addListener(marker, 'mouseout', function() {
infoBubble.close();
});
}
};
You could try "lazy loading" the google map, something like this:
var script=document.createElement("script");
script.type="text/javascript";
script.async=true;
script.src="http://maps.google.com/maps/api/js?sensor=false&callback=handleApiReady";
document.body.appendChild(script);
Or even like this is how I go it for Facebook AIP so that this doesn't slow down the initial load time.
$.getScript('http://connect.facebook.net/en_US/all.js#xfbml=1', function() {
FB.init({appId: opt.facebook_id, status: true, cookie: true, xfbml: true});
});
Example: http://blog.rotacoo.com/lazy-loading-instances-of-the-google-maps-api
Also looking at your HTML you have a lot of JS which should be in an external JS file and not inline, can you not pass a array instead of having a lot of duplicate inline code.
One option is to take a static snapshot and create the map behind it. once map is fully loaded, replace the dynamic one with the static map.
I also suggest that you take a look at:
https://developers.google.com/maps/documentation/staticmaps/
it might be that you can provide a simpler solution to your site without using the full dynamic maps api.
Related
I try to make a custom infobox. If I run for one marker, it works, but if I try from an imgUr (the array has got coordinates, name and other attributes for every marker) which have more than one marker I can't make it work. It shows only the first one marker and none else and no infobox.
Here is the code:
for (i = 0; i < imgAr.length; i++) {
var location = new google.maps.LatLng(imgAr[i].coords.coordinates[1],
imgAr[i].coords.coordinates[0]);
var marker = new google.maps.Marker({
position : location,
map : map,
icon : "icons/coffee1.png" ,
animation : google.maps.Animation.DROP,
infoboxIndex : i // <--- That's the extra attribute
});
var content = "<h5> Name:" + imgAr[i].name +
"<br> Description:" + imgAr[i].description +
"<br><img src='" + imgAr[i].image +
"' width='100px' height='100px'/> <h5>";
var var_infobox_props = {
content: content,
disableAutoPan: false,
maxWidth: 0,
pixelOffset: new google.maps.Size(-10, 0),
zIndex: null,
boxClass: "myInfobox",
closeBoxMargin: "2px",
closeBoxURL: "icons/close.png",
infoBoxClearance: new google.maps.Size(1, 1),
visible: true,
pane: "floatPane",
enableEventPropagation: false
};
var var_infobox = new InfoBox(var_infobox_props);
google.maps.event.addListener(marker, 'mouseover',
function(event) {
map.panTo(event.latLng);
map.setZoom(16);
infocoffee[this.infoboxIndex].open(map, this);
}
);
// Save infobox in array
infocoffee.push(var_infobox);
// Save marker in array
markers.push(marker);
}
markers.setMap(map);
What is not working? How can I fix it?
It sounds like either infocoffee.push(var_infobox); or markers.push(marker); results in an error, and the javascript stopped execute from there (That's why you have only one markers.)
I reproduce it on the JSFiddle. Take a look at the working sample, and the sample with error.
=-=-=- edited -=-=-=
The same concept should apply to the infobox plugin, as it is shown here.
I am a begginner in javascript, and I know that my code is a little messy. I feel as though I may have stepped on my own foot here but I am really hoping that there is some sort of work around.
What I am wondering is what the best way to add the event listener to the anchors is. Right now, I have a loop that sets all the markers on the map (so I didn't have to write the line of code each time for each marker) but looking back at it I am wondering if there is even a way to add an event listener now.
I have a list of links on the page, and I have an array full of data that i use to tag various things. What I need is to be able to click on the link (where it says "map it!") and for the info window to be prompted, and then I need to toggle that so that it closes if another one is opened
the website can be found here:
http://www.michiganwinetrail.com
And here is the full javascript page
http://www.michiganwinetrail.com/mainmap2.js
the code for the loop that I need to edit (which can be found at the bottom of that javascript link) is as follows:
function loadMap() {
var centerMich = new google.maps.LatLng(44.229457, -85.100098);
var myOptions = {
center: centerMich,
zoom: 7,
scrollwheel: false,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
map = new google.maps.Map(document.getElementById("mainMichMap"), myOptions);
var homesouthwestDiv = document.createElement('div');
var homenorthwestDiv = document.createElement('div');
var homesoutheastDiv = document.createElement('div');
homesouthwestDiv.index = 1;
homenorthwestDiv.index = 2;
homesoutheastDiv.index = 3;
var homeControl = {
southwest: swRegions(homesouthwestDiv, map),
northwest: nwRegions(homenorthwestDiv, map),
southeast: seRegions(homesoutheastDiv, map),
};
map.controls[google.maps.ControlPosition.RIGHT_TOP].push(homesouthwestDiv);
map.controls[google.maps.ControlPosition.RIGHT_TOP].push(homenorthwestDiv);
map.controls[google.maps.ControlPosition.RIGHT_TOP].push(homesoutheastDiv);
for (var i=0; i<=locations.length; i++) {
locations[i][0] = new google.maps.Marker({
position: new google.maps.LatLng(locations[i][1], locations[i][2]),
title: locations[i][3],
map: map,
content: locations[i][4]});
var infowindow = new google.maps.InfoWindow({
});
google.maps.event.addListener(locations[i][0], 'click', function() {
infowindow.setContent(this.content);
infowindow.open(map,this);
});
}
}
On click of the link you need to do something like this:
var myHtml = "<h2 class=\"firstHeading\">" + winery.name + "</h2>";
map.openInfoWindowHtml(new GLatLng(winery.fltLat, winery.fltLng), myHtml);
I had done a similar demo some time ago: http://dipoletech.com/beermenu/
I have a map that uses infoBubble.js.
In this map there is an array of locations that I iterate through.
The infoBubble should pop up when the custom icon is clicked but for some reason it only ever opens up the first data item.
Does anyone have an idea as to why that may happen?
I have developed the code for it here;
var arrMarkers = [
['Santiago de Cuba', 20.040450354169483, -75.8331298828125],
['Las Tunas', 20.97682772467435, -76.9482421875],
['Camaguey', 21.39681937408218, -77.9205322265625],
['Playa Santa Lucia', 21.555284406923192, -77.0526123046875],
['Santa Clara', 22.421184710331854, -79.9639892578125],
['Cienfuegos', 22.161970614367977, -80.4473876953125],
['Havana', 23.12520549860231, -82.3919677734375],
['San Cristobel', 22.730590425493833, -83.045654296875],
['Pinar del Rio', 22.43641760076311, -83.69384765625]
];
var arrInfoWindowsCuba = [];
var arrInfoWindows = [];
arrMarkers[i] = marker;
function init() {
var mapCenter = new google.maps.LatLng(21.616579336740603, -78.892822265625);
map = new google.maps.Map(document.getElementById('map_canvas'), {
zoom: 7,
center: mapCenter,
mapTypeId: google.maps.MapTypeId.TERRAIN
});
var image = '/wp-content/themes/Shootcuba/images/map-icon.png';
for (i = 0; i < arrMarkers.length; i++) {
var marker = new google.maps.Marker({
map: map,
position: new google.maps.LatLng(arrMarkers[i][1], arrMarkers[i][2]),
icon: image
});
var infoBubble = new InfoBubble({
content: '<div class="phoneytext">' + arrMarkers[i][0] + '<div class="left-col2"></div></div>',
boxClass: 'info-box',
alignBottom: true,
pixelOffset: new google.maps.Size(-150, -40),
maxWidth: 300,
padding: 0,
closeBoxMargin: '0px',
borderColor: '#ffffff',
borderRadius: '0',
maxWidth: 535,
disableAutoPan: false,
hideCloseButton: false,
backgroundClassName: 'phoney'
});
google.maps.event.addListener(marker, 'click', function () {
infoBubble.open(map, marker, i);
console.log(arrMarkers);
});
arrMarkers[i] = marker;
arrInfoWindowsCuba[i] = infoBubble;
}
}
Here's a working example. I took out a few of the arrays you had (I wasn't entirely sure what they were all for, and they were causing errors in just the snippet you posted), but otherwise is pretty true to what you were doing. The big difference is that I made a separate function for creating the markers. This was mainly done to keep the scope of the click events separate from one another, since the click event always triggering the last event indicates to me that the scopes aren't properly separate.
In particular, what I believe was happening is that the event function you kept overriding values to marker and infoBubble, and the event listener would refer to the current values of those variables, not the values when you first attach the listener. Making a separate function call to maintain the scope for the events strikes me as the cleanest solution.
I am using a Wordpress theme for a Real Estate site, and the theme has the following function:
When you add a listing, the theme will use the listing title (street address), and the 'city' and 'state' tags, to set the listing map pin. It can also default to lat/long (you can manually add it when creating a new listing)
Due to wanting to use a descriptive title - rather than a street address - for the listings, I want the map to place the pin based only on the lat/long. When I use a descriptive title the listing map does not load - so the map is not defaulting to the lat/long after it fails to geocode the descriptive title.
Mapping code is as follows (I am no expert so have been trying many changes, all which break the mappping functions!)
var estateMapping = (function () {
var self = {},
marker_list = [],
open_info_window = null,
x_center_offset = 0, // x,y offset in px when map gets built with marker bounds
y_center_offset = -100,
x_info_offset = 0, // x,y offset in px when map pans to marker -- to accomodate infoBubble
y_info_offset = -100;
function build_marker(latlng, property) {
var marker = new MarkerWithLabel({
map: self.map,
draggable: false,
flat: true,
labelContent: property.price,
labelAnchor: new google.maps.Point(22, 0),
labelClass: "label", // the CSS class for the label
labelStyle: {opacity: 1},
icon: 'wp-content/themes/realestate_3/images/blank.png',
position: latlng
});
self.bounds.extend(latlng);
self.map.fitBounds(self.bounds);
self.map.setCenter(convert_offset(self.bounds.getCenter(), x_center_offset, y_center_offset));
var infoBubble = new InfoBubble({
maxWidth: 275,
content: contentString,
borderRadius: 3,
disableAutoPan: true
});
var residentialString = '';
if(property['commercial'] != 'commercial') {
var residentialString='<p class="details">'+property.bed+' br, '+property.bath+' ba';
}
var contentString =
'<div class="info-content">'+
'<img class="left" src="'+property.thumb+'" />'+
'<div class="listing-details left">'+
'<h3>'+property.street+'</h3>'+
'<p class="location">'+property.city+', '+property.state+' '+property.zip+'</p>'+
'<p class="price"><strong>'+property.fullPrice+'</strong></p>'+residentialString+', '+property.size+'</p></div>'+
'</div>';
var tabContent =
'<div class="info-content">'+
'<img class="left" src="'+property.agentThumb+'" />'+
'<div class="listing-details left">'+
'<h3>'+property.agentName+'</h3>'+
'<p class="tagline">'+property.agentTagline+'</p>'+
'<p class="phone"><strong>Tel:</strong> '+property.agentPhone+'</p>'+
'<p class="email">'+property.agentEmail+'</p>'+
'</div>'+
'</div>';
infoBubble.addTab('Details', contentString);
infoBubble.addTab('Contact Agent', tabContent);
google.maps.event.addListener(marker, 'click', function() {
if(open_info_window) open_info_window.close();
if (!infoBubble.isOpen()) {
infoBubble.open(self.map, marker);
self.map.panTo(convert_offset(this.position, x_info_offset, y_info_offset));
open_info_window = infoBubble;
}
});
}
function geocode_and_place_marker(property) {
var geocoder = new google.maps.Geocoder();
var address = property.street+', '+property.city+' '+property.state+', '+property.zip;
//If latlong exists build the marker, otherwise geocode then build the marker
if (property['latlong']) {
var lat = parseFloat(property['latlong'].split(',')[0]),
lng = parseFloat(property['latlong'].split(',')[1]);
var latlng = new google.maps.LatLng(lat,lng);
build_marker(latlng, property);
} else {
geocoder.geocode({ address : address }, function( results, status ) {
if(status == google.maps.GeocoderStatus.OK) {
var latlng = results[0].geometry.location;
build_marker(latlng, property);
}
});
}
}
function init_canvas_projection() {
function CanvasProjectionOverlay() {}
CanvasProjectionOverlay.prototype = new google.maps.OverlayView();
CanvasProjectionOverlay.prototype.constructor = CanvasProjectionOverlay;
CanvasProjectionOverlay.prototype.onAdd = function(){};
CanvasProjectionOverlay.prototype.draw = function(){};
CanvasProjectionOverlay.prototype.onRemove = function(){};
self.canvasProjectionOverlay = new CanvasProjectionOverlay();
self.canvasProjectionOverlay.setMap(self.map);
}
function convert_offset(latlng, x_offset, y_offset) {
var proj = self.canvasProjectionOverlay.getProjection();
var point = proj.fromLatLngToContainerPixel(latlng);
point.x = point.x + x_offset;
point.y = point.y + y_offset;
return proj.fromContainerPixelToLatLng(point);
}
self.init_property_map = function (properties, defaultmapcenter) {
var options = {
zoom: 1,
center: new google.maps.LatLng(defaultmapcenter.mapcenter),
mapTypeId: google.maps.MapTypeId.ROADMAP,
disableDefaultUI: true,
streetViewControl: false
};
self.map = new google.maps.Map( document.getElementById( 'map' ), options );
self.bounds = new google.maps.LatLngBounds();
init_canvas_projection();
//wait for idle to give time to grab the projection (for calculating offset)
var idle_listener = google.maps.event.addListener(self.map, 'idle', function() {
for (i=0;i<properties.length;i++) {
geocode_and_place_marker(properties[i]);
}
google.maps.event.removeListener(idle_listener);
});
}
return self;
}());
It would be worth noting that the code above also applies to a function which places multiple listing markers on a homepage map. Preferably I would like the maps to always populate from the manually added lat/long (even the homepage one) but I just cannot get it to happen without breaking the maps...
Any information would be much appreciated.
It would be great if you would post an answer to your question with some code that explains the solution you found, to more fully help others with similar issues. I thought I would share the way I solved a similar problem recently.
Rather than use the post title to store the address, I created a custom meta box on these posts, requesting the street address, city name, and state. I hooked into the save_post action with a function that uses the Google Maps Geocode API to look up the latitude and longitude:
$base_url = "http://maps.googleapis.com/maps/api/geocode/json?";
$address=urlencode($_POST['address']); // address was the name of the field in the meta box
$request_url = $base_url . "&address=" . urlencode($address).'&sensor=false';
$query = lookup($request_url);
$results=json_decode($query, true);
$lat = $results["results"][0]["geometry"]["location"]["lat"];
$lng = $results["results"][0]["geometry"]["location"]["lng"];
//now that we have the latitude and longitude, we can save them as post meta
update_post_meta($postid, 'lat', $lat);
update_post_meta($postid, 'long', $lng);
Note that this uses a function called geocode to actually retrieve the latitude and longitude values. I can't find the tutorial where I originally learned how to make this function, but here is one (where the function is called 'lookup' instead): // lookup is a function that sends the geocode request using curl, you can find an example here: http://www.andrew-kirkpatrick.com/2011/10/google-geocoding-api-with-php/
Then, when displaying the map, you can retrieve those values when creating your new latitude and longitude in your javascript:
var latlng = new google.maps.LatLng(<?php get_post_meta($postid, 'lat'); ?>,<?php get_post_meta($postid, 'long'); ?>)
Problem: I want to put Twitter Share Button in my Google Map InfoWindow.
I have used a google.maps.event.addListener(infowindow, 'domready' function() { ...code...} to listen for the InfoWindow to be added to the DOM but it only works on the first InfoWindow launched.
The Twitter Share Button Examples apply the following script:
!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");
against the class:
"twitter-share-button"
Assumption:
Now the problem as I see it is that the InfoWindow and markers get created in the $.each() loop and I don't know what event to get jQuery to watch for.
I have tried creating a jsfiddle as an example but I can't get it to load the map if I use $.each() like in the live site but I link it here in case it helps you demonstrate an answer:
My non-working JSFiddle.
My live development site which shows the problem.
Other Stack Overflow questions: Similar but different.
Calling a JavaScript function within an InfoWindow (Google Maps)
Tweet button and infoWindow
I've seen the jQuery-ui-map plugin. That doesn't do anything I can't do in straight JavaScript.
Map.js: or Pastebin link to code below but with syntax highlighting
$('.page-map').live("pageshow", function() {
$.getJSON("/rest/Pub", function(data) {
var latlng = new google.maps.LatLng(59.920, 10.750);
var map = new google.maps.Map(document.getElementById("map-canvas"), {
// zoom is good on iphone # 13 but better on desktop # 14
zoom: 12,
center: latlng,
backgroundColor: 'black',
mapTypeControlOptions: {
style: google.maps.MapTypeControlStyle.DROPDOWN_MENU
},
mapTypeId: google.maps.MapTypeId.HYBRID
});
var bubble = new google.maps.InfoWindow();
var pubs = data.list.Pub;
$.each(pubs, function(i, pub) {
var beer;
var pubaddress = encodeURI(pub.pub_address);
var pubname = encodeURI(pub.pub_name);
var posi = new google.maps.LatLng(pub.pub_lat, pub.pub_long);
var marker = new google.maps.Marker({
animation: (pub.pub_bounce =='true') ? google.maps.Animation.BOUNCE : null,
map: map,
icon: "/static/images/" + pub.beer_price + ".png",
shadow: 'static/images/shadow.png',
position: posi,
title: pub.pub_name
});
if (pub.beer_price == 99) {
marker.setZIndex(1);
} else {
marker.setZIndex(100);
}
google.maps.event.addListener(marker, 'click', function() {
// http://mapki.com/wiki/Google_Map_Parameters
if (pub.beer_price == 99) {
beer = '?';
} else {
beer = pub.beer_price;
}
tweet = 'Tweet';
content_string = '<h1>'+ pub.pub_name + '</h1>'+
'<p><a href="http://maps.google.com/?saddr=Current%20Location&daddr=' +
pubaddress + '+(' + pubname + ')&dirflg=w&t=h">Google Maps Directions</a></p>' +
'<p>Phone:' + pub.pub_phone + '</p>' +
'<p>Halvliterpris: NOK <b>' + beer + '</b></p><p>' + tweet + '</p>';
bubble.setContent(content_string);
console.log(bubble.getContent());
bubble.open(map, this);
});
google.maps.event.addListener(bubble, 'domready', function() {
//$('.twitter-share-button').on('domready', function(event) {
console.log("Iteration:"+ i + " Twitter javascript loaded " + pub.pub_name);
!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");
});
});
});
});
Cheers.
Calling
twttr.widgets.load();
after
bubble.open(map, this);
should do the trick.
Since you're loading the twitter widgets JavaScript asynchronously, you might want to check if twttr is already defined, before calling it.