mapsplugin / cordova-plugin-googlemaps

Google Maps plugin for Cordova
Apache License 2.0
1.66k stars 916 forks source link

Performance problems when adding many markers. #835

Closed atannus closed 8 years ago

atannus commented 8 years ago

I'm proposing a patch to the PluginMarker.java file that will greatly improve the performance when adding hundreds of markers to the map.

I propose the creation of a createMarkers method (note the plural), which will take the same marker definition as does the currently available createMarker, except for the fact they'll be in an array, therefore allowing the creation of multiple markers with a single over-the-bridge request. Naturally, the callback has to return an array of markers, instead of a single marker.

A private createMarker_ method is to be called by createMarkers. This does not break the current API, and the current createMarker method can be refactored to simply call the private version with its argument in an array.

I have already implemented marker caching and marker pre-loading which on top of that will drastically reduce memory consumption and prevent leaks.

This is where my understanding of this project ends. I don't know what my constraints as a developer are:

I'm seeing fantastic performance with these improvements, I'd really like to contribute them.

Thanks a lot for the help.

hirbod commented 8 years ago

The APIs aren't frozen, but I would like to have a heterogeneous environment. iOS and android should be the same level if possible. While the clustering branch resolves most of the memory problems, I would like to incorporate your suggestions, cause not everybody need clustering and the cluster branch has a lot less functionality currently. Once the funding is done, it will be released as own setup, but I would incorporate your edits there, too (ass the clustering can be deactivated by config).

So I'm open for everything. I'm not the lead developer of this plugin. Masashi was, but he dropped it and I took over maintenance and fixed bugs and merged good PRs and incorporated some fixes and functions by myself. So I'm very thankful if someone would help me here.

atannus commented 8 years ago

Can I see the clustering branch? Perhaps I should focus on these changes on that.

hirbod commented 8 years ago

Sure. Are you on Skype? We should chat

atannus commented 8 years ago

I've though about this.

A cluster is just one marker where many would be, and it should be the end-user's job to cluster, not the plugin's. The user has the added benefit of being able to pick the clustering method which by the ways begs the question: which are yo using? Are you going to provide multiple? Or perhaps the injection of a clustering interface?

The questions on their own indicate that clustering should be handled outside the plugin. However you answer them, notice the plugin's clustering code will have to know about all the markers the user is creating, plus the clustering method the user wants to use and if the user wants a different clustering method they'll have to bake it in.

Clustering is a black box that takes a bunch of markers and returns one single marker. I would provide clustering as a factory method, to be used as:

var positions = [p1, p2, p3];
var clusterer = new plugin.google.maps.MarkerClusterer(opt_methodName);
var clusterPosition = clusterer.cluster(positions);

Where positions is an array of objects that expose lats and longs via properties or methods (i.e. objects that implement a certain Interface). Remember they're not likely to be the formal Marker objects, since we have not created them yet.

The clusterer will return one single such object exposing a lat-lng pair, which will be used to create the "real" Marker on the map.

This can take advantage of the changes I've made since the user can use the clusterer object to cluster multiple sets of positions outside the plugin then create all Markers (one for each cluster) at once.

We can also have it so that the user injects the clusterer as a function that takes markers and returns marker, both as described above. The user then calls some function with the data that to be clustered, and the function uses the injected clusterer. Once again, this puts into the plugin data that does not belong there.

These are just quick thoughts. Perhaps I miss the point entirely. Which way are you going?

hirbod commented 8 years ago

Test it:

cordova plugin add https://github.com/mapsplugin/cordova-plugin-googlemaps#clusterer --variable API_KEY_FOR_ANDROID="YOURKEY" --variable API_KEY_FOR_IOS="YOURKEY"

Add following config object to .getMap()

                'controller': {
                    'clustering': true,
                    'rendering': 'animated',
                    'algorithm': 'nonHierarchicalDistanceBasedAlgorithm'
                }

eg

            $rootScope.map = plugin.google.maps.Map.getMap(mapDiv, {
                'controls': {
                    'compass': false,
                    'myLocationButton': false,
                    'indoorPicker': false,
                    'toolbar': false,
                    'zoom': false
                },
                'gestures': {
                    'scroll': true,
                    'tilt': true,
                    'rotate': true
                },
                'controller': {
                    'clustering': true,
                    'rendering': 'animated',
                    'algorithm': 'nonHierarchicalDistanceBasedAlgorithm'
                }
            });

