Xamarin Forms Maps - how to refresh/update the map - CustomMap Renderer

Emixam23Emixam23 USMember ✭✭✭
edited April 2016 in Xamarin.Forms

I search a lot but I still have the same problem:

How can I update, refresh or reload the Xamarin.Forms.Maps?

In the class definition (class CustomMap : Map), there is no method to update the maps.. Maybe a MVVM logic can solves the problem, but I can't find it on the Web..

I followed this tutorial for the maps : Working with maps

To customise it, I followed this tutorial : Highlight a Route on a Map

So, after these tutorials (I made the same things, no changes), I tried with 2 RouteCoordinates which gave me a straight line... I then made an algorithm which works perfectly.

DirectionMap

    public class DirectionMap
    {
    public Distance distance { get; set; }
    public Duration duration { get; set; }
    public Address address_start { get; set; }
    public Address address_end { get; set; }
    public List<Step> steps { get; set; }

    public class Distance
    {
        public string text { get; set; }
        public int value { get; set; }
    }
    public class Duration
    {
        public string text { get; set; }
        public int value { get; set; }
    }
    public class Address
    {
        public string text { get; set; }
        public Position position { get; set; }
    }
    public class Step
    {
        public Position start { get; set; }
        public Position end { get; set; }
    }
}

ResponseHttpParser

public static void parseDirectionGoogleMapsResponse(HttpStatusCode httpStatusCode, JObject json, Action<DirectionMap, string> callback)
{
    switch (httpStatusCode)
    {
        case HttpStatusCode.OK:

            DirectionMap directionMap = null;
            string strException = null;

            try
            {
                directionMap = new DirectionMap()
                {
                    distance = new DirectionMap.Distance()
                    {
                        text = (json["routes"][0]["legs"][0]["distance"]["text"]).ToString(),
                        value = Int32.Parse((json["routes"][0]["legs"][0]["distance"]["value"]).ToString())
                    },
                    duration = new DirectionMap.Duration()
                    {
                        text = (json["routes"][0]["legs"][0]["duration"]["text"]).ToString(),
                        value = Int32.Parse((json["routes"][0]["legs"][0]["duration"]["value"]).ToString())
                    },
                    address_start = new DirectionMap.Address()
                    {
                        text = (json["routes"][0]["legs"][0]["start_address"]).ToString(),
                        position = new Position(Double.Parse((json["routes"][0]["legs"][0]["start_location"]["lat"]).ToString()), Double.Parse((json["routes"][0]["legs"][0]["start_location"]["lng"]).ToString()))
                    },
                    address_end = new DirectionMap.Address()
                    {
                        text = (json["routes"][0]["legs"][0]["end_address"]).ToString(),
                        position = new Position(Double.Parse((json["routes"][0]["legs"][0]["end_location"]["lat"]).ToString()), Double.Parse((json["routes"][0]["legs"][0]["end_location"]["lng"]).ToString()))
                    }
                };

                bool finished = false;
                directionMap.steps = new List<Step>();
                int index = 0;

                while (!finished)
                {
                    try
                    {
                        Step step = new Step()
                        {
                            start = new Position(Double.Parse((json["routes"][0]["legs"][0]["steps"][index]["start_location"]["lat"]).ToString()), Double.Parse((json["routes"][0]["legs"][0]["steps"][index]["start_location"]["lng"]).ToString())),
                            end = new Position(Double.Parse((json["routes"][0]["legs"][0]["steps"][index]["end_location"]["lat"]).ToString()), Double.Parse((json["routes"][0]["legs"][0]["steps"][index]["end_location"]["lng"]).ToString()))
                        };
                        directionMap.steps.Add(step);
                        index++;
                    }
                    catch (Exception e)
                    {
                        finished = true;
                    }
                }
            }
            catch (Exception e)
            {
                directionMap = null;
                strException = e.ToString();
            }
            finally
            {
                callback(directionMap, strException);
            }
            break;
        default:
            switch (httpStatusCode)
            {

            }
            callback(null, json.ToString());
            break;
    }
}

I just get the distance and duration for some private calculs and get each step that I put into a List<>;

When everything is finished, I use my callback which bring us back to the controller (MapPage.xaml.cs the XAML Form Page (Xamarin Portable))

Now, everything becomes weird.. It's like the map doesn't get that changes are made

