Friday, December 16, 2011

How to customize Esri’s Silverlight TimeSlider

image

The Esri ArcGIS SIlverlight Toolkit SDK is a companion of Esri’s ArcGIS API for Silverlight. The toolkit includes a number of useful controls including a magnifier, scale bar and an info window to display map popup windows. This post will demonstrate how to customize (or style) the toolkit’s TimeSlider control.

To view a live application click here.
To download the full source code click here.

By default, the toolkit’s TimeSlider looks like this:

image

With a style applied it is possible to transform the TimeSlider to look like this:

image

To achieve this appearance you but include the follow two styles as a resource. The first style is for the Thumb.

<Style x:Key="SleekThumb" TargetType="Thumb">
    <Setter Property="Foreground" Value="Black"/>
    <Setter Property="BorderBrush" Value="Transparent"/>
    <Setter Property="BorderThickness" Value="0"/>
    <Setter Property="IsTabStop" Value="False"/>
    <Setter Property="Height" Value="12"/>
    <Setter Property="Width" Value="12"/>
    <Setter Property="Cursor" Value="Hand"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Thumb">
                <Grid>
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="CommonStates">
                            <VisualState x:Name="Normal"/>
                            <VisualState x:Name="MouseOver"/>
                            <VisualState x:Name="Pressed" />
                            <VisualState x:Name="Disabled" />
                        </VisualStateGroup>
                        <VisualStateGroup x:Name="FocusStates">
                            <VisualState x:Name="Focused"/>
                            <VisualState x:Name="Unfocused"/>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    <Ellipse Fill="{TemplateBinding Background}"
Height="{TemplateBinding Height}"
Width="{TemplateBinding Width}"
Stroke="{TemplateBinding BorderBrush}"
StrokeThickness="{TemplateBinding BorderThickness}"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter
>
</
Style
>

And then the TimeSlider itself.

<Style x:Key="SleekTimeSlider" TargetType="esri:TimeSlider">
    <Setter Property="IsTabStop" Value="False" />
    <Setter Property="Foreground" Value="Black"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="esri:TimeSlider">
                <Grid>
                    <!--VisualState wiring - not used-->
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="CommonStates">
                            <VisualState x:Name="Normal"/>
                            <VisualState x:Name="MouseOver"/>
                            <VisualState x:Name="Disabled"/>
                        </VisualStateGroup>
                        <VisualStateGroup x:Name="FocusStates">
                            <VisualState x:Name="Focused" />
                            <VisualState x:Name="Unfocused"/>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>

                    <!-- Slider core -->
                    <Grid x:Name="HorizontalTrack">
                        <!--Hide tick marks (but still used for snapping)-->
                        <esriToolkitPrimitives:TickBar x:Name="TickMarks"
Margin="0,0,0,0"
IsHitTestVisible="False">
                            <esriToolkitPrimitives:TickBar.TickMarkTemplate>
                                <DataTemplate>
                                    <Grid />
                                </DataTemplate>
                            </esriToolkitPrimitives:TickBar.TickMarkTemplate>
                        </esriToolkitPrimitives:TickBar>

                        <!--Background Track-->
                        <Rectangle Height="3" Margin="5,0"
StrokeThickness="{TemplateBinding BorderThickness}"
Fill="LightGray"/>

                        <!--Left repeater button-->
                        <RepeatButton
                           x:Name="HorizontalTrackLargeChangeDecreaseRepeatButton"
                                                           
                          
IsTabStop="False"
                                 
                          
HorizontalAlignment="Stretch"
                           Opacity="0" />

                        <!--Minimum thumb-->
                        <Thumb
                           x:Name="MinimumThumb"
                          
                          
DataContext="{TemplateBinding Value}"
                           ToolTipService.ToolTip="{Binding Start}"
 
                          
ToolTipService.Placement="Top"
                           HorizontalAlignment="Left"
                           Foreground="{TemplateBinding Foreground}"
                           Style="{StaticResource SleekThumb}" />

                        <!--Middle thumb-->
                        <Thumb
                           x:Name="HorizontalTrackThumb"
                                                                                                                         
                          
IsTabStop="False"
                           Cursor="Hand"
                           HorizontalAlignment="Left"
              
                          
Foreground="{TemplateBinding Foreground}">
                            <Thumb.Template>
                                <ControlTemplate>
                                    <Rectangle Height="3"
Fill="{TemplateBinding Foreground}" />
                                </ControlTemplate>
                            </Thumb.Template>
                        </Thumb>

                        <!--Maximum thumb-->
                        <Thumb
                           x:Name="MaximumThumb"
                                   
                          
