neděle 26. června 2011

Silverlight Event Calendar

For one of my latest project I needed a quite simple Event Calendar component for Silverlight. I did not want to use any third party libraries and I wanted this component to stay simple.

I had following contraints on the component:
  • It has to be bindable
  • It should accept any IEnumerable collection
  • I should be able just specify which property of objects in the collection holds the DateTime value, which will be used to place the objects in the calendar
  • It should expose a template to be able to change the view of the event
  • It should expose events such as "Calendar Event Clicked"
  • It should expose a SelectedItem property

Here is the resulting component - it does not look great, but you can easily style it as you want.

You can get the code here from GitHub


The component is based on Calendar component. Calendar is not really flexible component but there are some workarounds to make it work the way, that you like. First the calendar is placed inside a UserControl.

<usercontrol x:class="EventCalendarLibrary.EventCalendar">
    <grid background="White" x:name="LayoutRoot">
        <controls:calendar x:name="InnerCalendar">
    </controls:calendar></grid>
</usercontrol>


Calendar component is composed of CalendarDayButtons. CalendarDayButton resides in the System.Windows.Controls.Primitives
namespace.

The problem is that the Calendar does not hold a collection of these buttons so we are not able to dynamically add components to these Buttons.

However the style of the each button in the calendar can be set by setting the CalendarDayButtonStyle property.

We can use this style to override the control template and this way set our proper handler for Loaded and Click events. The handler for Loaded event will simply allow us to add the loaded Button to a collection which we will maintain inside our components and which later allows us to add the "events" to the calendar.

<grid.resources>
<style targettype="controlsPrimitives:CalendarDayButton" x:key="CalendarDayButtonStyle">
            <setter Property="Template">
                <Setter.Value>
                    <controltemplate TargetType="controlsPrimitives:CalendarDayButton">
                        <border BorderBrush="#FF598788" BorderThickness="1,1,1,1" CornerRadius="2,2,2,2">
                            <stackpanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch" MinHeight="30" MinWidth="10">
                                <controlsPrimitives:CalendarDayButton 
                                    Loaded="CalendarDayButton_Loaded" 
                                    Background="{TemplateBinding Background}" 
                                    BorderBrush="{TemplateBinding BorderBrush}"
                                        
                                    Content="{TemplateBinding Content}"
                                    BorderThickness="{TemplateBinding BorderThickness}" 
                                        
                                    x:Name="CalendarDayButton" Click="CalendarDayButton_Click"/>
                            </StackPanel>        
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        
</style>
</grid.resources>
<controls:calendar background="White" calendardaybuttonstyle="{StaticResource CalendarDayButtonStyle}" x:name="InnerCalendar">
</controls:calendar>

So what is going on here is:
We are changing the ControlTemplate of CalendayDayButton for a new one which consits of a Border and a StackPanel containing a new CalendarDayButton. This is important, because now we now that each "day" in the Calendar will be represented by this StackPanel to which we can add additional components.
As promised we override the Loaded event. Let's see the code-behind:

private void CalendarDayButton_Loaded(object sender, RoutedEventArgs e)
{
 var button = sender as CalendarDayButton;
 calendarButtons.Add(button);

 //Resizing the buttons is the only way to change the dimensions of the calendar
 button.Width = this.ActualWidth / 9;
 button.Height = this.ActualHeight / 8;

 if (calendarButtons.Count == 42)
 {
  FillCalendar();
 }
}

We are simple take the button, store it in our inner collection (called calendarButtons) for further manipulations and then we perform some Resizing. The only way to force Calendar to Resize itself to the values which you specify in "Width" and "Height" properties is actually to change the dimensions of the inner buttons.

And last we check if all button had been loaded and if yes then we call "FillCallendar" method - yes this will be the method which will fill in the events to the calendar.

Before we go there we need to define the Dependency Properties which will allow us to bind the desired values (collection of items, DateTime property name event style and SelectedEvent property).


public static readonly DependencyProperty SelectedEventProperty = DependencyProperty.Register("SelectedEvent", typeof(Object), typeof(EventCalendar), null);
public Object SelectedEvent
{
 get { return (Object)GetValue(SelectedEventProperty); }
 set { SetValue(SelectedEventProperty, value); }
}