Edit: Btw, currently only nonHierarchicalDistanceBasedAlgorithm supported.

hirbod commented 8 years ago

As soon as the user has zoomed enough, the real markers will be created.

bildschirmfoto 2016-02-03 um 16 35 02

Same for iOS

bildschirmfoto 2016-02-03 um 16 36 45

A real app to see it in action https://itunes.apple.com/us/app/a-z-erdgastankstellen/id441500868?mt=8&ls=1 https://play.google.com/store/apps/details?id=de.lf.erdgas&hl=de

Not my app, but its the same codebase which is incorporated.

The Cluster branch works, but it is a bit hacky - and generally this plugin needs a lot refactoring (but I don't have time for that). If you have time and the mood to provide a better clustering solution, I'm open minded to follow your path and spending all donations to you, if you provide a better way for both systems.

RegexLLC commented 8 years ago

Without clustering, I'm using a mix of KML overlay for the bulk of the 'noise' and plain css markers placed over top of the mapdiv together.

dbesiryan commented 8 years ago

the clustering layers (orange markers) are showing wrong numbers. Always double of the real marker-value is shown. Any ideas?

hirbod commented 8 years ago

dbesiryan - I'm not supporting something that isn't officially released. This branch is not finished

Slavrix commented 8 years ago

I'd love to see this feature added. In an app I'm current working on, we have thousands of markers and the client doesn't want clustering. On ios so far it has been working ok, but on Android it lags out the app as it draws them all (calling add marker for each one inside a foreach) Deming the shore array to the plugin would be much better

RegexLLC commented 8 years ago

Could I maybe see your app that loads thousands of markers?

Slavrix commented 8 years ago

I'll set-up a test app with how I'm doing it over the weekend. Sorry it took a while to respond.

chrigi commented 8 years ago

@atannus Do you have a fork containing your changes? Have you implemented the bulk marker adding, caching and pre-loading for Android & iOS or only für Android?

atannus commented 8 years ago

No, I made changes directly on the version installed inside my project.

Is this project still being held hostage for funds? This throws me off.

chrigi commented 8 years ago

It appears so. There is a clusterer branch but I'm unsure about how complete that is. It would be great if you could commit your changes to your fork (and maybe even open a pull request) if you want to share your work.

atannus commented 8 years ago

I'll see what I can do.

On Thu, Mar 3, 2016 at 10:41 AM, Christian notifications@github.com wrote:

It appears so. There is a clusterer branch but I'm unsure about how complete that is. It would be great if you could commit your changes to your fork (and maybe even open a pull request) if you want to share your work.

— Reply to this email directly or view it on GitHub https://github.com/mapsplugin/cordova-plugin-googlemaps/issues/835#issuecomment-191764409 .

André Tannús | Epungo | +55 11 2389-4360 We are a layer

atannus commented 8 years ago

I'm on this now. Is there a suggested development flow? IDE? Should I edit the plugin while installed within a project or change->(re)install->test? Thanks.

On Thu, Mar 3, 2016 at 10:44 AM, Andre Tannus andre.tannus@gmail.com wrote:

I'll see what I can do.

On Thu, Mar 3, 2016 at 10:41 AM, Christian notifications@github.com wrote:

It appears so. There is a clusterer branch but I'm unsure about how complete that is. It would be great if you could commit your changes to your fork (and maybe even open a pull request) if you want to share your work.

— Reply to this email directly or view it on GitHub https://github.com/mapsplugin/cordova-plugin-googlemaps/issues/835#issuecomment-191764409 .

André Tannús | Epungo | +55 11 2389-4360 We are a layer

André Tannús | Epungo | +55 11 2389-4360 We are a layer

hirbod commented 8 years ago

Well calling this a hostage is not very fine. And it's just for the cluster-branch. The cluster branch which is avaiable here "inoffically" was a paid job. It was just improved and cleaned by me. I just was about to give some money back to the code donator. If you guys help to finish this cluster branch, I'm open for it. The branch is not hidden, try it, fix it, improve it.

atannus commented 8 years ago

Is there a suggested development flow?

I have the plugin project checked out, so I have to make changes to that, uninstall/reinstall (so that the Cordova project picks up the changes), test, repeat. Also, (re)installing is a pain because the plugin name doesn't match the manifest name (or I'm doing something wrong).

