mapsplugin/cordova-plugin-googlemaps

inconsistent intialization of map on iOS.

celron opened this issue · 33 comments

I have a very unusual problem in which I'm not sure if the problem is with iOS, the plugin or just google maps api.
On iOS 9.1, sometimes the map does not appear. In iOS 8.4 there is no problem.
In examining the logs, there does not appear to be any problems, and when I rotate the display to landscape, the screen is redrawn correctly.
I'm using Cordova 5.2 with 1.2.9 of the googlemaps plugin and am using jquery and jquery mobile though that shouldn't affect the problem.
The html is rendered correctly as can be seen the following picture.
image

The screen normally looks like
image

I tried using map.refreshLayout() but it does not cause the screen to be drawn. Is there any other function that I might use to refresh the map? or to see what can be happening with the display?

Please just try version 1.3.0. Please read the new Readme on the plugins front page (you need to remove google maps sdk before)

This has included the new version of iOS sdk. Your problem seem to me more framework independent, like there is a non transparent layer in front

@celron

I am using 1.2.5 and was also facing this problem on iOS (haven't tried with Android). I discovered that the main ion-nav-view (I am using Ionic) inside of my index.html was sometimes not being made transparent - at random.

The solution (hack) I found was to give it an ID and flick it's opacity to 0 and then straight away back to 1 inside the "plugin.google.maps.event.MAP_READY event"

Really hacky but it at least works every time whereas the map previously often did not appear.

@hirbod will try 1.3 soon and see if this is fixed.

You have to understand that this plugin will only make parent nodes transparent on domready. As soon as the DOM changes (sometimes delayed/ timing issue) the plugin won't be able to make parent nodes transparent. Sometimes (like on onsenui, maybe ionic too), there are elements, which are positioned absolutly and not being registered as a parent node. In this case you will have to do it manually.

In my case I have some onsen elements, which I also have to set to background rgba(0,0,0,0)

I think version 1.3.0 won't change anything. There is no way around. But I would not wait until map_ready. You should apply your hack as soon as you can consider that all DOM nodes are ready

Thanks for the explanation @hirbod. Just moved the instruction before I call plugin.google.maps.Map.getMap and it is now better, as there is not any flicker any more, which I had when I waited for the map_ready event.

Could you show me how you did that @johnrobertcobbold, im using ionic also had have seen this issue and couldn't workout what the problem was.
Just setting the maps div to opacity:0 though wont work for me, as I have buttons that are added to the div also so they overlay the map. But then again the plugin already does this doesnt it and it shows...gotta find how to do it.

@johnrobertcobbold I have seen this problem on 1.2.5, but it appear to disappear when I initially refreshed the screen. It was very, very hard to reproduce, however with the new versions of everything, (iOS 9, 1.2.9) it has been more common.
I have never seen the problem on Android, though it could exist. I test primarily on Android

@hirbod, I am curious as to what types of elements/layer may block the map (and not the html) I make the html a child of the map, so would have expected the html elements to be blocked as well.
I would not be surprised that there are timing issues that contribute to the problem, on iOS it appears that everything is just more complex.

I will try with 1.3 later today.

Let's assume following scenario: (or <div> is <ion-page> or <ons-page>)

<html> -- will be transparent
   <body> -- will be transparent
    <div class="page-background"></div>* -- NOT TRANSPARENT
      <div> -- will be transparent
         <div> -- will be transparent
           <div> -- will be transparent
              <div id="map"></div>
...
...

*this is something that happens e.g. on onsenUI, not a parent node, will not be recognized from plugin

Also, elements that where added after the plugin was initialized will not be transparent. refreshLayout() need to be called, sometimes you just need to make it transparent on your own.

I'm using OnsenUI, I have a global $rootScope (or use a service) function, which I call everytime I change the view, just to make sure that my "special" div's are transparent. (ons.ready()) - ionic will have for sure it's own ready mechanism, which can be called when it's ready rendering it's content.

You should hook on that. Use the DOM inspector and bubble up every parent-node until you find the one which was not set to transparent. After I understand that, it worked perfectly.

Greetings
Using 1.3.0 still sometimes comes up with a blank map, though otherwise it seems to work fine.

Inspecting the DOM and manipulating the z-index of the map div (it is set to 0), the map would suddenly appear. Is there an idea value for the z-index? I did not see anything that was above the map that would block it.

I will do some additional tests to see what happens when I try manipulating it.

Actually the z-index should not change anything. The mapview is placed under the webview. Changing the z-index will actually just trigger a redraw inside of webkit, which will indeed fix some visual problems sometimes.

White maps = drawing error. I'm very sorry, but the only method to display the map with the ability to place content on it is this way. Otherwise you need to stick on Google Maps JS. I know, this plugin is quite hacky, I don't regret, but it's the only way currently.

My time is very limited, but I will try to dig inside of the JS-code and optimize init. I'm sure there will be a better way to detect parent-nodes.

@hirbod I understand time is the one thing that is very valuable.

I have done plenty of programming in C, C++ etc, but very little with Objective C, but I am willing to look into this problem as it is very important to me. If you can tell me where to look in Objective C (or is it js-code), I will do what I can.

Is there a way to trigger a redraw of the webkit?

As for when my crude tests with CSS it did not appear to affect anything until I changed the CSS itself.

Try to change z-index of element, or just background-element or something.
The detection mechanism is JS only. The Java/Obj-C code should be fine.

https://github.com/wf9a5m75/phonegap-googlemaps-plugin/blob/master/www/googlemaps-cdv-plugin.js

https://github.com/wf9a5m75/phonegap-googlemaps-plugin/blob/master/www/googlemaps-cdv-plugin.js#L266-L344

Had some of those errors also today.
I've managed to fix them as I delayed the plugin init with 300ms

just pseudo-code

    init view......
        $scope.fixBackground = function(){

            $('.page__background').not('.page--menu-page__background').stop().velocity({
                backgroundColor: '#eee',
                backgroundColorAlpha: 0 
            });

        };

     $timeout(function(){
       ... init maps plugin
      $scope.fixBackground() // onsen specific
     $rootScope.map.refreshLayout()
     },300;)

real hacky but after 300 reloads I've never faced a white map again.

I think that you mean that I should initialize the getMap function and delay the return from plugin.google.maps.event.MAP_READY callback. I'm waiting for the update to xcode but will try that once done.

@Slavrix

In my index.html, I give my main ion-nav-view an id :

<ion-nav-view id="mainNavView"></ion-nav-view>

then in my map code :

 ` if( !init ) {
    document.getElementById("mainNavView").style.opacity = 0;
    $timeout(function() {
      document.getElementById("mainNavView").style.opacity = 1;
      init = true;
    });
  }`

Added the init variable so as to just do this once.

@hirbod after trying this native google maps plugin, there is no way that I could go back to the JS library :)