public static readonly DependencyProperty CalendarEventButtonStyleProperty = DependencyProperty.Register("CalendarEventButtonStyle", typeof(Style), typeof(EventCalendar), null);
public Style CalendarEventButtonStyle
{
 get { return (Style)GetValue(CalendarEventButtonStyleProperty); }
 set { SetValue(CalendarEventButtonStyleProperty, value); }
}

public static readonly DependencyProperty DatePropertyNameProperty = DependencyProperty.Register("DatePropertyName", typeof(String), typeof(EventCalendar), null);
public String DatePropertyName
{
 get { return (String)GetValue(DatePropertyNameProperty); }
 set { SetValue(DatePropertyNameProperty, value); }
}

public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(IEnumerable), typeof(EventCalendar),
 new PropertyMetadata(ItemsSourcePropertyChanged));

public IEnumerable ItemsSource
{
 get { return (IEnumerable)GetValue(ItemsSourceProperty); }
 set { SetValue(ItemsSourceProperty, value); }
}

You can see that there is a handler attached to the change of ItemsSourceProperty. This handler is called whenever this property changes. This is a important part, we take the Items, determine which property contains the DateTime value and we will group these Items by this property and store it in internal dictionary of type Dictionary<DateTime, <List<Object>>.
public static void ItemsSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var owner = d as EventCalendar;
    //if the property was set to null we have to clear all the events from calendar
    if (e.NewValue == null)
    {
        owner.ClearCallendar();
        return;
    }
    
    IEnumerable rawItems = (IEnumerable)e.NewValue;
    PropertyInfo property = null;

    //to determine the if the Count>0
    var enumerator = rawItems.GetEnumerator();
    if(!enumerator.MoveNext()){
        owner.ClearCallendar();
        return;
    }

    Object o = enumerator.Current;
    Type type = o.GetType();

    //get the type of the properties inside of the IEnumerable
    property = type.GetProperty(owner.DatePropertyName);
    
    if (property != null)
    {
        IEnumerable<Object> items = Enumerable.Cast<Object>((IEnumerable)e.NewValue);
        //group the items and store in a dictionary
        if (items != null)
        {
            var parDate = items
                        .GroupBy(x => GetDateValue(x, property))
                        .ToDictionary(x => x.Key, x => x.ToList());
            owner.ItemsSourceDictionary = parDate;
            owner.FillCalendar();
        }
    }
}

//Returns the DateTime value of a property specified by its information
public static DateTime GetDateValue (Object x, PropertyInfo property)
{
    return ((DateTime)property.GetValue(x,null)).Date;
}

It is a bit complicated - and that comes probably from my poor knowledge and experience of working with raw IEnumerable. Basically I need to get the type of the items inside of the IEnumerable and then using this Type I can obtain the value of the DateTime property and group the values and store in an inner dictionary.


You can see that there is a simple helper functions which just takes PropertyInfo and Object and returns the Date value of that property. I prefer to get when using "Data" property I am sure that I will have exact "day" without hours and minutes and than I can group this data by this "day".

Now that we have the grouped events, we have to place them in the calendar. To create this function I have used to example shown on this blog.

private void FillCalendar(DateTime firstDate)
{
    if (ItemsSourceDictionary!=null && ItemsSourceDictionary.Count >0)
    {                
        DateTime currentDay;

        int weekDay = (int)firstDate.DayOfWeek;
        if (weekDay == 0) weekDay = 7;
        if (weekDay == 1) weekDay = 8;

        for (int counter = 0; counter < calendarButtons.Count;counter++)
        {
            var button = calendarButtons[counter];
            var panel = button.Parent as StackPanel;


            int nbControls = panel.Children.Count;
            for (int i = nbControls - 1; i > 0; i--)
            {
                panel.Children.RemoveAt(i);
            }

            currentDay = firstDate.AddDays(counter).AddDays(-weekDay);

            if (ItemsSourceDictionary.ContainsKey(currentDay))
            {
                var events = ItemsSourceDictionary[currentDay];
                foreach (Object calendarEvent in events)
                {
                    Button btn = new Button();
                    btn.DataContext = calendarEvent;
                    btn.Style = CalendarEventButtonStyle;
                    panel.Children.Add(btn);
                    btn.Click += new RoutedEventHandler(EventButton_Click);
                }
            }
        }
    }
}