Is there a better workflow than this? It's a pain...

Thanks. AT

cvaliere commented 8 years ago

Hi @atannus

The development flow I would suggest is to edit the plugin while installed: use a very simple Ionic app that uses this map, run "ionic run android", then open the Android project into the IDE you like (eg. Android Studio) and edit directly in here with live reload allowed by Android Studio.

For clusters, I think you missed the point: the goal is to let the map decide, depending on the zoom level, to show individual markers or to cluster them. If you "create" the clusters outside the plugin, how is it supposed to work? Would you, for every zoom event, recalculate the clusters yourself?

Please tell me if I can help you

atannus commented 8 years ago

Thanks @cvaliere.

On Tue, Mar 15, 2016 at 11:19 AM, cvaliere notifications@github.com wrote:

Hi @atannus https://github.com/atannus

The development flow I would suggest is to edit the plugin while installed: use a very simple Ionic app that uses this map, run "ionic run android", then open the Android project into the SDK you like (eg. Android Studio) and edit directly in here with live reload allowed by Android Studio.

For clusters, I think you missed the point: the goal is to let the map decide, depending on the zoom level, to show individual markers or to cluster them. If you "create" the clusters outside the plugin, how is it supposed to work? Would you, for every zoom event, recalculate the clusters yourself?

Please tell me if I can help you

— You are receiving this because you were mentioned. Reply to this email directly or view it on GitHub:

https://github.com/mapsplugin/cordova-plugin-googlemaps/issues/835#issuecomment-196842172

André Tannús | Epungo | +55 11 2389-4360 We are a layer

PapyElGringo commented 8 years ago

Can you share the createMarkers method with us? I think it's would be a great evolution.

cvaliere commented 8 years ago

Hi @atannus

+1 for sharing createMarkers, I would definitely use it

thanks !

atannus commented 8 years ago

Hi guys.

Thanks for the support.

Aside from createMarkers, changes to the bridge call are also required. In fact, I created a whole new bridge call for this performance issue. Also, there's the need to keep track of the created markers, which I've done in an experimental manner. I consider the code I wrote a hack up, and I'm not comfortable sharing it just yet, especially since I did not bake in support for the various different ways to provide de marker image.

Last week I put some time into making these changes decent enough to contribute, but I could not finish the work.

I'll post something soon, please be patient.

On Fri, Mar 18, 2016 at 1:27 PM, cvaliere notifications@github.com wrote:

Hi @attanus

+1 for sharing createMarkers, I would definitely use it

thanks !

— You are receiving this because you were mentioned. Reply to this email directly or view it on GitHub https://github.com/mapsplugin/cordova-plugin-googlemaps/issues/835#issuecomment-198436813

André Tannús | Epungo | +55 11 2389-4360 We are a layer

atannus commented 8 years ago

Hi guys.

I've pushed the proposed changes to my fork, under branch develop/multiple-markers. https://github.com/atannus/cordova-plugin-googlemaps/tree/develop/multiple-markers

Support is still very limited, and only tested with URL icons. You must preload icons in order to achieve the performance enhancement. If you don't, icons won't be cached and will be loaded individually, just as if you were adding markers one by one.

Here is some code that demonstrate the plugin in action. Remeber to change the icon paths to something real.

var map;
var icons = [
    "http://some.thing/path/to/image1.png"
];

document.addEventListener("deviceready", function () {

    // Initialize the map.
    var div = document.getElementById("map_canvas");
    map = plugin.google.maps.Map.getMap(div);
    var initPosition = new plugin.google.maps.LatLng(-23.548, -46.5745);
    map.addEventListener(plugin.google.maps.event.MAP_READY, function (map) {

        // Post-preload callback.
        var donePreloading = function () {
            // Animate camera.
            map.animateCamera({
                'target': initPosition,
                'zoom': 16,
                'duration': 1000
            },
                    function () {
                        placeMarkers();
                    })
        };

        // Preload images.
        map.preloadImages(icons, function (results) {
            donePreloading();
        });
    });
}, false);