What is weird though is that I only call this plugin within a ionic.Platform.ready function so I would have assumed that all the DOM nodes are ready - seems like I am wrong.

@hirbod I've tried a few timings, I've placed a delay of 300ms and the same result, however when I did 1000ms (or 1sec) the display issue seems to be resolved.

I will do some more testing, and will look at googlemaps_cdv_plugin.js more closely.

I would think that MAP_READY would not return until it was truly ready.

The problem still occurs though not as frequently. I will have to check more later.

Hi @hirbod, I have been looking more into this issue.

I've changed the delay to 3000ms and this seems to solve the display issue on iOS, however it is not a very robust solution as I don't know why this works..

Looking for a more robust solution. When I looked at the googlemaps-cdv-plugin.js, I didn't notice anything that seemed odd, except for

  cordova.exec(function() {
    setTimeout(function() {
      self.refreshLayout();
      self.trigger(plugin.google.maps.event.MAP_READY, self);
    }, 100);
  }, self.errorHandler, PLUGIN_NAME, 'getMap', args);
  return self;

Which is toward the end of the App.prototype.getMap. This delay, while small is odd as you would assume that getMaps has completely returned.

Also, the underlying objective c call to getMap in GoogleMaps.m does processing for the the webView and sets the backgoundColor


    // Create a map view
    dispatch_async(gueue, ^{
      NSDictionary *options = [command.arguments objectAtIndex:0];
      self.mapCtrl = [[GoogleMapsViewController alloc] initWithOptions:options];
      self.mapCtrl.webView = self.webView;

      if ([options objectForKey:@"backgroundColor"]) {
        NSArray *rgbColor = [options objectForKey:@"backgroundColor"];
        self.pluginLayer.backgroundColor = [rgbColor parsePluginColor];
      }
    });