This function accepts a DateTime parameter which is the first date of the month which is being shown in the Calendar. When the first of the month is Monday, than it will be shown as a first in the second row. When it is Tuesday, it will be shown as the second in the second row. For other cases, it will be shown in the first row.
Thus we can easily subtract the integer values specifying which the day in the week (eg. 3 for Thursday) and we will obtain the date which is shown in the first cell.

The day which is being shown in the calendar is exposed by Calendar.DisplayDate Property and we can easily access that to obtain the month which is being shown (and thus the first day of the month).

So we just iterate over all the buttons, determine the date for each and knowing that the buttons are wrapped by a StackPanel we can add to this panel the events.
Each event is represented by a Button and the style which is exposed as DependencyProperty is applied.

Exposed events

This component exposes two events, one for the moment when the user clicks on existing "Event" in the calendar and second one for the click on the button of the day.
public event EventHandler<calendareventargs> EventClick;
public event EventHandler DayClick;
When the user clicks on existing event in the calendar, we pass the clicked "Event" wrapped by "CalendarEventArgs" class.
void EventButton_Click(object sender, RoutedEventArgs e)
{
    object eventClicked = (sender as Button).DataContext as object;
    
    //set the selected event
    SelectedEvent = eventClicked;

    //just pass the click event to the hosting envirenment of the component
    if (EventClick != null)
    {
        EventClick(sender, new CalendarEventArgs(eventClicked));
    }
}
When the user clicks on the button of the day, we passed the Date of this day wrapped up by CalendarEventArgs.
private void CalendarDayButton_Click(object sender, RoutedEventArgs e)
{
    CalendarDayButton button = sender as CalendarDayButton;
    DateTime date = GetDate(GetFirstCalendarDate(),button);

    if(date!=DateTime.MinValue && DayClick!=null)
    {
        DayClick(sender,new CalendarEventArgs(date));
    }
}
We can obtain the Date for the button by method similar to the one described above.

Summary

There is no more to that, as I said the component stays super simple, just one class, you can style the Events which are placed to the Calendar and you have to handle the other actions (like eg. adding an "Event" when clicking on the "day" button) by yourself.

Download the source from GitHub

úterý 7. června 2011

Map Creator - convert raster images to maps data (in Silverlight)

This posts talks about a tool which can help you convert lines in raster maps to a set of locations which later can be visualized using Silverlight Bing Maps (or some other mapping framework).

I call the application Silverlight Map Creator :) and it is disponible at:
http://mapcreator.codeplex.com/.


This application can help you a bit when you have an existing map in a bitmap picture format (png, jpeg) and you would like to use the route lines from this map in your application.

Then you have two options:

  • Use MapCruncher. MapCruncher is tool from MS which allows you to create your own Tiles source from existing map. OK, if you never heard of Tiles:

    When using a map component (Google Maps, Bing Maps or any other) the entire map is composed of several tiles, each tile, when you zoom in is again composed of "smaller tiles" (they are of the same size, but have higher precision).
    So MapCruncher lets you create your own map, composed of you own tiles based on the raster image. Later you can use this new map and "put it over" the standard Bing/Google/Open map - thus showing the additional information.

    This however has one disadvantage - the map is quite static - it is just a bunch of pixel on top of your the classic map - you are for example not able to get the total length of the route on the map.
  • You need to obtain the geo-data which specify the route lines - in other words coordinations of the route points. To obtain this data from predefined image you would need to first set the correspondence between the image and the map and than analyze the image to get all the points of your map.
    I decided to create a tool which would help me with this task - this post is a brief description of the tool.

Here is a screen shot of the Map Creator tool which will help you accomplish it:



If you are wondering in the screenshot I am converting a map of a ski-race (www.jiz50.cz) to a set of points. In the left part you can see the map (jpg image) and in the right part the resulting route.

Converting raster image to map

The task of converting raster image to map data is composed of the following parts:
  • Load the image (by clicking the browse button...)
  • Set correspondence points between the image and the map
  • Pick up the color which defines the route or path in the raster image
  • Set some parameters for the analysis of the map
  • Press Start and hope to get some results
  • Perform some changes to the route
  • -> change positions of the points -> remove points from route
  • Add the route which you have obtained to the "result" set
    -> result set defines the data which is used to generate the XML.
    -> also this is the data which is saved any time, that you press save
  • Generate XML data for your maps
To accomplish all of that the application has a simple menu:



Here are some details to the parts which are not straightforward:

Setting correspondences


Technical background
Generally to set correspondences between two coordination systems you need to determine if there is a transformation which could transform the coordinates of one point from the resource to the resulting coordination systems.

Map Creator is not really sophisticated tool so it supports only the case when there is a Affine Transformation between the two coordination systems.

Affine transformation preserves colinearity, that means that points which lie on a line in one coordination system will also lie on the map in the second one. Basically it means that Affine transformation can be composed of any linear transformation (scaling, rotation) and translation, for example skewing is not allowed.

The relation between the two coordination systems can be specified using the following equation:

sx = c00*rx + c01*ry + c02
sy = c10*rx + c11*ry + c12

(rx, ry) - coordinates in resource coordination system (so lets say pixels in the image)
(sx,sy) - coordinates in the resulting system (so lets say longitude and latitude)

So we need 6 parameters. For each point we have 2 equations, so we need 3 points to have 6 equations for 6 parameters. In matrix notation we can write it like this.

[c00 c01 c02] [x1 x2 x3] [u1 u2 u3]
[c10 c11 c12] [y1 y2 y3] = [v1 v2 v3]
[ 1 1 1]

In Map Creator
Just select the "Correspondences" radio button. Then every time you click "Right" on the image, a new point is added to a list, when you select the point and click right on the map a correspondence will be set.

Select colors of the route

Just select the "Color selection" radio button. Than when you click right button the mouse in the picture the color will be selected (and added to the list).

Set the parameters

There are 4 parameters which somehow change the why the resulting route is going to look like:
1)Search Range - basically it says what is the minimal distance in pixels of points of the same route. Setting this parameter to bigger value will cause connections between routes which are normally not connected. To low parameter will increase the density of point in the route (which is not desirable either).
2) Color toleration - determines the color of the pixel which will still be marked as in the route. Each color is composed of 3 parts (RGB) with values in range 0-255. This parameter sets the tolerance for each part (RGB).
Min Points Per Route - Each bike route is composed of several routes (or lets say lines). This is caused by side routes, which have to be represented separately. This parameter sets the minimal points for each route. If there is a route with less points that this parameter sets, it will not be added to the result.
Distance to connectAfter the analysis, some routes which should be connected are will not be. Typical example are the side routes. There will always be a little space between the main route and the side routes. This parameter sets what is the maximal distance between to routes which should be connected.

Performing changes to the resulting route

There are two possible changes that you can do:
1) Remove the point by clicking the right button
2) Change the position of the point by dragging it

Adding the route the the results

Generally a map is composed of several routes. The basic idea behind this tool is that once you have finished working on a route, you can add it to the result (pressing the button on the list of colors). When the route is in the results it will not be affected by running the analysis again.

Saving your work

By pressing "Save" button you can save your work. Saved will be the list of correspondences and the routes in the "result" set. This why the next time you can continue working on existing map.

Generating XML

The main idea is to use the data which you have generated in your application. The "Generate XML" button simply serializes the "result" set to XML.
As said before: Map is a collection of routes. Route is a collection of lines. Line is a collection of locations.
OK, in C# or Java or whichever language it is something like this:

List<List<List<Location>>> result

When you serialize this object to XML (here just using the standard C# serializer you will obtain XML with following structure:
<?xml version="1.0" encoding="utf-16"?>
<arrayofarrayofarrayoflocation xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <arrayofarrayoflocation>
    <arrayoflocation>
      <location>
        <latitude>50.834165728659457</Latitude>
        <longitude>15.292032040283674</Longitude>
        <altitude>0</Altitude>
        <altitudereference>Ground</AltitudeReference>
      </Location>
      <location>
        <latitude>50.83278001263735</Latitude>
        <longitude>15.293082116316537</Longitude>
        <altitude>0</Altitude>
        <altitudereference>Ground</AltitudeReference>
      </Location>
</ArrayOfLocation>
</ArrayOfArrayOfLocation>
</ArrayOfArrayOfArrayOfLocation>

OK, I agree - it is too verbose and not optimized and for most ugly, but I did not have time to implement my own format.

Future

So that's it. I am not sure that this tool will be useful to anyone, if you think that you might use it, if you have a suggestion or a bug, just leave me a note here...(ok not for the bugs, there is too many of them anyway).