public partial class MapPage : ContentPage
{
public MapPage()
   {
    InitializeComponent();
    setupMap();
    setupMapCustom();
}

public void setupMapCustom()
{
    customMap.RouteCoordinates.Add(new Position(37.785559, -122.396728));
    customMap.RouteCoordinates.Add(new Position(37.780624, -122.390541));
    customMap.RouteCoordinates.Add(new Position(37.777113, -122.394983));
    customMap.RouteCoordinates.Add(new Position(37.776831, -122.394627));

    customMap.MoveToRegion(MapSpan.FromCenterAndRadius(new Position(37.79752, -122.40183), Xamarin.Forms.Maps.Distance.FromMiles(1.0)));
}       

public async void setupMap()
{
    customMap.MapType = MapType.Satellite;

    string origin = "72100 Le Mans";
    string destination = "75000 Paris";

    HttpRequest.getDirections(origin, destination, callbackDirections);

    customMap.RouteCoordinates.Add(await MapUtilities.GetMapPointOfStreetAddress(origin));
    Position position = await MapUtilities.GetMapPointOfStreetAddress(destination);
    //customMap.RouteCoordinates.Add(position);

    var pin = new Pin
    {
        Type = PinType.Place,
        Position = position,
        Label = "Destination !!",
    };
    customMap.Pins.Add(pin);
}

private async void callbackDirections(Object obj, string str)
{
    if (obj != null)
    {
        DirectionMap directionMap = obj as DirectionMap;

        foreach (Step step in directionMap.steps)
        {
            customMap.RouteCoordinates.Add(step.start);
            System.Diagnostics.Debug.WriteLine("add step");
        }

        customMap.RouteCoordinates.Add(directionMap.address_end.position);
        System.Diagnostics.Debug.WriteLine("add last step");
    }
    else
    {
        System.Diagnostics.Debug.WriteLine(str);
    }
}

I run my app, everything works until it's something fast, because of the time spent by my algorithm etc, the callback is coming too late and then I need to refresh, reload or update my map... Anyway, I need to update my map in the future, so... If anyone can help, this one is welcome !

PS: Take a look at my post on Xamarin Forms Maps - how to refresh/update the map - CustomMap Renderer, maybe someone answered since I post this problem !

Answers

  • BananaTieBananaTie DKMember ✭✭

    Hi @Emixam23

    Did you ever find a solution to this?

    I just hit the exact same obstacle and seem to be unable to refresh the map with a new route.

    It displays fine, it the coordinates for the route line is present at screen creation time, but not if they are added at a later point, so a redraw of the entire map is needed (as a work-around) - but how to do that? Or alternatively, how to register the CoutesCoordinates has been updated to trigger the redraw?

    (Currently I am looking at the iOS version, but I am sure the Android version has similar issues)

  • Emixam23Emixam23 USMember ✭✭✭

    Hey,

    yeah I did find why, your coordinates has to be a BindableProperty so each time it gets updated, then from the renderer OnPropertyChanged() is called and you can do whatever you need.

  • BananaTieBananaTie DKMember ✭✭
    edited April 2018

    Thank you for your answer.

    I just figured it out:
    Added to the BindableProperty for RouteCoordinates, I need to update the Polyline for iOS in both OnElementPropertyChanged and OnElementChanged, which was not implemented in the original Xamarin example (Links in the original post by @Emixam23 )

    If anyone else get the problem, this is the iOS renderer I got to work:

    `

    ///


    /// Map route renderer for iOS.
    ///

    /// The map used in this implementation is called BindableMap, enherit from Xamarin.Forms.Maps
    /// and implements a List<Xamarin.Forms.Maps.Points> RouteCoordinates with a BindableProperty
    /// implementation for easy access. The name of the BindableProperty is RouteCoordinatesProperty.
    public class MapRouteRenderer : MapRenderer
    {
    ///
    /// The visual data for the map route on the map.
    /// </summary
    MKPolylineRenderer polylineRenderer;

    /// <summary>
    /// Empty constructor is redundant.... Initializes a new instance of the <see cref="T:MyMapApp.iOS.Renderer.Maps.MapRouteRenderer"/> class.
    /// </summary>
    public MapRouteRenderer()
    {
    }
    
    /// <summary>
    /// Called when any bindable property on the Map view changes. This is used to handle changes
    /// to the route line on the map.
    /// </summary>
    /// <param name="sender">Sender of the event; in this case the MapView of the type BindableMap</param>
    /// <param name="e">The property that had some change applied.</param>
    protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        base.OnElementPropertyChanged(sender, e);
    
        if (null == this.Element || null == this.Control)
            return;
    
        if (e.PropertyName == BindableMap.RouteCoordinatesProperty.PropertyName)
        {
            UpdatePolyLine((BindableMap)sender);
        }
    }
    
    /// <summary>
    /// Every time an element changes, this is fired.
    /// </summary>
    /// It is not enough to update the route line on the map - as this seems to fire
    /// only when the Xamarin map is created, not if the parameters are changing.
    /// <param name="e">E.</param>
    protected override void OnElementChanged(ElementChangedEventArgs<View> e)
    {
        base.OnElementChanged(e);
    
        if (e.OldElement != null)
        {
            var nativeMap = Control as MKMapView;
            if (nativeMap != null)
            {
                nativeMap.RemoveOverlays(nativeMap.Overlays);
                nativeMap.OverlayRenderer = null;
                polylineRenderer = null;
            }
        }
    
        if (e.NewElement != null)
        {
            var formsMap = (BindableMap)e.NewElement;
    
            UpdatePolyLine(formsMap);
        }
    }
    
    /// <summary>
    /// The common method between <seealso cref="OnElementChanged(ElementChangedEventArgs{View})"/> 
    /// and <seealso cref="OnElementPropertyChanged(object, PropertyChangedEventArgs)"/> to actually 
    /// change the polyline data for the renderer to use later.
    /// </summary>
    /// <param name="map">The map object containing the new map route line data.</param>
    private void UpdatePolyLine(BindableMap map)
    {
        var nativeMap = Control as MKMapView;
    
        nativeMap.OverlayRenderer = GetOverlayRenderer;
    
        CLLocationCoordinate2D[] coords = new CLLocationCoordinate2D[map.RouteCoordinates.Count];
    
        int index = 0;
        foreach (var position in map.RouteCoordinates)
        {
            coords[index++] = new CLLocationCoordinate2D(position.Latitude, position.Longitude);
        }
    
        var routeOverlay = MKPolyline.FromCoordinates(coords);
        nativeMap.AddOverlay(routeOverlay);
    }
    
    /// <summary>
    /// The overlay renderer for the native map, using previous stored polyline data.
    /// </summary>
    /// <returns>The overlay renderer.</returns>
    /// <param name="mapView">The native map view.</param>
    /// <param name="overlayWrapper">Overlay wrapper.</param>
    MKOverlayRenderer GetOverlayRenderer(MKMapView mapView, IMKOverlay overlayWrapper)
    {
        if (polylineRenderer == null && !Equals(overlayWrapper, null))
        {
            var overlay = ObjCRuntime.Runtime.GetNSObject(overlayWrapper.Handle) as IMKOverlay;
            polylineRenderer = new MKPolylineRenderer(overlay as MKPolyline)
            {
                FillColor = UIColor.Blue,
                StrokeColor = UIColor.Red,
                LineWidth = 3,
                Alpha = 0.4f
            };
        }
        return polylineRenderer;
    }
    

    }

    `

  • johnm0101johnm0101 Member ✭✭
    edited April 24

    I've been trying to implement this for a while now - I use prety much the same code as shown in previous post and which i derived from a 2016 post - the key being the bindable property. However....
    I have a list of tracks taken from a list of gpx files and turned into a List and passed to the customMap.Routecoordinates.

    the problem is when one selects a second track the previous is not cleared. The Routecoordinates list/collection changes as it should and an overlay with those Positions is created but the track on the map does not clear or change as it should.

    I have written code to clear the tracks by setting the customMap.Routecoordinates to an emptylist
    I have (in the ios customMap.renderer used nativeMap.RemoveOverlays(nativeMap.Overlays)
    but a portion of the previous track is always left

    Does anyone have any further suggestions for simply clearing an existing overlay?
    the method nativeMap.RemoveOverlays(nativeMap.Overlays) seems straightforward but how and where should it be called

    The android implementation works fine btw
    I just dont 'get' what is happening in the iOs custommap renderer

Sign In or Register to comment.