placeMarkers = function () {
    var markerDefinitions = createMarkers();
    map.addMarkers(markerDefinitions, function (createdMarkers) {
        console.log(createdMarkers);
    });
}

createMarkers = function () {
    var markers = [];
    for (var i = 1; i < mockData.length; i++) {
        var mock = mockData[i];
        var position = new plugin.google.maps.LatLng(mock.lat, mock.lng);
        var markerDefinition = {
            'position': position
            , 'icon': {
                'url': icons[0],
                'size': {
                    'width': 32,
                    'height': 32
                }
            }
        };
        markers.push(markerDefinition);
    }
    return markers;
}

var mockData = [
    {
        lat: "-23.548922600000000",
        lng: "-46.574610800000000",
    },
    {
        lat: "-23.549337900000000",
        lng: "-46.574795600000000",
    },
    {
        lat: "-23.550828933700000",
        lng: "-46.576023101800000",
    },
    {
        lat: "-23.550889500000000",
        lng: "-46.576046300000000",
    },
    {
        lat: "-23.551199100000000",
        lng: "-46.575804400000000",
    },
    {
        lat: "-23.550761500000000",
        lng: "-46.575932700000000",
    },
    {
        lat: "-23.550820100000000",
        lng: "-46.575986100000000",
    },
    {
        lat: "-23.550958200000000",
        lng: "-46.576498800000000",
    },
    {
        lat: "-23.550552368200000",
        lng: "-46.574832916300000",
    },
    {
        lat: "-23.550529480000000",
        lng: "-46.575012207000000",
    },
    {
        lat: "-23.549200058000000",
        lng: "-46.573200225800000",
    },
    {
        lat: "-23.549949646000000",
        lng: "-46.572673797600000",
    },
    {
        lat: "-23.550093900000000",
        lng: "-46.572460600000000",
    },
    {
        lat: "-23.550067000000000",
        lng: "-46.572427400000000",
    },
    {
        lat: "-23.549314300000000",
        lng: "-46.571722400000000",
    },
    {
        lat: "-23.548916800000000",
        lng: "-46.572370800000000",
    },
    {
        lat: "-23.550040200000000",
        lng: "-46.571646100000000",
    },
    {
        lat: "-23.551219500000000",
        lng: "-46.571387300000000",
    },
    {
        lat: "-23.550374984700000",
        lng: "-46.571079254200000",
    },
    {
        lat: "-23.551379900000000",
        lng: "-46.572379600000000",
    },
    {
        lat: "-23.551279068000000",
        lng: "-46.571372985800000",
    },
    {
        lat: "-23.550819700000000",
        lng: "-46.571475300000000",
    },
    {
        lat: "-23.550776800000000",
        lng: "-46.571910200000000",
    },
    {
        lat: "-23.551599502600000",
        lng: "-46.575599670400000",
    },
    {
        lat: "-23.551599502600000",
        lng: "-46.575599670400000",
    },
    {
        lat: "-23.552324295000000",
        lng: "-46.575397491500000",
    },
    {
        lat: "-23.551766600000000",
        lng: "-46.575586100000000",
    },
    {
        lat: "-23.551599502600000",
        lng: "-46.575599670400000",
    },
    {
        lat: "-23.552499771100000",
        lng: "-46.574798584000000",
    },
    {
        lat: "-23.552565800000000",
        lng: "-46.574789700000000",
    },
    {
        lat: "-23.552565800000000",
        lng: "-46.574789700000000",
    },
    {
        lat: "-23.551799774200000",
        lng: "-46.573898315400000",
    },
    {
        lat: "-23.551788330100000",
        lng: "-46.574462890600000",
    },
    {
        lat: "-23.551799774200000",
        lng: "-46.573898315400000",
    },
    {
        lat: "-23.553100585900000",
        lng: "-46.575500488300000",
    },
    {
        lat: "-23.553400039700000",
        lng: "-46.575199127200000",
    },
    {
        lat: "-23.553100500000000",
        lng: "-46.574628900000000",
    },
    {
        lat: "-23.552700042700000",
        lng: "-46.573799133300000",
    },
    {
        lat: "-23.553236007700000",
        lng: "-46.574249267600000",
    },
    {
        lat: "-23.553236007700000",
        lng: "-46.574249267600000",
    },
    {
        lat: "-23.552700042700000",
        lng: "-46.573799133300000",
    },
    {
        lat: "-23.553236007700000",
        lng: "-46.574249267600000",
    },
    {
        lat: "-23.552421900000000",
        lng: "-46.573419800000000",
    },
    {
        lat: "-23.552553176900000",
        lng: "-46.573181152300000",
    },
    {
        lat: "-23.552079300000000",
        lng: "-46.572764700000000",
    },
    {
        lat: "-23.552452087400000",
        lng: "-46.573001861600000",
    },
    {
        lat: "-23.552553176900000",
        lng: "-46.573181152300000",
    },
    {
        lat: "-23.552080154400000",
        lng: "-46.572765350300000",
    },
    {
        lat: "-23.552339553800000",
        lng: "-46.571506500200000",
    },
    {
        lat: "-23.552373800000000",
        lng: "-46.571509900000000",
    },
    {
        lat: "-23.551967620800000",
        lng: "-46.571239471400000",
    },
    {
        lat: "-23.553024292000000",
        lng: "-46.572566986100000",
    },
    {
        lat: "-23.553100585900000",
        lng: "-46.572799682600000",
    },
    {
        lat: "-23.553188324000000",
        lng: "-46.573322296100000",
    },
    {
        lat: "-23.553508758500000",
        lng: "-46.573505401600000",
    },
    {
        lat: "-23.552674900000000",
        lng: "-46.573789000000000",
    },
    {
        lat: "-23.552713600000000",
        lng: "-46.573731100000000",
    },
    {
        lat: "-23.553152200000000",
        lng: "-46.571818300000000",
    },
    {
        lat: "-23.553367614700000",
        lng: "-46.571437835700000",
    },
    {
        lat: "-23.553100585900000",
        lng: "-46.571701049800000",
    },
    {
        lat: "-23.553808212300000",
        lng: "-46.571708679200000",
    },
    {
        lat: "-23.553110600000000",
        lng: "-46.571771400000000",
    },
    {
        lat: "-23.553100585900000",
        lng: "-46.571701049800000",
    }
];
wf9a5m75 commented 8 years ago

