e-mission / e-mission-docs

Repository for docs and issues. If you need help, please file an issue here. Public conversations are better for open source projects than private email.
https://e-mission.readthedocs.io/en/latest
BSD 3-Clause "New" or "Revised" License
15 stars 34 forks source link

Maps tiles are sometimes only partially loaded on android #111

Closed shankari closed 2 years ago

shankari commented 8 years ago

Sometimes, the app has this weird behavior on android in which the map tiles are only partially loaded. Reloading the data does not change anything - the issue is only fixed when the app is killed and reloaded.

This issue was recently re-reported by @immesys, so let's track the issue and try to fix it before we get out of beta.

shankari commented 2 years ago

Let's poke around a bit to see if we can find the map since that would be the next fix if resetting the geojson does not work.

shankari commented 2 years ago

ok so I think that I have a reproducible error case:

opened from label broken map
Screen Shot 2022-04-27 at 11 25 53 PM Screen Shot 2022-04-27 at 11 26 53 PM

Note that not even the trajectories are correct any more

Yay! Reproducible on physical phone as well.

shankari commented 2 years ago

So one of the issues, and the reason why we can't look up the map, is because we use collection-repeat. This means that a bunch of items (including maps) are created ahead of time. This means that in the ui-leaflet module, all the keys are "map_". The ids change when we load the diary for a day, but the keys are not remapped since we don't watch on the id.

But I'm still not sure if this is related to the broken behavior. The popup map has a static ID (detailPopoverMap) so it shouldn't conflict with the map_ keys.

Let's see if this is due to the geojson...

shankari commented 2 years ago

Just before we show the detail, we set the geojson. And then we invalidate size. Let's start by commenting out the invalidate.

  $scope.showDetail = function($event, trip) {
    Timeline.confirmedTrip2Geojson(trip).then((tripgj) => {
        $scope.currgj = trip;
        $scope.currgj.data = tripgj;
        $scope.currgj.pointToLayer = DiaryHelper.pointFormat;
        $scope.tripDetailPopover.show();
        leafletData.getMap("detailPopoverMap").then(function(map) {
            map.invalidateSize();
        });
    });
  }
shankari commented 2 years ago

Removed the invalidateSize, did not fix the issue. Still broken if:

If there is no list in the diary view, the maps are initialized correctly when going from label -> map -> diary. Once the diary maps are in "a bad state", they stay in a bad state as we move between days.

Just to clarify, this only happens after the maps are initialized. Concretely:

everything is broken.

This might just be related to the map_ id issue given that it occurs only after the IDs change.

shankari commented 2 years ago

Since it is not the invalidateSize it must be the geojson. I still don't see why the maps are linked. After a time-bounded investigation, we may want to use a different way of displaying the label map, similar to the detail screen, to avoid this issue.

shankari commented 2 years ago

Maybe related to this:

                    if(!_hasSetLeafletData){//only do this once and play with the same ref forever
                        _hasSetLeafletData = true;
                        leafletData.setGeoJSON(leafletGeoJSON, attrs.id);
                    }
shankari commented 2 years ago

Couple of fixes for the map_ issue:

Given that the id seems to be a foundation of leafletData, the second option seems like the safer option.

shankari commented 2 years ago

making the id depend on the index still doesn't solve it because the items don't have an index until they are filled in.

Blank screen

Screen Map
Screen Shot 2022-04-29 at 10 13 19 AM Screen Shot 2022-04-29 at 10 13 44 AM

Loaded screen

Screen Map
Screen Shot 2022-04-29 at 10 15 23 AM Screen Shot 2022-04-29 at 10 15 04 AM
shankari commented 2 years ago

Switched to ng-repeat to see if that helps.

Blank list

Screen Shot 2022-04-29 at 11 11 25 AM

Hit leaflet directive breakpoints with map_0 through map_8 Hit geojson directive breakpoints with map_0 through map_8

Populated list

Screen Shot 2022-04-29 at 11 15 58 AM

So it is not the map_x mapping.

Going to give up for now and try to use something similar to the detail screen instead of a popup. We might end up using images in the future anyway... This also makes it closer to the list view prior to unifying them.

shankari commented 2 years ago

I think I didn't do that before because the detail screen looked up the tripgj from the cached list, and we weren't caching the trip objects from the infinite scroll list. But after unifying the code, tracked in https://github.com/e-mission/e-mission-docs/issues/674, fixed in https://github.com/e-mission/e-mission-phone/pull/799, this should be simpler?

shankari commented 2 years ago

ok, so there is a tripMap and a tripWrapperMap

      timeline.getTrip = function(tripId) {
        return angular.isDefined(timeline.data.tripMap)? timeline.data.tripMap[tripId] : undefined;
      };

      timeline.getTripWrapper = function(tripId) {
        return angular.isDefined(timeline.data.tripWrapperMap)? timeline.data.tripWrapperMap[tripId] : undefined;
      };