which I think is fine by itself. This is suppose to do things in the background that are trivial. I'm curious as to how quickly
self.mapCtrl = [[GoogleMapsViewController alloc] initWithOptions:options];
self.mapCtrl.webView = self.webView;

executes...

I ask this because right below the async call is

    // Create an instance of Map Class
    dispatch_async(gueue, ^{
      Map *mapClass = [[NSClassFromString(@"Map")alloc] initWithWebView:self.webView];
      mapClass.commandDelegate = self.commandDelegate;
      [mapClass setGoogleMapsViewController:self.mapCtrl];
      [self.mapCtrl.plugins setObject:mapClass forKey:@"Map"];


      dispatch_sync(dispatch_get_main_queue(), ^{
        if ([command.arguments count] == 3) {
          [self.mapCtrl.view removeFromSuperview];
          self.mapCtrl.isFullScreen = NO;
          self.pluginLayer.mapCtrl = self.mapCtrl;
          self.pluginLayer.webView = self.webView;


         [self.pluginScrollView attachView:self.mapCtrl.view];
          //[self.pluginScrollView addSubview:self.mapCtrl.view];
          [self resizeMap:command];
        }


        CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
        [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];

      });
    });
  }

Which is significant because it has the potential to use self.mapCtrl before it has not been allocated, or is this a "red-herring", and the timing is insignificant?

At this point, I would have to look in xcode and see the timings to make sure these are fine, and read more on the iOS documentaion, but I was wondering if you had insight if this could be a problem, as I know very little of xcode and iOS native programming.

Hey guys,

@hirbod

