1

I am creating a Xamarin.Forms.Maps.Polyline in runtime. How can I append positions dynamically, given that the Polyline.Geopath property is read-only?

Createing a polyline in runtime

Following the document to create a polyline : Xamarin.Forms Map Polygons and Polylines. this link is a tutorial with fixed position in the code. How to assign position in runtime dynamically.

using Xamarin.Forms.Maps;
// ...
Map map = new Map
{
// ...
};

Create a object to store routing data (data extracted from json)

public class MapRoute
{
    //[JsonPropertyName("timestamp")]
    public long timestamp { get; set; }
    //[JsonPropertyName("lat")]
    public double lat { get; set; }
    //[JsonPropertyName("lng")]
    public double lng { get; set; }

    public MapRoute(long v1, double v2, double v3)
    {
        timestamp = v1;
        lat = v2;
        lng = v3;
    }
}

Serialize routing object to JsonString.

public void RouteAppend(MapRoute route)
{
    JsonString.Append(JsonSerializer.Serialize(route));
    JsonString.Append(",");
}

In real life, there are more than 2 elements in jsonString (there are more than 1000 elements which stored in jsonString)

readonly string jsonString = " [ {\"timestamp\": 1514172600000, \"Lat\": 37.33417925, \"Lng\": -122.04153133}, " + "{\"timestamp\": 1514172610000, \"Lat\": 37.33419725, \"Lng\": -122.04151333} ]";

JsonDocument doc;
JsonElement root;

private IList<Position> pos;

doc = Parse(testString);
root = doc.RootElement;
     
var routes = root.EnumerateArray();
while (routes.MoveNext())
{
    var user = routes.Current;
  
    pos.Add(new Position(Convert.ToDouble(user.GetProperty("lat")), Convert.ToDouble(user.GetProperty("lng"))));
           
}

Finally, there is a list pos with a lot of Position, I would assign the pos to Geopath. Unfortunately, it isn't allowed. It is a readonly property:

// instantiate a polyline
Polyline polyline = new Polyline
{
    StrokeColor = Color.Blue,
    StrokeWidth = 12,
    Geopath = pos // It is a readonly property, cannot assign pos to Geopath
}

// add the polyline to the map's MapElements collection
map.MapElements.Add(polyline);

How can this problem be resolved?

dbc
  • 104,963
  • 20
  • 228
  • 340
madeinQuant
  • 1,721
  • 1
  • 18
  • 29

1 Answers1

1

While Polyline.Geopath is a get-only property, the returned IList<Position> is not a read-only collection, so you can add your Position objects to it after construction.

Thus you can create the following factory method:

public static class PolylineFactory
{
    const string latitudeJsonName = "Lat";
    const string longitudeJsonName = "Lng";
    
    public static Polyline FromLatLngJson(string jsonString, float? strokeWidth = default, Color strokeColor = default)
    {
        using var doc = JsonDocument.Parse(jsonString); 
        var query = doc.RootElement.EnumerateArray()
            // GetProperty performs property name matching as an ordinal, case-sensitive comparison.
            .Select(e => new Position(e.GetProperty(latitudeJsonName).GetDouble(), e.GetProperty(longitudeJsonName).GetDouble()));

        var polyline = new Polyline();
        if (strokeColor != default)
            polyline.StrokeColor = strokeColor;
        if (strokeWidth != default)
            polyline.StrokeWidth = strokeWidth.Value;
        foreach (var position in query)
            polyline.Geopath.Add(position);
        
        return polyline;
    }
}

Alternatively, in .NET 5 JsonSerializer supports deserializing to objects with a single, parameterized constructor, so if you modify your MapRoute class slightly as follows, you can deserialize your jsonString to a List<MapRoute> directly:

public class MapRoute
{
    public long timestamp { get; set; }
    public double lat { get; set; }
    public double lng { get; set; }

    public MapRoute(long timestamp, double lat, double lng)
    {
        // The constructor argument names and property names must match for JsonSerializer to bind to the constructor successfully
        this.timestamp = timestamp;
        this.lat = lat;
        this.lng = lng;
    }
}

public static class PolylineFactory
{
    public static Polyline FromLatLngJson(string jsonString, float? strokeWidth = default, Color strokeColor = default)
    {
        var routes = JsonSerializer.Deserialize<List<MapRoute>>(jsonString, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
        
        var polyline = new Polyline();
        if (strokeColor != default)
            polyline.StrokeColor = strokeColor;
        if (strokeWidth != default)
            polyline.StrokeWidth = strokeWidth.Value;
        foreach (var route in routes)
            polyline.Geopath.Add(new Position(route.lat, route.lng));
        
        return polyline;
    }
}

(In .NET Core 3.x you would need to add a parameterless constructor to MapRoute and ensure all properties are mutable to deserialize it successfully.)

Either way, you can call the factory method as follows:

var polyline = PolylineFactory.FromLatLngJson(jsonString, 12, Color.Blue);

Notes:

  • JsonElement.GetProperty() performs property name matching as an ordinal, case-sensitive comparison, so you need to pass "Lat" and "Lng" rather than "lat" and "lng", since your jsonString is pascal-cased not camel-cased.

    If this is a mistake in your question and your JSON string is really camel-cased, modify latitudeJsonName and longitudeJsonName appropriately.

    If you need to ignore case when fetching a property see Ignore case when using TryGetProperty.

  • Convert.ToDouble() interprets the incoming value by using the formatting conventions of the current thread culture. E.g. in many European locales a , comma is used as the decimal separator when formatting floating values. As this is inconsistent with the JSON standard it is better to use the built-in method JsonElement.GetDouble() which will always use the correct decimal separator.

  • JsonDocument is disposable:

    This class utilizes resources from pooled memory to minimize the impact of the garbage collector (GC) in high-usage scenarios. Failure to properly dispose this object will result in the memory not being returned to the pool, which will increase GC impact across various parts of the framework.

    In your code you do not dispose of your doc, but you should.

Demo fiddle #1 here using JsonDocument, and #2 here using MapRoute.

dbc
  • 104,963
  • 20
  • 228
  • 340