trip map is filled in for

       var processTripsForDay = function(day, tripListForDay) {
        console.log("About to show 'Processing trips'");
        $ionicLoading.show({
          template: 'Processing trips...'
        });
        tripListForDay.forEach(function(item, index, array) {
          console.log(index + ":" + item.properties.start_fmt_time+", "+item.properties.duration);

        });
        timeline.data.currDay = day;
        timeline.data.currDayTrips = tripListForDay;

        timeline.data.tripMap = {};

        timeline.data.currDayTrips.forEach(function(trip, index, array) {
          timeline.data.tripMap[trip.id] = trip;
        });

In the list view, we convert the trips to trip wrappers and store them as well.

          var currDayTripWrappers = Timeline.data.currDayTrips.map(
            DiaryHelper.directiveForTrip);
          Timeline.setTripWrappers(currDayTripWrappers);

However, we never set it from the infinite scroll list

shankari commented 2 years ago

Options are:

Let's start with the second option since:

shankari commented 2 years ago

we can't pass in the trip directly because the params are passed in as strings.

$stateParams
{tripId: '[object Object]'}
   tripId: "[object Object]"
   [[Prototype]]: Object

Falling back to the first option

shankari commented 2 years ago

The first option was fairly complex, but works! Looks like the popup was the key to messing up the other maps, although I don't quite understand why.

shankari commented 2 years ago

Experimented with the ability to label directly from the detail screens (commits will show up soon). Thanks to the prior modularization of the multi-label UI, this was fairly straightforward. HOWEVER, doing so brings back our bad-maps-in-list problem

Even if we set the values in the label detail, which uses a completely different list than the list view (see video below). And it happens as soon as we set any of the labels, even if we don't set both. No time to dig deeper into this; removing the labeling functionality for now pending a restructuring/unification later.

https://user-images.githubusercontent.com/2423263/166113481-50832f2f-d693-453b-97a4-7c4cca44b52e.mov

shankari commented 2 years ago

Argh! Terrible news! Even if I set the values from the infinite list, the map is reset. Maybe we have to switch to images or something...

https://user-images.githubusercontent.com/2423263/166125979-e5ce15ec-71c2-43c9-8af2-501f6a428e9b.mov

shankari commented 2 years ago

ok, so after adding breakpoints in the leaflet code directly, I can see the issue. The problem is that opening the popover (even the label popover) triggers a resize event. And leaflet calls invalidateSize on reset

    _onResize: function () {
        cancelAnimFrame(this._resizeRequest);
        this._resizeRequest = requestAnimFrame(
                function () { this.invalidateSize({debounceMoveend: true}); }, this);
    },

invalidateSize tries to compute the old size and the new size. All of this works as long as the map is visible.

However, if the map is not visible (because it is in a hidden tab), it has a new size of zero. Which means that we end up moving the map view, and breaking the list map.

        var newSize = this.getSize(),
            oldCenter = oldSize.divideBy(2).round(),
            newCenter = newSize.divideBy(2).round(),
            offset = oldCenter.subtract(newCenter);

        if (!offset.x && !offset.y) { return this; }

        if (options.animate && options.pan) {
            this.panBy(offset);

        } else {
            if (options.pan) {
                this._rawPanBy(offset);
            }

One simple hack might be to ignore invalidateSize if the new size is 0.

shankari commented 2 years ago

Reproduction case:

https://user-images.githubusercontent.com/2423263/166132288-1637cd3e-2b37-49fb-bd95-935095751917.mp4

shankari commented 2 years ago

Hacked around this by editing the leaflet src

                    oldCenter = oldSize.divideBy(2).round(),
                    newCenter = newSize.divideBy(2).round(),
                    offset = oldCenter.subtract(newCenter);
+
+        // Hack added by shankari to fix
+        // https://github.com/e-mission/e-mission-docs/issues/111#issuecomment-1114128934
+        if (newSize.x == 0 && newSize.y == 0) { return this; }

                if (!offset.x && !offset.y) { return this; }

That seems to work.

shankari commented 2 years ago

Tracking the longer-term fix in https://github.com/e-mission/e-mission-docs/issues/726

shankari commented 2 years ago

Still broken for the following use case:

https://user-images.githubusercontent.com/2423263/166133933-71d9881c-bb54-4103-ad5f-3a92eb047328.mp4

shankari commented 2 years ago

As I guessed, this is because the old zero size is cached somewhere and messes up the reload. It is cached because invalidateSize calls getSize to get the new size, and getSize caches the size

    getSize: function () {^M
        if (!this._size || this._sizeChanged) {^M
            this._size = new Point(^M
                this._container.clientWidth || 0,^M
                this._container.clientHeight || 0);^M
^M
            this._sizeChanged = false;^M
        }^M
        return this._size.clone();^M
    },^M

Changing the hack to read the container sizes directly without calling getSize works

        // Hack added by shankari to fix
        // https://github.com/e-mission/e-mission-docs/issues/111#issuecomment-1114128934
        // console.log("LEAFLET: clientWidth = "+this._container.clientWidth+" clientHeight = "+this._container.clientHeight + " combined: "+ (this._container.clientWidth && this._container._clientHeight));
        if ((this._container.clientWidth || 0) == 0 && (this._container.clientHeight || 0) == 0) { return this; }
shankari commented 2 years ago

Finally, we ensure that labels set in the diary screen are reflected in the label screen and vice versa. This ensures that, no matter which screen the user chooses, they see a consistent view of their trip information.

At this point, with https://github.com/e-mission/e-mission-phone/pull/823/commits/c302d65904662253214dce9c5ea966060b7e9a65, we can set values from:

and it will be reflected in all those areas without any further user input.

https://user-images.githubusercontent.com/2423263/166184862-3c4db6b1-f2b1-414f-901e-b9bdf4fc9839.mp4

shankari commented 2 years ago

Closing this issue for now Tracking the longer-term fix in conjunction with the leaflet maintainers at https://github.com/e-mission/e-mission-docs/issues/726