DataContext="{TemplateBinding Value}"
ToolTipService.ToolTip="{Binding End}"
 
                          
ToolTipService.Placement="Top"
                           HorizontalAlignment="Left"
                           Foreground="{TemplateBinding Foreground}"
                           Style="{StaticResource SleekThumb}" />

                        <!--Right repeater button-->
                        <RepeatButton
                           x:Name="HorizontalTrackLargeChangeIncreaseRepeatButton"
   
                          
IsTabStop="False"
                                   
                          
HorizontalAlignment="Stretch"
                           Opacity="0" />
                    </Grid>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter
>
</
Style
>

The XAML page containing two styles above must have the following namespaces defined.

xmlns:esri="http://schemas.esri.com/arcgis/client/2009"
xmlns:esriToolkitPrimitives=
"clr-namespace:ESRI.ArcGIS.Client.Toolkit.Primitives;
assembly=ESRI.ArcGIS.Client.Toolkit"

Finally, apply the style to the TimeSlider as shown below.

<esri:TimeSlider
   Grid.Row="1"
   x:Name="TimeSlider"
   TimeMode="TimeExtent"
   Foreground="Black"

    Style="{StaticResource SleekTimeSlider}"
/>

In closing, the controls included in the ArcGIS Silverlight Toolkit are no different from other controls with respect to styling and templating. To assist with styling other toolkit controls I would recommend browse the toolkit’s source code on codeplex. Alternatively you can just tweak the XAML in the post to fit your needs.

Thursday, December 15, 2011

How to use WPF in a web service?


Using WPF in a web service might seem bizarre, but consider the following scenarios:
  1. You need to burn in a watermark into images requested from web application,
  2. You want to apply a red-eye reduction process to photographs takes from a mobile application, or
  3. You want to apply a Jason Bourne night effect to images in a web mapping application.