Hi everyone, this is Masashi who is the original author of this plugin. I have already known this issue, many people crime to me since old versions, and I know what I should do. I left this project at once (because it was crazy busy), but I came back to this project again. Now, I have been fixing the issues currently filed one bye one. I will buckle down this issue also. Hold on please.

cvaliere commented 8 years ago

Thank you @atannus for your work Hi @wf9a5m75 it's good to have you back :) Do you have an idea when you can release a new version?

wf9a5m75 commented 8 years ago

Sorry I have no idea when next is released at this point.

atannus commented 8 years ago

I'm happy to help with anything I can.

wf9a5m75 commented 8 years ago

Thank you for waiting. The one of the big problem is that the plugin uses reflection technique both Android and iOS. Reflection is useful technique for me, because any methods are executable like pointer, but it is slow.

And thank you for testing. This might effect. https://github.com/cabify/cordova-plugin-googlemaps/blob/d6192536b02093a66f2209580cf8532aca2793e5/src/android/plugin/google/maps/GoogleMaps.java#L792-L825

hirbod commented 8 years ago

@wf9a5m75 should we port that changes?

wf9a5m75 commented 8 years ago

Not test it yet.

guillenotfound commented 8 years ago

Here an example of performance on Android, quite annoying: https://drive.google.com/open?id=0BzzwDw5jEO0xMnhNeklrN2NJaG8

Same amount of markers on iOS works much better, I can try to share a demo video on iOS if you need it as well.

hirbod commented 8 years ago

Well, there are just too many markers. This will happen with every implementation without clustering. I don't know how much time it will take to make @wf9a5m75 clustering-plugin to work fine with the current codebase. I don't think that the Codechanges mentioned above will help - but you could try (but as far as I can see, this only take place when resizing)

wf9a5m75 commented 8 years ago

I have been working on performance optimization. It's just started a little, but the optimization definitely works with the same code before and after.


test code

