Symbolization of Elements in XamMap

[Infragistics] Mihail Mateev / Tuesday, August 31, 2010

Custom symbolization for the map elements in XamMap component is a common case .

The solution often is to use a Value Template for the specific MapLayer. 

By default position of the symbol is in the center of the bounding rectangle of the multi-polygons.
It is possible to have a symbol outside of the polylines, included in the multi-polygons, representing surface elements.

Sample demo application demonstrates how to change a position of the MapElement Symbol into the Centroid (x = Sum(point[i].X/n, y= Sum(y[i]/n) where n is the number of points for the polyline with a biggest area from all polylines in the map element).

Demo application:

Demo application will be created for Silverlight. It is possible to make a similar one for WPF.

Requirements:

To be possible to build demo applications you need to install:

SQL Server 2008 R2 Express or higher license.

http://www.microsoft.com/express/Database/InstallOptions.aspx

NetAdvantage for Silverlight Data Visualization 2010 vol.2

http://ko.infragistics.com/dotnet/netadvantage/silverlight/data-visualization.aspx#Downloads

 

Steps to create a demo application:

Steps to create a demo application:

  • Create a base Silverlight application, using XamMap and
    data from SQL Server 2008 Spatial. 
  •  Add a custom ValueTemplate for a MapLayer, named "world":
  •  Add an Event Handler for the MapLayer.Imported event and calculate in its implementation a new position for MapElement.SymbolOrigin property: 
  •  Change the size of the symbols, depending of the value:

 Implementation:

 

<ig:MapLayer

    Name="World" Brushes="#4C2C42C0 #4C218B93 #4CDC1C1C #4C3D7835 #4C701B51"

    FillMode="Choropleth" WorldRect="{Binding ElementName=MainWindow, Path=WorldRect}"

    DataMapping="Name=Name;Value=Value;Caption=Value" VisibleFromScale="0"

    >

<ig:MapLayer.Reader>

    <ig:SqlShapeReader DataMapping="Data=geom; Name=CNTRY_NAME; Value=POP_CNTRY; Caption=CNTRY_NAME; ToolTip=CNTRY_NAME">

    </ig:SqlShapeReader>

</ig:MapLayer.Reader>

<ig:MapLayer.ValueScale>

    <ig:LinearScale IsAutoRange="True"/>

</ig:MapLayer.ValueScale>

    <ig:MapLayer.ValueTemplate>

        <DataTemplate>

            <Grid Width="20" Height="20">

                <ToolTipService.ToolTip>

                    <Border CornerRadius="4" BorderBrush="Bisque" BorderThickness="3" Background="Beige" Padding="8">

                        <StackPanel>

                            <TextBlock FontSize="12" FontWeight="Bold" Text="{Binding Path=ToolTip, StringFormat='Location: {0}'}" />

                            <TextBlock FontSize="12" FontWeight="Bold" Text="{Binding Path=Value, StringFormat='Population: {0}'}" />

                        </StackPanel>

                    </Border>

                </ToolTipService.ToolTip>

                <Grid.ColumnDefinitions>

                    <ColumnDefinition Width="0.15*"/>

                    <ColumnDefinition Width="0.7*"/>

                    <ColumnDefinition Width="0.15*"/>

                </Grid.ColumnDefinitions>

                <Grid.RowDefinitions>

                    <RowDefinition Height="0.04*"/>

                    <RowDefinition Height="0.4*"/>

                    <RowDefinition Height="0.56*"/>

                </Grid.RowDefinitions>

                <Ellipse Grid.RowSpan="3" Grid.ColumnSpan="3" Fill="Aqua" Opacity="0.7"/>

                <Ellipse Grid.RowSpan="3" Grid.ColumnSpan="3" IsHitTestVisible="False" Opacity="0.7">

                    <Ellipse.Fill>

                        <LinearGradientBrush>

                            <GradientStop Color="Transparent" Offset="0"/>

                            <GradientStop Color="#30000000" Offset="1"/>

                        </LinearGradientBrush>

                    </Ellipse.Fill>

                </Ellipse>

                <Ellipse Grid.RowSpan="3" Grid.ColumnSpan="3" IsHitTestVisible="False" Opacity="0.7">

                    <Ellipse.Fill>

                        <RadialGradientBrush>

                            <GradientStop Color="Transparent" Offset="0"/>

                            <GradientStop Color="#50000000" Offset="1"/>

                        </RadialGradientBrush>

                    </Ellipse.Fill>

                </Ellipse>

                <Ellipse Grid.Row="1" Grid.Column="1" IsHitTestVisible="False" Opacity="0.7">

                    <Ellipse.Fill>

                        <LinearGradientBrush StartPoint="0, 0" EndPoint="0, 1">

                            <GradientStop Color="#a0ffffff" Offset="0.0"/>

                            <GradientStop Color="#00ffffff" Offset="1.0"/>

                        </LinearGradientBrush>

                    </Ellipse.Fill>

                </Ellipse>

            </Grid>

        </DataTemplate>

    </ig:MapLayer.ValueTemplate>

</ig:MapLayer>

Run the application:

Startup screen has symbols for each country. Some of them are outside of the parts of multi-polygons, representing countries.

 Silverlight application Initial screen.

 

  •  Add an Event Handler for the MapLayer.Imported event and calculate in its implementation a new position for MapElement.SymbolOrigin property:

<ig:MapLayer

    Name="World" Brushes="#4C2C42C0 #4C218B93 #4CDC1C1C #4C3D7835 #4C701B51"

    FillMode="Choropleth" WorldRect="{Binding ElementName=MainWindow, Path=WorldRect}"

    DataMapping="Name=Name;Value=Value;Caption=Value" VisibleFromScale="0"

    Imported="Layer_Imported">

..... 

private void Layer_Imported(

        object sender,

        MapLayerImportEventArgs e)

{

    MapLayer layer = sender as MapLayer;

    if (layer == null)

    {

        return;

    }

 

    List<MapElement> list =

        new List<MapElement>(layer.Elements);

     foreach (MapElement ele in list)

    {

        var el = ele as SurfaceElement;

        if (el == null)

        {

            continue;

        }

        MapPolylineCollection plines = el.Polylines;

        var ordered = plines.OrderByDescending(c => c.GetPolygonArea());

        MapPolyline poly = ordered.ElementAt(0);

        ele.SymbolOrigin = poly.GetCentroid();

    }

}

 

/// <summary>

/// Implements extension methods to calculate a geometry properties of a polyline

/// </summary>

public static class Util

{

    public static double GetPolygonSignedArea(this IList<Point> c)

    {

        int n = c.Count;

        double a = 0.0;

        if (n > 2)

        {

            for (int k = n - 2, j = n - 1, i = 0; i < n; k = j, j = i, ++i)

            {

                a += c[j].X * (c[i].Y - c[k].Y);

            }

        }

         return a;

    }

 

     public static double GetPolygonArea(this IList<Point> c)

    {

        return Math.Abs(0.5 * GetPolygonSignedArea(c));

    }

 

    public static Point GetCentroid(this IEnumerable<Point> points)

    {

        double x = 0.0;

        double y = 0.0;

        double c = 0.0;

        foreach (Point point in points)

        {

            x += point.X;

            y += point.Y;

 

            c += 1.0;

        }

        return new Point(x / c, y / c);

    }

}

 

Run the application:

Positions of the symbols now are on appropriate places

 

  • Change the size of the symbols, depending of the value:

<ig:MapLayer

    Name="World" Brushes="#4C2C42C0 #4C218B93 #4CDC1C1C #4C3D7835 #4C701B51"

    FillMode="Choropleth" WorldRect="{Binding ElementName=MainWindow, Path=WorldRect}"

    DataMapping="Name=Name;Value=Value;Caption=Value" VisibleFromScale="0"

    Imported="Layer_Imported">

<ig:MapLayer.Reader>

    <ig:SqlShapeReader DataMapping="Data=geom; Name=CNTRY_NAME; Value=POP_CNTRY; Caption=CNTRY_NAME; ToolTip=CNTRY_NAME">

    </ig:SqlShapeReader>

</ig:MapLayer.Reader>

<ig:MapLayer.ValueScale>

    <ig:LinearScale IsAutoRange="True"/>

</ig:MapLayer.ValueScale>

    <ig:MapLayer.ValueTemplate>

        <DataTemplate>

            <Grid Width="{Binding Path=Value}" Height="{Binding Path=Value}">

                <ToolTipService.ToolTip>

                    <Border CornerRadius="4" BorderBrush="Bisque" BorderThickness="3" Background="Beige" Padding="8">

                        <StackPanel>

                            <TextBlock FontSize="12" FontWeight="Bold" Text="{Binding Path=ToolTip, StringFormat='Location: {0}'}" />

                            <TextBlock FontSize="12" FontWeight="Bold" Text="{Binding Path=Value, StringFormat='Population: {0}'}" />

                        </StackPanel>

                    </Border>

                </ToolTipService.ToolTip>

                <Grid.ColumnDefinitions>

                    <ColumnDefinition Width="0.15*"/>

                    <ColumnDefinition Width="0.7*"/>

                    <ColumnDefinition Width="0.15*"/>

                </Grid.ColumnDefinitions>

                <Grid.RowDefinitions>

                    <RowDefinition Height="0.04*"/>

                    <RowDefinition Height="0.4*"/>

                    <RowDefinition Height="0.56*"/>

                </Grid.RowDefinitions>

                <Ellipse Grid.RowSpan="3" Grid.ColumnSpan="3" Fill="Aqua" Opacity="0.7"/>

                <Ellipse Grid.RowSpan="3" Grid.ColumnSpan="3" IsHitTestVisible="False" Opacity="0.7">

                    <Ellipse.Fill>

                        <LinearGradientBrush>

                            <GradientStop Color="Transparent" Offset="0"/>

                            <GradientStop Color="#30000000" Offset="1"/>

                        </LinearGradientBrush>

                    </Ellipse.Fill>

                </Ellipse>

                <Ellipse Grid.RowSpan="3" Grid.ColumnSpan="3" IsHitTestVisible="False" Opacity="0.7">

                    <Ellipse.Fill>

                        <RadialGradientBrush>

                            <GradientStop Color="Transparent" Offset="0"/>

                            <GradientStop Color="#50000000" Offset="1"/>

                        </RadialGradientBrush>

                    </Ellipse.Fill>

                </Ellipse>

                <Ellipse Grid.Row="1" Grid.Column="1" IsHitTestVisible="False" Opacity="0.7">

                    <Ellipse.Fill>

                        <LinearGradientBrush StartPoint="0, 0" EndPoint="0, 1">

                            <GradientStop Color="#a0ffffff" Offset="0.0"/>

                            <GradientStop Color="#00ffffff" Offset="1.0"/>

                        </LinearGradientBrush>

                    </Ellipse.Fill>

                </Ellipse>

            </Grid>

        </DataTemplate>

    </ig:MapLayer.ValueTemplate>

</ig:MapLayer>

 

const double Min = 5;

const double Max = 70;

....

private void Layer_Imported(

        object sender,

        MapLayerImportEventArgs e)

{

    MapLayer layer = sender as MapLayer;

    if (layer == null)

    {

        return;

    }

 

    List<MapElement> list =

        new List<MapElement>(layer.Elements);

    foreach (MapElement ele in list)

    {

        var el = ele as SurfaceElement;

        if (el == null)

        {

            continue;

        }

 

        MapPolylineCollection plines = el.Polylines;

        var ordered = plines.OrderByDescending(c => c.GetPolygonArea());

        MapPolyline poly = ordered.ElementAt(0);

        ele.SymbolOrigin = poly.GetCentroid();

 

        //Normalize a population value

        double value = ele.Value / 5000000.0;

         if (value < Min)

        {

            value = Min;

        }

        else if (value > Max)

        {

            value = Max;

        }

 

        ele.Value = value;

    }

 

}

Run the application:

Size of the symbols depends on country population, normalized in the event handler