I am a very similar problem (sorry to bring this up again). I'm using this code:

         var mapDiv = document.querySelector("#map_canvas");
                // Initialize the map plugin
                var map = plugin.google.maps.Map.getMap(mapDiv);

        map.on(plugin.google.maps.event.MAP_READY, 
            function() {
               console.log("showed");
            }
        )
}, 5000);```

No map is showed at this point. I also don't see the documentation how to limit the higher and width of the view - is this possible?

Greg

@gregavola I placed the timeout after the return of MAP_READY. It appears that there is some possible delay issues so that when the MAP_READY is returned, it really isn't ready. I would change your code to

            map.on(plugin.google.maps.event.MAP_READY, setTimeout(
                function(){
                   console.log("showed");
                },5000));

or something like that. Please let me know if that works for you. I found that 3000 seemed to be optimal in my case.
As for the height and width of the view (the map I assume) that is done through styling #map_canvas

I got another fix (for now, till I find the real issue for this)

        $scope.fixBackground = function(){

            $('.page__background').not('.page--menu-page__background').stop().velocity({
                backgroundColor: '#eee',
                backgroundColorAlpha: 0 
            });

          // you could also use .animate() or just .css() - but my fixBackground hack is just for OnsenUI

        };

            $timeout(function(){

                var mapDiv = document.getElementById("map_canvas");

                if($('#map_canvas').length) {
                    $('#map_canvas').css('height', (($(window).height() - $('#map_canvas').offset().top - $('.ons-tabbar-inner').height())));
                }


                $scope.tilt = 50;
                $scope.maptype = plugin.google.maps.MapTypeId.ROADMAP;

                $rootScope.map = plugin.google.maps.Map.getMap(mapDiv, {
                    'mapType': $scope.maptype,
                    'controls': {
                        'compass': true,
                        'myLocationButton': false,
                        'indoorPicker': false,
                        'zoom': false
                    },
                    'gestures': {
                        'scroll': true,
                        'tilt': true,
                        'rotate': true
                    }
                });

                $rootScope.map.one(plugin.google.maps.event.MAP_READY, $scope.onMapInit);

            }, 300); 

        $scope.onMapInit = function(){

            $scope.fixBackground();
            $rootScope.map.clear(); // this fixed everything for me

            // now add marker
            ....
            ....

        });

First I've inited my map inside of a small timeout (300ms), then I will call a one-time trigger of the map-ready event, and then I call the clear() function (even when there is nothing)

This is for sure a hack, but I will try to incorporate some of my thoughts into the final .js

But I do not have any white map issues anymore

By the way, I also call something on deviceready (e.g. angular.run())

        // we need this workaround because of location.reload
        window.onbeforeunload = function(e) {
            if($rootScope.map) {
                $rootScope.map.clear();
                $rootScope.map.remove();
            }

        };

        plugin.google.maps.Map.clear();  // just call on every app-start. Seemed to fix many problems.

The unbeforeunload-trigger is important if you want to use cordova-hot-code-push or when you just call a window.location.reload() - this will fix multiple map views being added.

@celron

I just removed all the dispatch_async and dispatch_sync wraps and I do not see any issues and the bug is gone. I will release the next version without the call and we will see how it works..

Hope my commit will fix the problems

quick question what type of problems does calling Map.clear() do?

It just remove the markers without removing the map

But you had a comment that it fixed many problems on app start.

Yes, in case of a reload: window.location.reload()

Greetings @hirbod , I have been testing 1.3.3 and 1.3.4 with iOS9, and on the iPhone, the problem seems to be resolved.
However, on an iPad, it seems to display only half of the map until I rotate the display (from portrait landscape). This half display does not always happen, and I'm not sure why it seems to only happen on an iPad..
As a hacky workaround, forcing the rotation of the screen seems to fix the problem, which maybe of use to someone else who is encountering this problem and is unsure how to fix it.
I will try to manipulate other things to see if I can get the display to correctly appear.

The issue was not fixed even with the latest commit.

I noticed that sometimes (especially if the internet is slow), webViewDidFinishLoad() in CDVViewController.m gets called AFTER the google map is initialized, which is the root of the problem. By delaying plugin.google.maps.Map.getMap() till AFTER webViewDidFinishLoad(), the issue was solved.
In order to detect that the webview finished loading in javascript, you can add the following inside webViewDidFinishLoad():

NSString* jsString = [NSString stringWithFormat:@"yourJavascriptFunction('%s');", ""];
[self.commandDelegate evalJs:jsString];

And then once yourJavascriptFunction() is called, you can safely call plugin.google.maps.Map.getMap().

I hope this helps someone.

Thanks, but this would require to patch Cordova files. Actually I thought device ready should be safe enough. Maybe there is any way to detect webviewdidload inside of the maps plugin. But what you say seems legit. I also faced some problems (1 of 20 loads resulted in white screen). Weird...

Turns out this isn't a full-proof solution. I am still getting a white map sometimes. Working to fix this.

Looks like this is a Google Bug. (as it started to happen since 1.10)
Google has released 1.11.0 today, in the changelogs they have a "fixed blank map" note.

I will update to 1.11.0 now

Bitcode is now included in the SDK binary for all device architectures.
Added Place Autocomplete widget classes.
New events for long press on an info window, and closing an info window, have been added to GMSMapViewDelegate.
GMSMapViewDelegate has new events to indicate when map tiles and labels are pending and finished rendering.
GMSPanoramaViewDelegate has new events to indicate when panorama tiles are pending and finished rendering.
GMSGroundOverlay now supports an alpha multiplier via the opacity property.
Added a holes property to GMSPolygon to allow for the subtraction away from the filled area in order to create more complex shapes.
At zoom levels greater than 14, the maximum tilt has been increased.
Added an autocomplete screen to the Place Picker.
Split autocomplete predictions into primary and secondary text fields.
Added a country filter option to GMSAutocompleteFilter.
Added a viewport field to GMSPlace.

Resolved Issues:

Correct handling of taps on overlapping markers.
Address a race condition dependent crash which can happen when an application enters and leaves the background while showing a map.
Fix blank maps which can happen when launching an app into the background.
Workaround issues with core animation that caused markers to jump.
Updated to avoid subtle conflicts with applications which use google-toolbox-for-mac.
Use the iPhone language instead of the region formatting language for Places API results.

1.3.6 is out. Make sure that the SDK-Plugin will be removed when you call cordova plugin rm plugin.google.maps (even when there is a Uh Oh! Error, thats fine). I tried at least 100 App starts. Not even one white map. Looks like it is fixed.