dotnet / maui

.NET MAUI is the .NET Multi-platform App UI, a framework for building native device applications spanning mobile, tablet, and desktop.
https://dot.net/maui
MIT License
22.16k stars 1.74k forks source link

Map Control, adding large number of pins issue #19906

Open MunteanuGabi opened 9 months ago

MunteanuGabi commented 9 months ago

Description

Adding a large number of pins to the map control takes an unreasonable amount of time and also the map freezes while doing the rendering for both Android and iOS. For example placing 1000 pins takes around 1 min to render. Obs1. During the freeze there are a lot of log messages regarding garbage collection like: [ark.android.dev] Explicit concurrent copying GC freed 102687(3555KB) AllocSpace objects, 0(0B) LOS objects, 49% free, 6444KB/12MB, paused 12us,10us total 12.616ms Obs2. If the number of pins is small(several dozens) then the rendering is instant. Obs3. On Android if the pins are added sooner than around 200ms then the pins adding works fine even with more than a thousand of pins, but adding them later than that results in freezing and the rendering issues mentioned Obs4. The old map from Xamarin forms works without any issues for the same large number of pins

Steps to Reproduce

Run the project and observe the map freezing and taking a lot of time for the pins rendering.

Link to public reproduction project repository

https://github.com/MunteanuGabi/MauiSamples

Version with bug

8.0.3

Is this a regression from previous behavior?

Not sure, did not test other versions

Last version that worked well

Unknown/Other

Affected platforms

iOS, Android

Affected platform versions

No response

Did you find any workaround?

No response

Relevant log output

No response

Beryl0 commented 9 months ago

I got around this by loading the data and doing the binding to the map before setting it to the Content of the page. Hopefully this helps and following to see if this gets improved.

`

public GritBinsPage(GritBinsViewModel viewModel) : base(viewModel)
{
    Title = "Grit Bins"
    _viewModel = viewModel;
    BindingContext = _viewModel;
    Content = LoadingPageTemplate.CreateLoadingTemplate("Grit Bins page");    
}
protected override async void OnNavigatedTo(NavigatedToEventArgs args)
{
    await InitMap();
    base.OnAppearing();
}

private async Task InitMap()
{
    _viewModel.Map.ItemsSource = _viewModel.Positions;
    _viewModel.Map.ItemTemplate = new DataTemplate(() =>
    {
        Pin pin = new Pin().Bind(Pin.LocationProperty, nameof(GritBin.LatLonLocation))
        .Bind(Pin.LabelProperty, nameof(GritBin.Location))
        .Bind(Pin.AddressProperty, nameof(GritBin.Street));
        return pin;
    });
    await _viewModel.GetGritBins();
    Content = _viewModel.Map;

}`
alexgavru commented 9 months ago

Same thing happening to me. I need to load the pins dynamically so I cannot add them before the map is created.

magtimmermans commented 8 months ago

I am not able to display more than 20 items on iOS. Is there a limit? Doesn't look like it but more than 20 will display nothing...

VToegel commented 7 months ago

I got the same problem: once I open the map page adding pins dynamically I get freezing throughout the app.

VToegel commented 7 months ago

I can confirm that the workaround by @Beryl0 solves the issue. I have the map in an absolute layout named MainLayout. I create a map and add the map in code in the OnNavigatedTo method. After adding the pins i add the map to the absolute layout. The map performance increased significantly, i have no more freezing. I solved the issue of dynamically adding the pins by triggering the InitMap() method with the collectionchanged event of the pin collection using a weak reference messenger.

private Map? map;
  MapSpan? currentMapSpan;
  private async Task InitMap()
  {
      try
      {
          if (MainLayout.Contains(map))
              MainLayout.Remove(map);

          map = new()
          {
              IsShowingUser = true,
              IsTrafficEnabled = true,
              ItemTemplate = new DataTemplate(() =>
              {
                  CustomPin pin = new()
                  {
                      ImageSource = "aerolocationpin.png"
                  };

                  pin.Bind(CustomPin.AddressProperty, getter: static (ObservableVenue venue) => venue.Address);
                  pin.Bind(CustomPin.LabelProperty, getter: static (ObservableVenue venue) => venue.Name);
                  pin.Bind(CustomPin.LocationProperty,
                      binding1: new Binding(nameof(ObservableVenue.Latitude)),
                      binding2: new Binding(nameof(ObservableVenue.Longitude)),
                      mode: BindingMode.OneWay,
                      convert: ((double Latitude, double Longitude) values) => new Location(values.Latitude, values.Longitude));

                  pin.InfoWindowClicked += Pin_InfoWindowClicked;
                  return pin;
              })
          };
          map.Bind(Map.ItemsSourceProperty, getter: static (MapViewViewModel vm) => vm.VenueDataService.Venues);

          AbsoluteLayout.SetLayoutBounds(map, new Rect(0, 0, 1, 1));
          AbsoluteLayout.SetLayoutFlags(map, AbsoluteLayoutFlags.All);
          MainLayout.Insert(0, map);

          if (MainLayout.Contains(activityIndicator)) MainLayout.Remove(activityIndicator);

          if (currentMapSpan is null) { MainThread.BeginInvokeOnMainThread(CenterMap); } else { MainThread.BeginInvokeOnMainThread(() => map.MoveToRegion(currentMapSpan)); }
      }
      catch (Exception)
      {
          // Unable to render map
          await _viewModel.DialogService.ShowAlertAsync("Warning", "The map could not be rendered. If this problem persists please write a report.", "OK");
      }
  }
LeoJHarris commented 1 month ago

First time loading map with 512 pins seems to be okay, albeit could be faster. I do something like this with a switch to remove Pins then re-add them again when switch toggled on, same 512 pins to be readded:

 [RelayCommand(CanExecute = nameof(CanToggleMapPinsVisibilityCommandExecute))]
    private void ToggleMapPinsVisibility(ToggledEventArgs? toggledEventArgs)
    {
        if (toggledEventArgs?.Value == true)
        {
            StoresMap.Pins.Clear();

            foreach (CustomStorePin customStorePin in StoreLocatorPageViewModel.StoreLocatorListTabViewModel?.AllStoreLocations?.Select(GenerateCustomStorePin) ?? [])
            {
                StoresMap.Pins.Add(customStorePin); <====== iterating here takes very long!
            }
        }
        else
        {
            CustomStorePins.Clear();

            if (StoreLocatorPageViewModel is not null)
            {
                StoreLocatorPageViewModel.SelectedCustomStorePin = null;
            }
        }
    }

Repopulating the map with the stopwatch running took 37 seconds.