Delaying execution of Javascript function relative to Google Maps / geoxml3 parser? - javascript

I'm working on a implementing a Google map on a website with our own tiles overlays and KML elements. I've been previously requested to create code so that, for instance, when the page is loaded from a specific URL, it would initialize with one of the tile overlays already enabled. Recently, I've been requested to do the same for the buildings which are outlined by KML elements so that, arriving at the page with a specific URL, it would automatically zoom, center, and display information on the building.
However, while starting with the tile overlays work, the building KML does not. After doing some testing, I've determined that when the code which checks the URL executes, the page is still loading the KML elements and thus do not exist for the code to compare to or use:
Code for evaluating URL (placed at the end of onLoad="initialize()")
function urlClick() {
var currentURL = window.location.href; //Retrieve page URL
var URLpiece = currentURL.slice(-6); //pull the last 6 digits (for testing)
if (URLpiece === "access") { //If the resulting string is "access":
access_click(); //Display accessibility overlay
} else if (URLpiece === "middle") { //Else if the string is "middle":
facetClick('Middle College'); //Click on building "Middle College"
};
};
facetClick();
function facetClick(name) { //Convert building name to building ID.
for (var i = 0; i < active.placemarks.length; i++) {
if (active.placemarks[i].name === name) {
sideClick(i) //Click building whose id matches "Middle College"
};
};
};
Firebug Console Error
active is null
for (var i = 0; i < active.placemarks.length; i++) {
active.placemarks is which KML elements are loaded on the page, and being null, means no KML has been loaded yet. In short, I have a mistiming and I can't seem to find a suitable place to place the URL code to execute after the KMl has loaded. As noted above, I placed it at the end of onLoad="initialize()", but it would appear that, instead of waiting for the KML to completely load earlier in the function, the remainder of the function is executed:
onLoad="initialize()"
information(); //Use the buttons variables inital state to set up description
buttons(); //and button state
button_hover(0); //and button description to neutral.
//Create and arrange the Google Map.
//Create basic tile overlays.
//Set up parser to work with KML elements.
myParser = new geoXML3.parser({ //Parser: Takes KML and converts to JS.
map: map, //Applies parsed KML to the map
singleInfoWindow: true,
afterParse: useTheData //Allows us to use the parsed KML in a function
});
myParser.parse(['/maps/kml/shapes.kml','/maps/kml/shapes_hidden.kml']);
google.maps.event.addListener(map, 'maptypeid_changed', function() {
autoOverlay();
});
//Create other tile overlays to appear over KML elements.
urlClick();
I suspect one my issues lies in using the geoxml3 parser (http://code.google.com/p/geoxml3/) which converts our KML files to Javascript. While the page has completed loading all of the elements, the map on the page is still loading, including the KML elements. I have also tried placing urlClick() in the parser itself in various places which appear to execute after all the shapes have been parsed, but I've had no success there either.
While I've been intending to strip out the parser, I would like to know if there is any way of executing the "urlClick" after the parser has returned the KML shapes. Ideally, I don't want to use an arbitrary means of defining a time to wait, such as "wait 3 seconds, and go", as my various browsers all load the page at different times; rather, I'm looking for some way to say "when the parser is done, execute" or "when the Google map is completely loaded, execute" or perhaps even "hold until the parser is complete before advancing to urlClick".
Edit: Here are links to the map with the basic form of the issue found above. Since I've been developing the next update to the map on a test server, facetClick() is not part of this live version and I instead use its output function sideClick(); however the error is still the same in this arrangement:
active is null
google.maps.event.trigger(active.gpolygons[poly],'click');
Map: http://www.beloit.edu/maps/
Map w/Accessibility: http://www.beloit.edu/maps/?access
Map w/Building Click: http://www.beloit.edu/maps/?middle
EDIT: Spent most of my day working on rebuilding the functionality of the parser in Javascript and, low and behold, without the parser it works just fine. I figure that is obvious as I have to define each shape individually before the code, rather than waiting for it to be passed along by the parser. It would seem the answer is "if you want unique URLs, drop the parser". >_<

I've come across a similar problem when dealing with waiting for markers and infoWindows to load before executing a function. I found a solution here ( How can I check whether Google Maps is fully loaded? see #Veseliq's answer) that using the google maps event listener function for checking when the map is 'idle', does the trick. I assume this solution would work for KML layers as well. Essentially what you will have to do is include the following at the end of your initialize function:
google.maps.event.addListenerOnce(map, 'idle', function(){
// do something only the first time the map is loaded
});
In the API reference ( https://developers.google.com/maps/documentation/javascript/reference ) it states that the 'idle' event "is fired when the map becomes idle after panning or zooming". However, it seems to hold true that it is also fires on initial page load after everything in the map_canvas has loaded. And by using the addListenerOnce call, you ensure that it is never executed again after the initial page load (meaning it won't fire after a zoom or a pan action).
Second option:
As I mentioned you can take the callback approach, I believe this will only call your urlClick function after completing the parsing. Here's how you should probably arrange your code to make it work:
function someFunction(callback){
myParser.parse(['/maps/kml/shapes.kml','/maps/kml/shapes_hidden.kml']);
callback();
}
and then in your initialize you will have:
someFunction(function(){
urlClick();
});
You will have to make your map and myParser variables global.
Resources: This link had an excellent and detailed brief on how callback functions work in javascript, http://www.impressivewebs.com/callback-functions-javascript/

Related

Google Maps callback not always triggering

Problem: It seems like when loading the Google Maps script, the callback is not performed. It might be related to how to load the Google Maps script.
Context: My website displays a Map in the frontpage when certain minimum viewport conditions are met. Specifically, I avoid loading it when I'm on mobile. I do not want to use jQuery :)
Implementation: The way I'm doing it is by first loading a small handcrafted script (google-map.js), which does a bunch of things and ends with:
function dynamicallyLoadScript(url) {
var script = document.createElement("script");
script.src = url;
script.type = "text/javascript";
script.defer = true;
document.head.appendChild(script);
};
// Load Google Maps when NOT on mobile
if ( ! mobile ) {
dynamicallyLoadScript('https://maps.googleapis.com/maps/api/js?key=wIzaScChUs5NxF3Z5LZoxkAS4wca9A7Pk53I024&callback=initMap');
}
(The JS key is fake).
Outcome: This actually works when I load the site for the first time. I see that google-map.js is loaded, the Google Maps script tag is added to the head, and both resources are loaded and working well. But sometimes (!) when I refresh, the map stops loading and upon inspection I see that the script tag was added successfully, and the resource is loaded, but the initMap() callback function hasn't triggered. If I go to the console, it does fire.
Since this happens only sometimes (and never upon a fresh load without cache), I think it might have something to do with a race condition or just with how cache works. But I'm not sure how to address it. Thoughts?
Solution in a nutshell: This is not a very elegant solution imho, but I figured that what I needed to create is some retry logic for loading initMap() when it runs before it should. In other words, I'm diagnosing this as a race condition present when cache is available.
Implementation: Added a small function to check every 0.1 seconds if the DOM has an element that only exists when the map is fully loaded. When so, the timer stops, but otherwise, it runs the initMap() function.
function checkMaps() {
if (document.getElementsByClassName("gm-style").length == 0) {
initMap();
} else {
clearInterval(checker);
}
}
checker = setInterval(checkMaps, 100);

Find polygon for place in KML

NOTE: Please make sure you read my updates at the bottom!!
I have a KML file that I load 'onto' a Google Map; I also have a searchbox where users can search for a city. When the city is found I place a marker which, in most (if not all) cases should fall in one of the polygons defined in the KML.
I can click a polygon and it shows an info-popup with the areacode for that area; however: when a marker is placed I would like to have this info-popup shown automatically (and possibly other(s) that are shown before placing the marker hidden).
I have looked over the Maps V3 documentation but was unable to find anything. Is this possible?
You can view the project at http://netnummer.robiii.me, the source can be found at https://github.com/RobThree/NetnummersNL
The relevant (snippet of) code is:
google.maps.event.addListener(autocomplete, 'place_changed', function () {
var place = autocomplete.getPlace();
if (!place.geometry)
return;
// Remove any existing markers
for (var i = 0, marker; marker = markers[i]; i++)
marker.setMap(null);
markers = [];
// Create a marker for place.
var marker = new google.maps.Marker({
map: map,
icon: image,
title: place.name,
position: place.geometry.location
});
markers.push(marker);
// ... Here I should figure out in which polygon the marker is positioned
// ... and preferrably open/display the info-window which is shown when a
// ... polygon is clicked.
// Scroll (pan) to marker
map.panTo(place.geometry.location);
});
Also, as a side-question: autocomplete.getPlace(); always returns an object; what is the best way to find out if the object is an actual (useful) place? When I search for xxxx for example, getPlace() returns Object {name: "xxxx"}, an actual result (like searching for Amsterdam) returns Object {address_components: Array[4], adr_address: "<span class="locality">Amsterdam</span>, <span class="country-name">Nederland</span>", …}. I currently use if (!place.geometry) to find out if the place is useful / an actual place; however I guess there's a better way ('best practice'?) to do this?
Edit 1: just stumbled across With Google Maps API V3, determine if a marker is inside a KML Layer boundary. Currently looking into it. However, I should probably note that the KML is hosted by a 3rd party and updated at will by them; as I use static site hosting I prefer not to have a separate process to extract the coordinates from polygons from the KML file. I prefer to "read" the KML (in)directly.
Edit 2: I moved from (direct) KML to a FusionTables based solution. I can now "highlight" polygons where the place is in. Now all I (still) need is a way to figure out how to show it's InfoWindow. I'll look into that tomorrow; it seems I need to query (again) into a DataTable or something and get the label info that way.
Edit 3: Solved!
I should've been in bed by now (see edits and comments) but hey... I just don't let go that easily...
So here's what I did:
Move from (direct) KML to Fusion Tables (Resulting html)
Fire off an extra query using Google Visualization's datatables to get the data I want for the specific region (Resulting html)
Use the place variable I already had for lat/lng and name and show infowindow
...
Profit!
I purposely linked above links to specific diffs or files of a specific version so later changes/updates won't mess with this answer. It's a bit too much to post all specifics in this post but I hope this helps someone.
The final result can be seen here.

Popout Map for Google Maps application

I've written a single-page application using Google Maps in conjunction with AngularJS.
In short I have three columns - one filled with tools, one with a table of data, and the final is the geographical representation of the table's data.
The data is updated every minute.
The users all have multiple monitors, and it's been requested that adding the ability to have the map on one window, and table on the other - so that the map can have more real estate and we can add more columns to the table.
The solution that I really want is to create a new window using JS, and move the map's DIV to this window.
This works in chrome and it works exactly as I expected - moving the div doesn't break google map's or Angular's ties to the DOM, as the DIV is never deleted nor cloned, just moved within the DOM.
However IE11 treats the new window as an entirely separate document, and disallows the use of appendChild (throwing heirachy exceptions).
I have tried using adoptNode to move the node to the new window's document, but IE throws a 'No such interface supported' error on the adoptNode call, however IE allows you to use adoptNode to move the node between iframes, see here: http://jsfiddle.net/nx9u4y9w/9/
This works in FF/Chrome.
var f1 = window.f1,
f2 = window.f2,
pop = window.open(f2.location.href, 'pop'),
x = 0;
window.fx = function(o) {
var source = f1,
target = f2,
map;
if (x++ == 1) {
source = f2;
target = pop;
o.disabled = true;
}
map = source.document.getElementById('map-canvas');
var adopted = target.document.adoptNode(map);
target.document.body.appendChild(adopted);
}
Is there a simple way to get this working in IE11?
I wanted to avoid creating a new HTML page to load into the window, as it'd cause a serious break in the UX whilst the map reloads, plus it'll there's the code overhead of having to create an intra-browser messaging using postMessage, etc.

Google Earth API - time primitive set even though KML file has no time elements

I am trying to load via Google Earth API a KML file and have my own time slider control.
To determine if I need to enable/display my own custom time slider I am interogating the google earth TimeControl to see if there are any begin and end times set.
If I load a KML with a timespan I am seeing that a begin and end time are set and the google earth time slider appears in google earth.
If I then load another KML file but this time without any timespan, google earth correctly does not display the time slider but when I interrogate the TimeControl through the google earth API it still has the start and end times of the first KML.
I have attached (via kmltree bind() method) a function which gets called once the KML has loaded. It is this function that I interrogate the TimeControl.
Why does the TimeControl still contain the start and end times of the first KML? Am I checking too early?
Javascript code is:
tree = kmltree({
url: kmlUrl,
gex: gex,
element: $('.tree3'),
mapElement: $('#map3d'),
setExtent: true,
restoreState: false
});
$(tree).bind('kmlLoaded', function(event, kmlObject)
{
console.log("In kmlLoaded function");
time = ge.getTime();
if(ge.getTime().getControl())
{
console.log("We have a time primitive.");
control = time.getControl();
extents = control.getExtents();
// We have some time elements
beginTime = extents.getBegin();
console.log("beginTime is: " + beginTime.get());
endTime = extents.getEnd();
console.log("endTime is: " + endTime.get());
mainwindow.enablePlaybackWidgets(beginTime.get(), endTime.get());
}
else
{
console.log("We do NOT have a time primitive.");
mainwindow.enablePlaybackWidgets("", "");
}
});
tree.load()
I am using this in conjunction with a QT app and upon finding out if there is a timespan within the new loaded KML or not, it sould then call in to the QT mainwindow object and enable/disable my custom time slider widgets accordingly.
When the second KML is loaded I am expecting there to be no TimeControl (as Google Earth is not displaying one) and hence enter the "else" part of the code above. This is not the case. The TimeControl still exists and has the start and end times from the first KML file loaded.
any help would be very much appreciated.
thanks

Executing javascript mixed with HTML from a jQuery ajax response

Or, not precisely "executing," but updating a function that exists before the response with a function returned in the response.
Step 1
I have an HTML5 page that describes a location.
On the server side this page includes a ColdFusion file "MapFunction.cfm." (Which is used for consistent mapping all over the site at large.)
MapFunction.cfm outputs a javascript function "loadMap" mixed with the HTML.
loadMap() contains all the javascript needed to place a Bing map of the location on the page.
Some javascript in a separate js file actually calls loadMap().
This works when the page is first loaded.
Step 2
Search & results stuff is all fine too. Nothing needs to be done with the map here.
Step 3
When a search result is clicked, the result detail is loaded asynchronously via a jQuery $.get() request.
It returns mixed HTML and javascript which I use jQuery to traverse through.
With the jQuery objects I update specific areas of the page to show different details.
One of the areas I need to update is the map. That part isn't working.
What I'm working with is mixed HTML and Javascript that is identical in both Step 1 and Step 3:
<section id="mod-map" class="module mod-map">
<header class="mod-head">
Map <span class="arrow"></span>
</header>
<div id="map" class="mod-body">
<div id="cmMap" style="position:relative;width:369px;height:303px;"></div>
<script type="text/javascript" id="cfLoadMap">
// ...some global variables are defined to use...
function loadMap()
{
// ...Bing/Virtual Earth Map Stuff...
// This part here is unique to each location's detail
var propertypoint = new VELatLong(parseFloat(36.707756),parseFloat(-78.74204));
// ...More Bing/Virtual Earth Map Stuff...
// This part here is unique to each location's detail
var label = "<div class=\"wrapper\"><img onerror=\"replaceImage(this);\" src=\"noimage.jpg\" width=\"100\" class=\"thumb\" alt=\"\" /><div class=\"caption\"><br />City<br /> State, 12345</div></div>";
// ...More Bing/Virtual Earth Map Stuff...
}
</script>
</div>
</section>
Now, in Step 3 loadMap() does get called again, but it just refreshes the map to the same location. The loadMap() function as the browser knows it doesn't get updated with the one retrieved via ajax.
That updated block of mixed HTML & javascript above does get successfully added to the page after each ajax call. It is placed right where it originally is, but with different coordinates and captions where indicated by the comments above. The ajax callback looks like (slightly simplified):
$.get(urlToLoad, {}, function(data, status, request){
var newData = $(innerShiv(data, false)),
newModules = newData.find(".module");
// (innerShiv is used to make HTML5 tags work in IE. It's possible I'm going a little overboard with using it, but I had a lot of issues with IE. :-))
newModules.each(function(i){
var thisId = "#" + $(this).attr("id"),
thisBody = $(this).find(".mod-body").html(),
toReplaceAll = $("body").find(thisId),
toReplaceBody = toReplaceAll.find(".mod-body");
// These variables are used to choose add content in different ways based on thisID. Below is the one the map area is subject to.
toReplaceBody.html(innerShiv(thisBody));
}); // each
// Various things including loadMap() get called/re-initiated/etc. here
}, "html"); // get
This works in Firefox 3.6, but nowhere else I've tested (Opera 11, IE 7, Chrome 8).
I have done this before in a similar situation with dynamically PHP generated javascript written to a separate js file--$.getScript works great there. But this is mixed into the HTML of the ajax response.
I've been looking and have found and tried the following (among other things):
Attempted Solutions
1. var myScript = new Function($('script#cfLoadMap', data).text()); myScript();
2. eval(newData.text());
3. eval(newData.find("#cfLoadMap").text());
4. $("head").append(newData.find("#cfLoadMap"));
None of these so far seem to be doing any good.
I know there are a few other ways this could theoretically be done. But as it stands at the moment, I do not have any ability to change much of anything but what I do with the mixed HTML & javascript response. I need a solution where,
The details will be updated via ajax.
The javascript will be mixed in with the HTML.
The javascript will be a javascript function generated dynamically by ColdFusion.
Very similar questions have been asked & resolved before, so I hope this can be done. However, none of the solutions I've found are working for me. Might be making a mistake or missing something, or maybe it's just different when it's a function?
Any help would be appreciated.
Answer
It suddenly started working with the following code:
$.get(urlToLoad, {}, function(data, status, request){
var safeData = $(innerShiv(data, false)),
newModules = safeData.find(".module"),
newScript = safeData.find("script#cfLoadMap");
// Update Each module
newModules.each(function(i){
var jqoThis = $(this),
thisId = "#" + jqoThis.attr("id"),
newModule = jqoThis,
newModBody = jqoThis.find(".mod-body"),
curModule = $("body").find(thisId),
curModBody = curModule.find(".mod-body");
// Varies by id, this one is used by the map area.
curModBody.html(innerShiv(newModBody.html()));
}); // each
// Make sure plugins are bound to new content
$("body").oneTime(100, function(){
// Various things get initiated here
// Maps -- this one works: Chrome, Firefox, IE7, Opera
$("head").append(newScript);
// Maps -- these did not work
/*
// Firefox only (but Firefox always works)
runScript = new Function(newScript.text());
runScript();
*/
/*
// Firefox only (but Firefox always works)
eval(newScript.text());
*/
}); // oneTime
}, "html"); // get
One thing I did notice for sure was that without innerShiv, in all my browsers, $(data).find("script#cfLoadMap").text() was blank -- which I did not expect.
Most likely it's that functions in JavaScript can be declared in non-global scope, so when you're inserting the <script> tag, jQuery is evaling it, but not replacing the original function (as you noticed).
A fix for this would be to change how you declare the function from this:
function loadMap()
{
...
}
to this:
window.loadMap = function loadMap() {
...
}
That way the top-level loadMap will always be the latest one that came down from the server.
You may want to consider not modifying the client code this way as it can make debugging trickier, but that's totally up to you. Hopefully this answer works for you either way.
It suddenly started working with the following code:
$.get(urlToLoad, {}, function(data, status, request){
var safeData = $(innerShiv(data, false)),
newModules = safeData.find(".module"),
newScript = safeData.find("script#cfLoadMap");
// Update Each module
newModules.each(function(i){
var jqoThis = $(this),
thisId = "#" + jqoThis.attr("id"),
newModule = jqoThis,
newModBody = jqoThis.find(".mod-body"),
curModule = $("body").find(thisId),
curModBody = curModule.find(".mod-body");
// Varies by id, this one is used by the map area.
curModBody.html(innerShiv(newModBody.html()));
}); // each
// Make sure plugins are bound to new content
$("body").oneTime(100, function(){
// Various things get initiated here
// Maps -- this one works: Chrome, Firefox, IE7, Opera
$("head").append(newScript);
// Maps -- these did not work
/*
// Firefox only (but Firefox always works)
runScript = new Function(newScript.text());
runScript();
*/
/*
// Firefox only (but Firefox always works)
eval(newScript.text());
*/
}); // oneTime
}, "html"); // get
One thing I did notice for sure was that without innerShiv, in all my browsers, $(data).find("script#cfLoadMap").text() was blank -- which I did not expect.
However, I don't really see how this is different from what I had tried before and which failed. If someone spots a substantive difference, please let me know, for future reference?
(Note: it doesn't seem to make a difference that the Map bit is placed in the timeout, it works as well above it.)

Categories

Resources