Add 200 markers on the map.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1">
      <script type="text/javascript" src="cordova.js"></script>
      <script type="text/javascript">
        // 200 location data 
        var data = {"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[135.5568524,34.580776]}},...,{"type":"Feature","geometry":{"type":"Point","coordinates":[139.616858,35.432656]}}]};

        var map;

        document.addEventListener("deviceready", function() {
          var div = document.getElementById("map_canvas");

          var POIs = data.features.map(function(feature) {
            return {
              position: {lat: feature.geometry.coordinates[1], lng: feature.geometry.coordinates[0]},
              icon: "https://iai-dojo.jp/modules/dojo/img/flag9.gif"  // <-- In this test code, use the same icon for all.
            }
          });

          // Create bounds
          var bounds = POIs.map(function(poi) {
            return poi.position;
          });

          // Initialize the map view
          map = plugin.google.maps.Map.getMap(div, {
            camera: {
              target: bounds
            }
          });

          // Wait until the map is ready status.
          map.addEventListener(plugin.google.maps.event.MAP_READY, function() {
            var start = Date.now();
            addMarkers(POIs, function(markers) {
              var end = Date.now();
              alert(markers.length + " markers are added : " + ((end - start) / 1000) + " sec.")
            });
          });
        }, false);

        function addMarkers(data, callback) {
          var markers = [];
          function onMarkerAdded(marker) {
            markers.push(marker);
            if (markers.length === data.length) {
              callback(markers);
            }
          }
          data.forEach(function(markerOptions) {
            map.addMarker(markerOptions, onMarkerAdded);
          });
        }

      </script>
      </head>
  <body>
    <h3>PhoneGap-GoogleMaps-Plugin</h3>
    <div style="width:100%;height:500px" id="map_canvas"></div>
  </body>
</html>

Before

before

After

after


The optimization is under construction. Please wait for a while for public beta.

wf9a5m75 commented 8 years ago

It's turn on the iOS. I use the same test code above.

While the current iOS code is not so slow, but the optimized version is more faster.

Before (current master branch)

before

After

after

cvaliere commented 8 years ago

hi @wf9a5m75 your optimization seems very promising! can't wait to have it :)

guillenotfound commented 8 years ago

Will this improve performance while dragging as well?

wf9a5m75 commented 8 years ago

@ZiFFeL1992 Which dragging action do you ask, marker or map?

guillenotfound commented 8 years ago

@wf9a5m75 map, sorry for not specifying before :/

wf9a5m75 commented 8 years ago

@ZiFFeL1992 Current optimization process does not related with the map dragging behavior. In the feature, I will optimize that.

cvaliere commented 8 years ago

hi @wf9a5m75 currently using your optimization branch for Android, it works, and it rocks, thanks for your work! we would like to do the same for iOS, but optimization branch crashes; do you know when you release it? would it help if we give you a snippet that makes it crash?

thanks!

guillenotfound commented 8 years ago

I'm getting 'getMap' is not defined in GoogleMaps plugin. -> BaseClass.js:124. Optimization branch on Android.

wf9a5m75 commented 8 years ago

I don't support using the optimization branch. It's under construction.

cvaliere commented 8 years ago

hi @wf9a5m75 currently using your optimization branch for Android, it works, and it rocks, thanks for your work! we would like to do the same for iOS, but optimization branch crashes; do you know when you release it? would it help if we give you a snippet that makes it crash?

thanks!

wf9a5m75 commented 8 years ago

As I said the above, I don't support using the optimization branch. It's under construction. No ETA.

cvaliere commented 8 years ago

hi @wf9a5m75 I don't want to be an asshole, but what's the point of so much time? I mean, I have the impression that the optimization branch is already OK on Android, and very close to be OK on iOS. If you need help, you could ask for help, but instead you go like "It's OK guys, I'll handle it all by myself"... but it's been 6 weeks now. Can we do something to help? If so, please ask. If not, could you release this optimization we're all waiting for?

wf9a5m75 commented 8 years ago

Sorry for late, but keep waiting please. I have been developing the feature of multiple maps currently.

multiple_maps

cvaliere commented 8 years ago

hi @wf9a5m75 my usual ping for you :) do you have an ETA now?

cvaliere commented 8 years ago

hi @wf9a5m75 you sure know how to handle suspense seriously, what can we do to help?