We recently implemented something similar to the third scenario (see blog, source, live app). A client web application written in JavaScript, Flex or Silverlight could apply an image rendering effects by parsing an image’s url through a proxy service. The proxy service, using WPF, would apply a bitmap effect and return a modified image. This technique adds the power of WPF to thin client at the cost of minimal network latency.
Below is a detailed description of this proxy web service.
The following class is an ASP.NET generic web handler called “invert”. When published it will be accessible as ../virtualdirectory/invert.ashx. and will perform a color negative effect on parsed images. Because our server is accommodating multiple effects, one effect per handler, the web context is handed to static method to reduce the amount of redundant code.
public class Invert : IHttpHandler {
    public void ProcessRequest(HttpContext context) {
        Shader.Process(context, ShaderType.Invert);
    }
    public bool IsReusable {
        get { return false; }
    }
}
The code below is performing a number of important tasks. Firstly, because this proxy is published on the internet we need to add some restrictions so that it is only used as intended. The proxy will only entertain requests from web applications published on the maps.esri.com domain and it will only process images hosted on the services.arcgisonline.com domain. If the proxy request is legitimate, the remote image is downloaded and passed to a new background thread. The key is the creation of a background thread with a single-threaded apartment which is required to use COM and hence WPF.  The proxy (or IIS) process is put only hold as the WPF operation is underway.  The background thread returns a new image as an array of bytes which are then passed back to the client.
public class Shader {
    public static void Process(HttpContext context, ShaderType effect) {
        // Only accept request from apps from this domain!
        if (!context.Request.UrlReferrer.ToString().ToLowerInvariant().
                Contains("maps.esri.com")) {
            context.Response.StatusCode = 403;
            context.Response.End();
            return;
        }
            
        // Check for query string
        string query = Uri.UnescapeDataString(context.Request.QueryString.ToString());
        if (string.IsNullOrEmpty(query)) {
            context.Response.StatusCode = 403;
            context.Response.End();
            return;
        }
 
        // Filter requests
        if (!query.ToLowerInvariant().Contains("services.arcgisonline.com")) {
            context.Response.StatusCode = 403;
            context.Response.End();
            return;
        }
 
        // Create web request
        WebRequest webRequest = WebRequest.Create(new Uri(query));
        webRequest.Method = context.Request.HttpMethod;
 
        // Send the request to the server
        WebResponse serverResponse = null;
        try {
            serverResponse = webRequest.GetResponse();
        }
        catch (WebException webException) {
            context.Response.StatusCode = 500;
            context.Response.StatusDescription = webException.Status.ToString();
            context.Response.Write(webException.Response);
            context.Response.End();
            return;
        }
 
        // Exit if invalid response
        if (serverResponse == null) {
            context.Response.End();
            return;
        }
 
        // Configure reponse        context.Response.ContentType = serverResponse.ContentType;
        Stream stream = serverResponse.GetResponseStream();
 
        // Read response
        byte[] buffer = new byte[32768];
        int size = 0;
        int chunk;
        while ((chunk = stream.Read(buffer, size, buffer.Length - size)) > 0) {
            size += chunk;
            if (size != buffer.Length) { continue; }
            int nextByte = stream.ReadByte();
            if (nextByte == -1) { break; }
 
            // Resize the buffer
            byte[] newBuffer = new byte[buffer.Length * 2];
            Array.Copy(buffer, newBuffer, buffer.Length);
            newBuffer[size] = (byte)nextByte;
            buffer = newBuffer;
            size++;
        }
        serverResponse.Close();
        stream.Close();
 
        // Shrink buffer
        byte[] bytes = new byte[size];
        Array.Copy(buffer, bytes, size);
 
        // If the request is not a JPEG image then return data
        if (!serverResponse.ContentType.ToLowerInvariant().Contains("image/jpg")) {
            context.Response.OutputStream.Write(bytes, 0, bytes.Length);
            context.Response.End();
            return;
        }
 
        // Create class to send to STA backgroung thread
        ShaderWorker work = new ShaderWorker() {
            Source = bytes,
            ShaderEffect = effect
        };
 
        // Execute background thread
        Thread worker = new Thread(new ThreadStart(work.Execute));
        worker.SetApartmentState(ApartmentState.STA);
        worker.Name = "CreateImageWorker";
        worker.Start();
        worker.Join();
 
        // Return processed stream
        context.Response.OutputStream.Write(work.Target, 0, work.Target.Length);
        context.Response.End();
    }
}
This enumerator lists available pixel shaders. These shaders (or “effects”) were downloaded from the Windows Presentation Foundation Pixel Shader Effects Library.
public enum ShaderType {
    Emboss,
    Invert,
    Monochrome,
    Mosaic,
    Pixelate,
    Tint
}
And lastly, this is were the magic happens. This class is executed from the main IIS thread as a single-apartment thread as explained above. The source image is loaded as a generic BitmapImage in an Image, rendered with an effect and then exported to a new byte array.
public class ShaderWorker {
    public ShaderType ShaderEffect { private get; set; }
    public byte[] Source { private get; set; }
    public byte[] Target { get; private set; }
    public void Execute() {
        // Create bitmapimage
        MemoryStream m = new MemoryStream(this.Source);
        BitmapImage b = new BitmapImage();
        b.BeginInit();
        b.DecodePixelWidth = 256;
        b.DecodePixelHeight = 256;
        b.StreamSource = m;
        b.EndInit();
        b.Freeze();
 
        // Create image element
        Image i = new Image() {
            Source = b
        };
 
        // Assign shade effect
        switch (this.ShaderEffect) {
            case ShaderType.Emboss:
                i.Effect = new EmbossedEffect();
                break;
            case ShaderType.Invert:
                i.Effect = new InvertEffect();
                break;
            case ShaderType.Monochrome:
                i.Effect = new MonochromeEffect();
                break;
            case ShaderType.Mosaic:
                i.Effect = new MosaicEffect();
                break;
            case ShaderType.Pixelate:
                i.Effect = new PixelateEffect();
                break;
            case ShaderType.Tint:
                i.Effect = new TintEffect();
                break;
        }
 
        // Assign element size        i.Arrange(
            new System.Windows.Rect(
                new System.Windows.Size(256, 256)
            )
        );
        i.UpdateLayout();
 
        // Render image element to a new bitmap
        RenderTargetBitmap r = new RenderTargetBitmap(256, 256, 96, 96,
                                                      PixelFormats.Default);
        r.Render(i);
 
        // Create a new memory stream
        MemoryStream memoryStream = new MemoryStream();
 
        // Export bitmap to the stream
        JpegBitmapEncoder encoder = new JpegBitmapEncoder();
        encoder.Frames.Add(BitmapFrame.Create(r));
        encoder.Save(memoryStream);
 
        // Export the stream to a byte array
        byte[] bytes = memoryStream.ToArray();
        memoryStream.Close();
 
        // Shutdown rendering thread (if alive)
        if (r.Dispatcher.Thread.IsAlive) {
            r.Dispatcher.InvokeShutdown();
        }
 
        // Store processed image (as a byte array)
        this.Target = bytes;
    }
}
In summary, this post describes the technique of using WPF in a web service to perform advanced rendering. In theory, this technique is not restricted to image processing but could also be used to generate graphics of charts, diagrams and even three dimensional scenes. During limited testing it is obvious that this technique introduces network latency, particularly when the service has been dormant. And what is still unknown is whether the rendering is performed in hardware (i.e. GPU) or software (i.e. CPU). Please comment below if you know the answer to this or know of a definitive test for GPU vs. CPU.