Loading...
[-]

Java Pan / Zoom listener for Android

I’ve been doing some Android programming recently, and I wrote a pan / zoom listener that can be used on a VideoView, or an ImageView. This is an extension on some code I found here http://stackoverflow.com/questions/3813119/scale-limits-on-pinch-zoom-of-android which it turns out was from a touch / pinch example in a book called Hello Android http://pragprog.com/book/eband3/hello-android

The advantage my code has over other implementations is that the image will zoom in on the exact spot between your fingers, and it is limited to stop the image from going offscreen.

Here’s some example code of using it with an ImageView:

FrameLayout view = new FrameLayout(context);

FrameLayout.LayoutParams fp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT, Gravity.TOP | Gravity.LEFT);

ImageView imageView = new ImageView(this);
//Use line below for large images if you have hardware rendering turned on
//imageView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
// Line below is optional, depending on what scaling method you want
imageView.setScaleType(ImageView.ScaleType.MATRIX);
view.addView(imageView, fp);
view.setOnTouchListener(new PanAndZoomListener(view, imageView, Anchor.TOPLEFT));

Here’s some example code of using it with a VideoView:

FrameLayout view = new FrameLayout(context);
FrameLayout.LayoutParams fp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT, Gravity.CENTER);
VideoView videoView = new VideoView(this);
view.addView(videoView, fp);
view.setOnTouchListener(new PanAndZoomListener(view, videoView, Anchor.CENTER));

Here’s the source

import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.FloatMath;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
import android.widget.FrameLayout;
import android.widget.ImageView;

public class PanAndZoomListener implements OnTouchListener {

  public static class Anchor {

    public static final int CENTER = 0;
    public static final int TOPLEFT = 1;
  }
  private static final String TAG = "PanAndZoomListener";
  // We can be in one of these 3 states
  static final int NONE = 0;
  static final int DRAG = 1;
  static final int ZOOM = 2;
  int mode = NONE;
  // Remember some things for zooming
  PointF start = new PointF();
  PointF mid = new PointF();
  float oldDist = 1f;
  PanZoomCalculator panZoomCalculator;

  public PanAndZoomListener(FrameLayout containter, View view, int anchor) {
    panZoomCalculator = new PanZoomCalculator(containter, view, anchor);
  }

  public boolean onTouch(View view, MotionEvent event) {

    // Handle touch events here...
    switch (event.getAction() & MotionEvent.ACTION_MASK) {
      case MotionEvent.ACTION_DOWN:
        start.set(event.getX(), event.getY());
        Log.d(TAG, "mode=DRAG");
        mode = DRAG;
        break;
      case MotionEvent.ACTION_POINTER_DOWN:
        oldDist = spacing(event);
        Log.d(TAG, "oldDist=" + oldDist);
        if (oldDist > 10f) {
          midPoint(mid, event);
          mode = ZOOM;
          Log.d(TAG, "mode=ZOOM");
        }
        break;
      case MotionEvent.ACTION_UP:
      case MotionEvent.ACTION_POINTER_UP:
        mode = NONE;
        Log.d(TAG, "mode=NONE");
        break;
      case MotionEvent.ACTION_MOVE:
        if (mode == DRAG) {
          panZoomCalculator.doPan(event.getX() - start.x, event.getY() - start.y);
          start.set(event.getX(), event.getY());
        } else if (mode == ZOOM) {
          float newDist = spacing(event);
          Log.d(TAG, "newDist=" + newDist);
          if (newDist > 10f) {
            float scale = newDist / oldDist;
            oldDist = newDist;
            panZoomCalculator.doZoom(scale, mid);
          }
        }
        break;
    }
    return true; // indicate event was handled
  }

  // Determine the space between the first two fingers 
  private float spacing(MotionEvent event) {

    float x = event.getX(0) - event.getX(1);
    float y = event.getY(0) - event.getY(1);
    return FloatMath.sqrt(x * x + y * y);
  }

  // Calculate the mid point of the first two fingers
  private void midPoint(PointF point, MotionEvent event) {
    // ...
    float x = event.getX(0) + event.getX(1);
    float y = event.getY(0) + event.getY(1);
    point.set(x / 2, y / 2);
  }

  class PanZoomCalculator {

    /// The current pan position
    PointF currentPan;
    /// The current zoom position
    float currentZoom;
    /// The windows dimensions that we are zooming/panning in
    View window;
    View child;
    Matrix matrix;
    // Pan jitter is a workaround to get the video view to update it's layout properly when zoom is changed
    int panJitter = 0;
    int anchor;

    PanZoomCalculator(View container, View child, int anchor) {
      // Initialize class fields
      currentPan = new PointF(0, 0);
      currentZoom = 1f;;
      this.window = container;
      this.child = child;
      matrix = new Matrix();
      this.anchor = anchor;
      onPanZoomChanged();
      this.child.addOnLayoutChangeListener(new OnLayoutChangeListener() {
        // This catches when the image bitmap changes, for some reason it doesn't recurse

        public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
          onPanZoomChanged();
        }
      });
    }

    public void doZoom(float scale, PointF zoomCenter) {

      float oldZoom = currentZoom;

      // multiply in the zoom change
      currentZoom *= scale;

      // this limits the zoom
      currentZoom = Math.max(getMinimumZoom(), currentZoom);
      currentZoom = Math.min(8f, currentZoom);

      // Adjust the pan accordingly
      // Need to make it such that the point under the zoomCenter remains under the zoom center after the zoom

      // calculate in fractions of the image so:

      float width = window.getWidth();
      float height = window.getHeight();
      float oldScaledWidth = width * oldZoom;
      float oldScaledHeight = height * oldZoom;
      float newScaledWidth = width * currentZoom;
      float newScaledHeight = height * currentZoom;

      if (anchor == Anchor.CENTER) {

        float reqXPos = ((oldScaledWidth - width) * 0.5f + zoomCenter.x - currentPan.x) / oldScaledWidth;
        float reqYPos = ((oldScaledHeight - height) * 0.5f + zoomCenter.y - currentPan.y) / oldScaledHeight;
        float actualXPos = ((newScaledWidth - width) * 0.5f + zoomCenter.x - currentPan.x) / newScaledWidth;
        float actualYPos = ((newScaledHeight - height) * 0.5f + zoomCenter.y - currentPan.y) / newScaledHeight;

        currentPan.x += (actualXPos - reqXPos) * newScaledWidth;
        currentPan.y += (actualYPos - reqYPos) * newScaledHeight;
      } else {
        // assuming top left
        float reqXPos = (zoomCenter.x - currentPan.x) / oldScaledWidth;
        float reqYPos = (zoomCenter.y - currentPan.y) / oldScaledHeight;
        float actualXPos = (zoomCenter.x - currentPan.x) / newScaledWidth;
        float actualYPos = (zoomCenter.y - currentPan.y) / newScaledHeight;
        currentPan.x += (actualXPos - reqXPos) * newScaledWidth;
        currentPan.y += (actualYPos - reqYPos) * newScaledHeight;
      }

      onPanZoomChanged();
    }

    public void doPan(float panX, float panY) {
      currentPan.x += panX;
      currentPan.y += panY;
      onPanZoomChanged();
    }

    private float getMinimumZoom() {
      return 1f;
    }

    /// Call this to reset the Pan/Zoom state machine
    public void reset() {
      // Reset zoom and pan
      currentZoom = getMinimumZoom();
      currentPan = new PointF(0f, 0f);
      onPanZoomChanged();
    }

    public void onPanZoomChanged() {

      // Things to try: use a scroll view and set the pan from the scrollview
      // when panning, and set the pan of the scroll view when zooming

      float winWidth = window.getWidth();
      float winHeight = window.getHeight();

      if (currentZoom <= 1f) {
        currentPan.x = 0;
        currentPan.y = 0;
      } else if (anchor == Anchor.CENTER) {

        float maxPanX = (currentZoom - 1f) * window.getWidth() * 0.5f;
        float maxPanY = (currentZoom - 1f) * window.getHeight() * 0.5f;
        currentPan.x = Math.max(-maxPanX, Math.min(maxPanX, currentPan.x));
        currentPan.y = Math.max(-maxPanY, Math.min(maxPanY, currentPan.y));
      } else {
        // assume top left
        
        float maxPanX = (currentZoom - 1f) * window.getWidth();
        float maxPanY = (currentZoom - 1f) * window.getHeight();
        currentPan.x = Math.max(-maxPanX, Math.min(0, currentPan.x));
        currentPan.y = Math.max(-maxPanY, Math.min(0, currentPan.y));
      }

      if (child instanceof ImageView && ((ImageView) child).getScaleType()== ImageView.ScaleType.MATRIX) {
        ImageView view = (ImageView) child;
        Drawable drawable = view.getDrawable();
        if (drawable != null) {
          Bitmap bm = ((BitmapDrawable) drawable).getBitmap();
          if (bm != null) {
            // Limit Pan

            float bmWidth = bm.getWidth();
            float bmHeight = bm.getHeight();

            float fitToWindow = Math.min(winWidth / bmWidth, winHeight / bmHeight);
            float xOffset = (winWidth - bmWidth * fitToWindow) * 0.5f * currentZoom;
            float yOffset = (winHeight - bmHeight * fitToWindow) * 0.5f * currentZoom;

            matrix.reset();
            matrix.postScale(currentZoom * fitToWindow, currentZoom * fitToWindow);
            matrix.postTranslate(currentPan.x + xOffset, currentPan.y + yOffset);
            ((ImageView) child).setImageMatrix(matrix);
          }
        }
      } else {
        MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        lp.leftMargin = (int) currentPan.x + panJitter;
        lp.topMargin = (int) currentPan.y;
        lp.width = (int) (window.getWidth() * currentZoom);
        lp.height = (int) (window.getHeight() * currentZoom);
        panJitter ^= 1;

        child.setLayoutParams(lp);
      }
    }
  }
}

Getting WPF DataGrid column headers to use the DisplayName property attribute

The source code to this post is here (requires VS2010): DataGridDisplayNameTest.zip

One of the great things about the new DataGrid in .NET 4.0 is that it allows you to just assign a collection to its ItemSource and it auto generates the columns for you. Great!

I had a class that looked like this:

  public class TestItem {
    [DisplayName("Display Name 1")]
    public int Value1 { get; set; }

    [DisplayName("Display Name 2")]
    public int Value2 { get; set; }

    [DisplayName("Display Name 3")]
    public int Value3 { get; set; }
  }

However when I put a list of these into the DataGrid I didn’t get the Display Name as the column header but rather I got the property name like so:

This next image shows what I wanted:

The new DataGrid started out life as part of the WPF toolkit, I’d figured that the original control needed a fix and so I posted a patch at wpf.codeplex.com, however until that patch was applied to the release version it wasn’t going to get around my immediate problem, so I came up with the solution below which handles the AutoGeneratingColumn event in the DataGrid to set the headings.

MainWindow.xaml:

<Window x:Class="DataGridDisplayNameTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525"
        xmlns:dg="clr-namespace:System.Windows.Controls;assembly=PresentationFramework">
    <Grid>
      <dg:DataGrid x:Name="theGrid" AutoGenerateColumns="True" AutoGeneratingColumn="dg_AutoGeneratingColumn">
      </dg:DataGrid>
  </Grid>
</Window>

MainWindow.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Reflection;

namespace DataGridDisplayNameTest {

  /// <summary>
  /// This test app demonstrates how to set the Column Headers in the WPF DataGrid
  /// to the display name attribute of the bound property.
  /// </summary>
  public partial class MainWindow : Window {
    public MainWindow() {
      this.Initialized += new EventHandler(MainWindow_Initialized);
      InitializeComponent();
    }

    /// <summary>
    /// Add data to controls after they have been initialized
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void MainWindow_Initialized(object sender, EventArgs e) {
      this.theGrid.ItemsSource = new ObservableCollection<TestItem>{
        new TestItem {
          Value1 = 11, Value2 = 12, Value3 = 13 
        },
        new TestItem {
          Value1 = 21, Value2 = 22, Value3 = 23 
        },
        new TestItem {
          Value1 = 31, Value2 = 32, Value3 = 33 
        }
      };
    }

    /// <summary>
    /// Event handler for when columns are added to the data grid
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void dg_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e) {

      string displayName = GetPropertyDisplayName(e.PropertyDescriptor);
      if (!string.IsNullOrEmpty(displayName)) {
        e.Column.Header = displayName;
      }

    }

    /// <summary>
    /// Gets the Display Name for the property descriptor passed in
    /// </summary>
    /// <param name="descriptor"></param>
    /// <returns></returns>
    public static string GetPropertyDisplayName(object descriptor) {

      PropertyDescriptor pd = descriptor as PropertyDescriptor;
      if (pd != null) {
        // Check for DisplayName attribute and set the column header accordingly
        DisplayNameAttribute displayName = pd.Attributes[typeof(DisplayNameAttribute)] as DisplayNameAttribute;
        if (displayName != null && displayName != DisplayNameAttribute.Default) {
          return displayName.DisplayName;
        }

      } else {
        PropertyInfo pi = descriptor as PropertyInfo;
        if (pi != null) {
          // Check for DisplayName attribute and set the column header accordingly
          Object[] attributes = pi.GetCustomAttributes(typeof(DisplayNameAttribute), true);
          for (int i = 0; i < attributes.Length; ++i) {
            DisplayNameAttribute displayName = attributes[i] as DisplayNameAttribute;
            if (displayName != null && displayName != DisplayNameAttribute.Default) {
              return displayName.DisplayName;
            }
          }
        }
      }
      return null;
    }

  }

  public class TestItem {
    [DisplayName("Display Name 1")]
    public int Value1 { get; set; }
    [DisplayName("Display Name 2")]
    public int Value2 { get; set; }
    [DisplayName("Display Name 3")]
    public int Value3 { get; set; }

  }
}

Group random dates into contiguous sequences

The goal here is to group random dates like:

1-Nov
2-Nov
5-Nov
6-Nov
7-Nov
8-Nov

into groups of contiguous dates listing the first and last like this:

1-Nov, 2-Nov
5-Nov, 8-Nov

Using a couple of analytic functions it turns out to be not too hard. Below there is a “dates” inline view built with some recursive SQL. “Dates” contains two sample sequences to be grouped. The first query contains all the inner workings of the process expanded out so you can see what is going on and provides an intermediate result. The second query removes the excess calculation columns and produces the final result.

 
SQL> set echo on pagesiz 50
SQL> @sequences.sql
SQL> ALTER SESSION SET NLS_DATE_FORMAT = 'DD-Mon-YYYY';

Session altered.

SQL>
SQL> EXEC dbms_random.seed(123);

PL/SQL procedure successfully completed.

SQL>
SQL> -- Generate a random list of dates to put into sequential groups
SQL> WITH dates AS (
  2  SELECT DISTINCT DECODE(MOD(LEVEL,2),0,'A','B') name, TRUNC(SYSDATE + dbms_random.value(-15,15)) dte FROM dual
  3  CONNECT BY LEVEL <= 60
  4  )
  5  -- Using LAG to compare rows determine where sequences start and stop, tag new sequences with 1, continuations with 0
  6  -- Calculations shown in separate columns for clarity
  7  , start_seq AS (
  8  SELECT name, dte, LAG(dte) OVER (PARTITION BY name ORDER BY dte) dte_lag,
  9         dte - LAG(dte) OVER (PARTITION BY name ORDER BY dte) dte_diff,
 10         DECODE(dte - LAG(dte) OVER (PARTITION BY name ORDER BY dte),1,0,1) seq_start
 11    FROM dates
 12  )
 13  -- SUM up the seq_start column in order of dte to number the sequences
 14  SELECT ss.*, SUM(seq_start) OVER (PARTITION BY name ORDER BY dte) seq_no
 15    FROM start_seq ss
 16       ;

N DTE         DTE_LAG       DTE_DIFF  SEQ_START     SEQ_NO
- ----------- ----------- ---------- ---------- ----------
A 05-Nov-2010                                 1          1
A 09-Nov-2010 05-Nov-2010          4          1          2
A 10-Nov-2010 09-Nov-2010          1          0          2
A 11-Nov-2010 10-Nov-2010          1          0          2
A 13-Nov-2010 11-Nov-2010          2          1          3
A 14-Nov-2010 13-Nov-2010          1          0          3
A 15-Nov-2010 14-Nov-2010          1          0          3
A 16-Nov-2010 15-Nov-2010          1          0          3
A 18-Nov-2010 16-Nov-2010          2          1          4
A 19-Nov-2010 18-Nov-2010          1          0          4
A 20-Nov-2010 19-Nov-2010          1          0          4
A 21-Nov-2010 20-Nov-2010          1          0          4
A 24-Nov-2010 21-Nov-2010          3          1          5
A 25-Nov-2010 24-Nov-2010          1          0          5
A 28-Nov-2010 25-Nov-2010          3          1          6
A 01-Dec-2010 28-Nov-2010          3          1          7
A 02-Dec-2010 01-Dec-2010          1          0          7
A 03-Dec-2010 02-Dec-2010          1          0          7
B 06-Nov-2010                                 1          1
B 07-Nov-2010 06-Nov-2010          1          0          1
B 10-Nov-2010 07-Nov-2010          3          1          2
B 11-Nov-2010 10-Nov-2010          1          0          2
B 12-Nov-2010 11-Nov-2010          1          0          2
B 13-Nov-2010 12-Nov-2010          1          0          2
B 14-Nov-2010 13-Nov-2010          1          0          2
B 15-Nov-2010 14-Nov-2010          1          0          2
B 16-Nov-2010 15-Nov-2010          1          0          2
B 19-Nov-2010 16-Nov-2010          3          1          3
B 20-Nov-2010 19-Nov-2010          1          0          3
B 21-Nov-2010 20-Nov-2010          1          0          3
B 22-Nov-2010 21-Nov-2010          1          0          3
B 23-Nov-2010 22-Nov-2010          1          0          3
B 24-Nov-2010 23-Nov-2010          1          0          3
B 26-Nov-2010 24-Nov-2010          2          1          4
B 28-Nov-2010 26-Nov-2010          2          1          5
B 29-Nov-2010 28-Nov-2010          1          0          5
B 30-Nov-2010 29-Nov-2010          1          0          5
B 02-Dec-2010 30-Nov-2010          2          1          6
B 03-Dec-2010 02-Dec-2010          1          0          6

39 rows selected.

SQL>
SQL>
SQL> -- Once you have numbered sequences you can GROUP BY to get the final result.
SQL> -- Stripping the code down to the simplest possible and grouping yields:
SQL>
SQL> EXEC dbms_random.seed(123);

PL/SQL procedure successfully completed.

SQL>
SQL> WITH dates AS (
  2  SELECT DISTINCT DECODE(MOD(LEVEL,2),0,'A','B') name, TRUNC(SYSDATE + dbms_random.value(-15,15)) dte FROM dual
  3  CONNECT BY LEVEL <= 60
  4  )
  5  , start_seq AS (
  6  SELECT name, dte, DECODE(dte - LAG(dte) OVER (PARTITION BY name ORDER BY dte),1,0,1) seq_start
  7    FROM dates
  8  ), numbered_seq AS (
  9  SELECT ss.*, SUM(seq_start) OVER (PARTITION BY name ORDER BY dte) seq_no
 10    FROM start_seq ss
 11  )
 12  SELECT name, MIN(dte) dte_min, MAX(dte) dte_max
 13    FROM numbered_seq
 14  GROUP BY name, seq_no
 15  ORDER BY name, seq_no
 16       ;

N DTE_MIN     DTE_MAX
- ----------- -----------
A 05-Nov-2010 05-Nov-2010
A 09-Nov-2010 11-Nov-2010
A 13-Nov-2010 16-Nov-2010
A 18-Nov-2010 21-Nov-2010
A 24-Nov-2010 25-Nov-2010
A 28-Nov-2010 28-Nov-2010
A 01-Dec-2010 03-Dec-2010
B 06-Nov-2010 07-Nov-2010
B 10-Nov-2010 16-Nov-2010
B 19-Nov-2010 24-Nov-2010
B 26-Nov-2010 26-Nov-2010
B 28-Nov-2010 30-Nov-2010
B 02-Dec-2010 03-Dec-2010

13 rows selected.

SQL>

Note: Because I’ve seeded the random value generator it produces a consistent set for each query.

Passing a managed handle via a void pointer

A quick C++/CLI communicating through native tip. The sample below shows how you can pass a managed reference to native code via a void pointer, and recover the managed reference after the native code passes the void pointer back to you. Useful for C++ callbacks that pass back user data.

void SomeFunction(void* input)
{
  gcroot* pointer = (gcroot*)(input);
  (*pointer)->ManagedFunction();
}

void Example()
{
  ManagedClass^ handle = gcnew ManagedClass();
  gcroot* pointer = new gcroot(handle);
  SomeFunction((void*)pointer);
  delete pointer;
}

Turning off Clear Type or Font Smoothing in VS2010

So you’ve got yourself a fresh install of VS2010 and the fonts are all smoothed. Naturally the first thing you want to do is turn that off. The trick is that you have to change the default font from Consolas to Courier New.

Go to:

Tools->Options->Environment->Fonts and Colors

and change it.

Multi-Column Histograms (EXTENDED_STATS)

Source: mcol_histogram.sql

Oracle 11gR1 iconWhen running an OLTP system you may have a transactions table with things like transaction ids, transaction types and states of transaction (open, closed, cancelled, etc). Different transactions tend to have different usage patterns – some might be automatically dealt with, others require user interaction, some typically rejected and still others where a lot of mistakes are common so they get cancelled a lot. There are differences in the in-flight and final states between the various transactions but since they are mixed into the one transaction table the skew in the states can be obscured when looking at only a single column. The skew may be only visible when both the type and state are considered together – a situation for which extended stats is perfect for. To test the effect of multi-column histograms or EXTENDED_STATS in this type of scenario we’ll need a sample table which shows a skew only when multiple columns are considered. I’ve got type A transactions which typically have a final state of closed and type B transactions which typically end up getting cancelled. However a few As (10) do get cancelled and a few Bs (10) make it to a closed state. I have no open or other state transactions.

The test scenario is:

  1. Create a transaction table with skewed data, transtype A mostly closed, B mostly cancelled.
  2. Take a look at the data to verify that it has the expected skew. Note that there are 10 cancelled A transactions.
  3. Explain plan for filter(“TRANSTYPE”=’A’ AND “STATE”=’Cancelled’) and note the expected row count: 500, which is Oracle’s best guess based on information available in normal single column histograms.
  4. Try again but with a dynamic_sampling level of 4 and see the correct expected row count (10).
  5. Generate multi-column stats on ‘(transtype,state)’
  6. Retry the query without dynamic sampling and see that Oracle 11g is able to use the new stats to correctly estimate the row count of 10 and uses an index in preference to a table scan.

Obviously this is a contrived example which deliberately obscures the skew in the data with perfect balance between the two transaction types but with a larger number of transaction types it is easy for such patterns to be muddied. With the aid of a histogram on the two critical columns here Oracle was able to see the situation accurately and modify the execution plan accordingly going from a full table scan to an index lookup. A more accurate picture of the data will result in superior execution plans. Using the dynamic_sampling hint results in the exact same benefits. Oracle’s documentation on dynamic sampling levels says of level 4:

Level 4: Apply dynamic sampling to all tables that meet Level 3 criteria, plus all tables that have single-table predicates that reference 2 or more columns. The number of blocks sampled is the default number of dynamic sampling blocks. For unanalyzed tables, the number of blocks sampled is two times the default number of dynamic sampling blocks.

Note: The “estimate” mentioned in step 6 is the exact number because for the stats gathering parameter estimate_percent I’ve used NULL or compute which may not be practical in a production system. You’ll need to use a percentage which balances accurately modelling the data against stats gathering time. This interesting piece on that topic comes to the conclusion that between 5-20% is the right number but the analysis extends only as far as 10g.

Here are the results of running the SQL script mcol_histogram which you can download from the link up near the start of this post:

SQL> set linesize 100 echo on
SQL> @mcol_histogram
SQL> -- Step 1: Create a transaction table with skewed data, transtype A mostly closed, B mostly cancelled
SQL> CREATE TABLE transactions AS
  2  SELECT tmp.*, (CASE WHEN (tmp.transtype = 'A' AND MOD(transid,100) != 0) OR
  3                           (tmp.transtype = 'B' AND MOD(transid,100)  = 0) THEN 'Closed'
  4                      ELSE 'Cancelled'
  5                  END) state FROM
  6      (
  7      SELECT LEVEL transid, DECODE(FLOOR(LEVEL/1001),0,'A','B') transtype
  8        FROM dual
  9      CONNECT BY LEVEL <= 2000
 10      ) tmp;

Table created.

SQL>
SQL> CREATE INDEX ix1transactions ON transactions(transtype, state);

Index created.

SQL>
SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS(ownname => SYS_CONTEXT('USERENV','CURRENT_USER'), 
tabname => 'transactions', estimate_percent => NULL, method_opt => 'FOR ALL COLUMNS SIZE SKEWONLY', cascade => true);

PL/SQL procedure successfully completed.

SQL>
SQL> -- Step 2: Take a look at the data to verify that it has the expected skew.
SQL> SELECT transtype, state, COUNT(*)
  2    FROM transactions
  3  GROUP BY transtype, state
  4  ORDER BY transtype, state
  5    ;

T STATE       COUNT(*)
- --------- ----------
A Cancelled         10
A Closed           990
B Cancelled        990
B Closed            10

SQL>
SQL> -- Step 3:  This only returns 10 rows, but we have a full table scan and high cardinality
SQL> EXPLAIN PLAN FOR
  2  SELECT *
  3    FROM transactions t
  4   WHERE transtype = 'A'
  5     AND state = 'Cancelled';

Explained.

SQL>
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
Plan hash value: 1321366336

----------------------------------------------------------------------------------
| Id  | Operation         | Name         | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |              |   500 |  7500 |     3   (0)| 00:00:45 |
|*  1 |  TABLE ACCESS FULL| TRANSACTIONS |   500 |  7500 |     3   (0)| 00:00:45 |
----------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------

   1 - filter("TRANSTYPE"='A' AND "STATE"='Cancelled')

13 rows selected.

SQL>
SQL> -- Step 4:  Try again but use dynamic sampling this time and get much better results
SQL> EXPLAIN PLAN FOR
  2  SELECT /*+ dynamic_sampling (4) */ *
  3    FROM transactions t
  4   WHERE transtype = 'A'
  5     AND state = 'Cancelled';

Explained.

SQL>
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
Plan hash value: 278489313

-----------------------------------------------------------------------------------------------
| Id  | Operation                   | Name            | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |                 |    10 |   150 |     2   (0)| 00:00:30 |
|   1 |  TABLE ACCESS BY INDEX ROWID| TRANSACTIONS    |    10 |   150 |     2   (0)| 00:00:30 |
|*  2 |   INDEX RANGE SCAN          | IX1TRANSACTIONS |    10 |       |     1   (0)| 00:00:15 |
-----------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
---------------------------------------------------

   2 - access("TRANSTYPE"='A' AND "STATE"='Cancelled')

Note
-----
   - dynamic sampling used for this statement (level=4)

18 rows selected.

SQL>
SQL> -- Step 5:  Generate some multi-column stats
SQL> SELECT DBMS_STATS.CREATE_EXTENDED_STATS (SYS_CONTEXT('USERENV','CURRENT_USER'),'transactions',
'(transtype,state)') FROM dual;

DBMS_STATS.CREATE_EXTENDED_STATS(SYS_CONTEXT('USERENV','CURRENT_USER'),'TRANSACTIONS','(TRANSTYPE,ST
----------------------------------------------------------------------------------------------------
SYS_STUMWLR#03VMK70K_1F3J$2ZMZ

SQL>
SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS(ownname => SYS_CONTEXT('USERENV','CURRENT_USER'), 
tabname => 'transactions', estimate_percent => NULL, method_opt => 'FOR ALL COLUMNS SIZE SKEWONLY', cascade => true);

PL/SQL procedure successfully completed.

SQL>
SQL> -- Step 6:  The new extended stats yield a cardinality of 10 and use an index lookup
SQL> EXPLAIN PLAN FOR
  2  SELECT *
  3    FROM transactions t
  4   WHERE transtype = 'A'
  5     AND state = 'Cancelled';

Explained.

SQL>
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
Plan hash value: 278489313

-----------------------------------------------------------------------------------------------
| Id  | Operation                   | Name            | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |                 |    10 |   150 |     2   (0)| 00:00:30 |
|   1 |  TABLE ACCESS BY INDEX ROWID| TRANSACTIONS    |    10 |   150 |     2   (0)| 00:00:30 |
|*  2 |   INDEX RANGE SCAN          | IX1TRANSACTIONS |    10 |       |     1   (0)| 00:00:15 |
-----------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
---------------------------------------------------

   2 - access("TRANSTYPE"='A' AND "STATE"='Cancelled')

14 rows selected.

SQL>
SQL> DROP TABLE transactions;

Table dropped.

SQL>
SQL> SELECT * FROM v$version;

BANNER
--------------------------------------------------------------------------------
Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - Production
PL/SQL Release 11.2.0.1.0 - Production
CORE    11.2.0.1.0      Production
TNS for Linux: Version 11.2.0.1.0 - Production
NLSRTL Version 11.2.0.1.0 - Production

SQL>

Final Note: The new stats are difficult to understand in the dba_histogram/user_histogram views with the endpoint_value being encoded in a different format to normal stats. I tried running Tom Kyte’s hexstr to decode it but was unsuccessful. Perhaps someone else will have better luck. Note that only the first 6 characters of endpoint_value can be decoded for normal histogram columns so it is perhaps better to stick to 6 letter transaction states rather than using things like ‘cancelled’.

Compound triggers and mutating tables

Source: trigger.sql

Oracle 11gR1 iconIf you are in a mutating table situation where your trigger needs to read from the same table that it is firing on your only real option before Oracle 11g was a multi trigger solution with a muddle of row and statement triggers communicating via a package. But now Oracle 11g comes through with a great new feature: compound triggers which neatly solve the problem … or do they. Well they purport to do so but it isn’t all smooth sailing: Beware the compound trigger. But I figured – that’s 11.1.0.6 whereas I’m using 11.1.0.7 and those particular failure modes don’t apply to my situation so I’ll be fine. Not exactly. What I have is a normal after statement trigger firing on the “banner” column and modifying data in the “changed” column and I need to fire a second trigger on that second column. What I saw was that the compound trigger worked properly once and then not again until it was recompiled.

trg_after => fires on column “banner” and modifies column “changed”.
trg_compound_row => fires on column “changed”
trg_simple_row => fires on same criteria as compound trigger for comparative purposes

The test scenario is:
0) Create table + triggers
1) Update table to see triggers firing then rollback to reset
2) Do step 1 again
3) Recompile trigger
4) Do step 1 twice
5) Drop table

SQL> set echo on
SQL> @trigger.sql
SQL> SET SERVEROUT ON LINESIZE 200
SQL>
SQL> CREATE table trigger_test AS
  2  SELECT rownum id, banner, 'A' changed FROM v$version;

Table created.

SQL>
SQL> CREATE OR REPLACE TRIGGER trg_after
  2      AFTER UPDATE OF banner ON trigger_test
  3  BEGIN
  4      dbms_output.put_line('Update');
  5      UPDATE trigger_test SET changed = 'B'
  6       WHERE id = 1;
  7  END;
  8  /

Trigger created.

SQL> CREATE OR REPLACE TRIGGER trg_compound_row
  2    FOR UPDATE OF changed ON trigger_test
  3  COMPOUND TRIGGER
  4
  5  AFTER EACH ROW IS BEGIN
  6      dbms_output.put_line('Compound fired <== Should happen every time');
  7  END AFTER EACH ROW;
  8
  9  END trg_compound_row;
 10  /

Trigger created.

SQL> CREATE OR REPLACE TRIGGER trg_simple_row
  2      AFTER UPDATE OF changed ON trigger_test
  3      FOR EACH ROW
  4  BEGIN
  5      dbms_output.put_line('Simple fired');
  6  END;
  7  /

Trigger created.

SQL> SELECT * FROM trigger_test;

        ID BANNER                                                                           C
---------- -------------------------------------------------------------------------------- -
         1 Oracle Database 11g Enterprise Edition Release 11.1.0.7.0 - 64bit Production     A
         2 PL/SQL Release 11.1.0.7.0 - Production                                           A
         3 CORE 11.1.0.7.0      Production                                                       A
         4 TNS for Solaris: Version 11.1.0.7.0 - Production                                 A
         5 NLSRTL Version 11.1.0.7.0 - Production                                           A

SQL>
SQL> UPDATE trigger_test SET banner = banner || ' ' WHERE id = 2;
Update
Compound fired <== Should happen every time
Simple fired

1 row updated.

SQL> ROLLBACK;

Rollback complete.

SQL>
SQL> UPDATE trigger_test SET banner = banner || ' ' WHERE id = 2;
Update
Simple fired

1 row updated.

SQL> ROLLBACK;

Rollback complete.

SQL>
SQL> ALTER TRIGGER trg_compound_row COMPILE;

Trigger altered.

SQL>
SQL> UPDATE trigger_test SET banner = banner || ' ' WHERE id = 2;
Update
Compound fired <== Should happen every time
Simple fired

1 row updated.

SQL> ROLLBACK;

Rollback complete.

SQL>
SQL> UPDATE trigger_test SET banner = banner || ' ' WHERE id = 2;
Update
Simple fired

1 row updated.

SQL> ROLLBACK;

Rollback complete.

SQL>
SQL> DROP TABLE trigger_test;

Table dropped.

SQL>

So the lesson is clear, as Dominic Brooks already stated: Beware the compound trigger.

Bing Maps 3D – A Dashed Line With Constant Spacing

I was only going to write 3 Bing Maps 3D posts (they start Here), but today I added something that I thought was rather cool.

You can download the source code for this post here 3DViewerBingMaps.zip

In OpenGL you can draw something called a Stippled Line which is basically a dashed line with some extra control over the dash pattern, however in Bing Maps 3D there is no such thing, which is why I wrote my own dashed line mesh class. My class supports the following things:

- Lines are made up of multiple segments
- Dashes and gaps between dashes are independent in size
- You can set an offset each frame allowing for a moving ant effect
- You can set different widths for the start and the end of the line

Here’s 2 pictures of the same line showing the billboard effect of the wide dashes, and the constant dash and gap size:

DashedLine1

DashedLine2

If you download the sample at the top of this post you can run the application shown in the screenshots.

Here’s the code snippet for setting up the above Dashed Mesh:

DashedLineMesh dashedLine = new DashedLineMesh();

dashedLine.DashLength = 8;
dashedLine.GapLength = 16;
dashedLine.LineStartWidth = 1;
dashedLine.LineEndWidth = 64;
dashedLine.Color = Color.White;
dashedLine.ZBufferEnable = true;

dashedLine.Points.Clear();
GeodeticViewpoint centrePoint = new GeodeticViewpoint();
double alt = 1000000;
double lat = 45.0;
double lon = 180.0;

for(double i = 0; i<(Math.PI*2); i+=0.103)
{
  double x = 8+30 * Math.Sin(i);
  double y = 8+30 * Math.Cos(i);
  centrePoint.Position.Location = LatLonAlt.CreateUsingDegrees(lat+x, lon+y, alt);
  dashedLine.Points.Add(new Vector3F(centrePoint.Position.Vector));
}

and as a bonus here’s the complete source code for the DashedLineMesh class

// Dashed Line Mesh by John Stewien 2010

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.MapPoint.Rendering3D.GraphicsProxy;
using Microsoft.MapPoint.Geometry.VectorMath;
using Microsoft.MapPoint.Geometry;
using Microsoft.MapPoint.Graphics3D;
using Microsoft.MapPoint.Rendering3D.State;

namespace _3DViewerBingMaps.Plugin {

  /// <summary>
  /// Draws a multi-segment line that can be broken up into dashes
  /// </summary>
  public class DashedLineMesh : MeshGraphicsObject<Vertex.PositionOnly, ushort> {

    // ************************************************************************
    // Private Fields
    // ************************************************************************
    #region Private Fields

    /// <summary>
    /// A list of points that make up the line to be drawn. Points are in
    /// Lat, Lon, Alt format.
    /// </summary>
    private List<Vector3F> points;
    /// <summary>
    /// The starting width of the line in pixels
    /// </summary>
    private float lineStartWidth = 1f;
    /// <summary>
    /// The ending width of the line in pixels
    /// </summary>
    private float lineEndWidth = 1f;
    /// <summary>
    /// The length of the dashes in pixels
    /// </summary>
    private float dashLength = 0f;
    /// <summary>
    /// the length of the gaps between the dashes in pixels
    /// </summary>
    private float gapLength = 0f;
    /// <summary>
    /// The offset of the dahses in pixels. Incrementing this by .5 every frame
    /// gives a moving and effect.
    /// </summary>
    private float dashOffset = 0f;
    /// <summary>
    /// Enable alpha blending for this mesh
    /// </summary>
    private bool alphaEnable = false;
    /// <summary>
    /// Enable using the Depth Buffer for this mesh
    /// </summary>
    private bool zBufferEnable = true;
    /// <summary>
    /// The color to draw this line in
    /// </summary>
    private System.Drawing.Color color = System.Drawing.Color.White;

    #endregion Private Fields

    // ************************************************************************
    // Public Methods
    // ************************************************************************
    #region Public Methods

    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="radius"></param>
    /// <param name="material"></param>
    /// <param name="materialId"></param>
    public DashedLineMesh()
      : base(GraphicsBufferUsage.Dynamic, GraphicsBufferUsage.Dynamic, 4, 4, true) {

      this.points = new List<Vector3F>();

      if (base.State.Materials.Count == 0) {
        base.State.Materials.Add(new Material());
      }
      base.ProjectionEnabled = false;
      this.UpdateRenderState();
    }

    /// <summary>
    /// Updates the graphic prior to rendering it in a frame. Needs to be
    /// called every frame.
    /// </summary>
    /// <param name="camera"></param>
    /// <param name="transform"></param>
    /// <remarks>
    /// Unfortunately the GraphicsObject.UpdateInternalRenderObject method
    /// can't be overriden because of its "internal" protection, so this
    /// needs to be called every frame before the graphics is rendered
    /// </remarks>
    public void PreRenderUpdate(CameraData camera, Matrix4x4D transform) {
      int width = camera.View.Size.Width;
      int height = camera.View.Size.Height;
      this.UpdateLineGeometry(width, height, transform*camera.Snapshot.TransformMatrix);
    }

    #endregion Public Methods

    // ************************************************************************
    // Private Methods
    // ************************************************************************
    #region Private Methods

    /// <summary>
    /// Calculates the required line width from the user specified value and the
    /// size of the display area
    /// </summary>
    /// <param name="deviceWidth"></param>
    /// <param name="deviceHeight"></param>
    /// <returns></returns>
    private Vector3F GetWidth(float lineWidth, int deviceWidth, int deviceHeight) {
      float x = lineWidth / ((float)deviceWidth);
      float y = lineWidth / ((float)deviceHeight);
      Vector3F retVal = new Vector3F(x, y, 1f);
      return retVal;
    }

    /// <summary>
    /// Apply the position/scale tranform, and the projection transform to the points
    /// to convert them to screen coordinates
    /// </summary>
    /// <param name="projectionTransform"></param>
    /// <returns></returns>
    private IList<Vector3F> TransformedPoints(Matrix4x4D projectionTransform) {
      List<Vector3F> list = new List<Vector3F>(this.points.Count);
      Matrix4x4D matrix = projectionTransform;
      foreach (Vector3F vectorf in this.points) {
        Vector4D vector = new Vector4D((double)vectorf.X, (double)vectorf.Y, (double)vectorf.Z, 1.0);
        vector = Vector4D.TransformCoordinate(ref vector, ref matrix);
        if (vector.W > 0.0) {
          vector.MultiplyBy(1.0 / vector.W);
        } else {
          vector.MultiplyBy(1.0 / -vector.W);
        }
        list.Add(new Vector3F((float)vector.X, (float)vector.Y, (float)vector.Z));
      }
      return list;
    }

    /// <summary>
    /// This method takes all the solid lines and breaks them up into dashes
    /// </summary>
    /// <param name="lineList"></param>
    /// <param name="widthList"></param>
    /// <param name="deviceWidth"></param>
    /// <param name="deviceHeight"></param>
    private bool ConvertToDashed(ref IList<Vector3F> lineList, ref IList<float> widthList, int deviceWidth, int deviceHeight) {
      this.dashOffset = dashOffset % (dashLength + gapLength);

      // Scale everything to viewport space
      float dashStep = 2 * this.dashLength / Math.Min(deviceWidth, deviceHeight);
      float gapStep = 2 * this.gapLength / Math.Min(deviceWidth, deviceHeight);
      float offsetStep = 2 * this.dashOffset / Math.Min(deviceWidth, deviceHeight);

      // Sanity check dashStep and gapStep
      if (dashStep <= 0 || gapStep <= 0) {
        return false;
      }

      // Calculate the initial conditions
      float remainder = offsetStep;
      bool doDash = remainder==0;
      if (remainder > gapStep) {
        remainder -= gapStep;
        doDash = true;
      }

      bool doingJoin = false;

      List<Vector3F> dashedLineList = new List<Vector3F>();
      List<float> dashedWidthList = new List<float>();

      for (int i = 0; i < (lineList.Count - 1); i++) {
        Vector3F fromPoint = lineList[i];
        Vector3F toPoint = lineList[i + 1];

        // Store the original line length. Used for calculating the line width later on.
        float lineLength = Vector3F.Distance(fromPoint, toPoint);

        // Clamp the line to the visible area otherwise a lot more calculation will be done
        // than is required
        if (ClampLineToBox(ref fromPoint, ref toPoint, new Vector2F(-1, -1), new Vector2F(1, 1))) {

          // Need to break up the line in the X-Y direction regardless of Z
          Vector2F segment = new Vector2F(toPoint.X - fromPoint.X, toPoint.Y - fromPoint.Y);
          float segLength = segment.Length();
          if (segLength > 0) {

            float widthDiff = widthList[i + 1] - widthList[i];

            Vector3F step = new Vector3F(segment.X, segment.Y, toPoint.Z - fromPoint.Z) / segLength;

            float distTravelled = 0;
            float nextDash = remainder > 0 ? remainder : dashStep;
            float nextgap = remainder > 0 ? remainder : gapStep;

            // Calcluate the line width for the point we are starting at and
            // Calculate the line width increments for when we iterate over the dashes

            float invLineLength = 1f / lineLength;
            float progressiveWidth = widthList[i] + widthDiff * (remainder + Vector3F.Distance(fromPoint, lineList[i])) * invLineLength;
            float dashDWidth = widthDiff * dashStep * invLineLength;
            float gapDWidth = widthDiff * gapStep * invLineLength;

            // Keep on adding dashes until we reach the end of the line segment
            // Also calculate the line widths for each end of the dashes

            while (distTravelled < segLength) {
              if (doDash) {
                if (doingJoin) {
                  dashedLineList.RemoveAt(dashedLineList.Count - 1);
                  dashedWidthList.RemoveAt(dashedWidthList.Count - 1);
                } else {
                  dashedLineList.Add(distTravelled * step + fromPoint);
                  dashedWidthList.Add(progressiveWidth);
                }

                doingJoin = false;

                distTravelled += nextDash;
                progressiveWidth += dashDWidth;

                dashedLineList.Add(Math.Min(segLength, distTravelled) * step + fromPoint);
                dashedWidthList.Add(progressiveWidth);

                if (distTravelled > segLength && i < (lineList.Count - 2)) {
                  doingJoin = true;
                }
              } else {
                distTravelled += nextgap;
                progressiveWidth += gapDWidth;
              }
              doDash = !doDash;
              nextDash = dashStep;
              nextgap = gapStep;
            }
            remainder = distTravelled - segLength;
            if (remainder > 0) {
              doDash = !doDash;
            }
          }
        } else {
          doingJoin = false;
        }
      }
      lineList = dashedLineList;
      widthList = dashedWidthList;
      return true;
    }

    /// <summary>
    /// Clamps the line between the 2 vectors passed to be within the box limits passed in.
    /// </summary>
    /// <param name="from"></param>
    /// <param name="to"></param>
    /// <param name="min"></param>
    /// <param name="max"></param>
    /// <returns>
    /// true if the line is visible, false otherwise
    /// </returns>
    private bool ClampLineToBox(ref Vector3F from, ref Vector3F to, Vector2F boxMin, Vector2F boxMax) {
      // need to limit everything to within -1 to 1 on all dimensions

      // Test if anything is inside the viewbox
      if (Math.Min(from.X, to.X) > boxMax.X || Math.Max(from.X, to.X) < boxMin.X ||
        Math.Min(from.Y, to.Y) > boxMax.Y || Math.Max(from.Y, to.Y) < boxMin.Y) {
        return false;
      }

      // Test if the line is completely contained in the viewbox
      if (Math.Min(from.X, to.X) > boxMin.X && Math.Max(from.X, to.X) < boxMax.X &&
        Math.Min(from.Y, to.Y) > boxMin.Y && Math.Max(from.Y, to.Y) < boxMax.Y) {
        return true;
      }

      ClampDestToBox(ref from, ref to, boxMin, boxMax);
      ClampDestToBox(ref to, ref from, boxMin, boxMax);

      return true;
    }

    /// <summary>
    /// Clamps the destination point to the box limits passed in
    /// </summary>
    /// <param name="from"></param>
    /// <param name="dest"></param>
    /// <param name="boxMin"></param>
    /// <param name="boxMax"></param>
    private void ClampDestToBox(ref Vector3F from, ref Vector3F dest, Vector2F boxMin, Vector2F boxMax) {
      Vector3F diff = dest - from;

      if (dest.X > boxMax.X) {
        dest = (boxMax.X - from.X) / diff.X * diff + from;
      } else if (dest.X < boxMin.X) {
        dest = (boxMin.X - from.X) / diff.X * diff + from;
      }
      if (dest.Y > boxMax.Y) {
        dest = (boxMax.Y - from.Y) / diff.Y * diff + from;
      } else if (dest.Y < boxMin.Y) {
        dest = (boxMin.Y - from.Y) / diff.Y * diff + from;
      }
    }

    /// <summary>
    /// Updates the vertex and index buffer for rendering the line for the current viewpoint
    /// </summary>
    /// <param name="deviceWidth"></param>
    /// <param name="deviceHeight"></param>
    /// <param name="projectionTransform"></param>
    private void UpdateLineGeometry(int deviceWidth, int deviceHeight, Matrix4x4D projectionTransform) {
      this.Indices.Clear();
      this.Vertices.Clear();

      // Get all the points in screen coordinates
      IList<Vector3F> list = this.TransformedPoints(projectionTransform);
      IList<float> widths = this.GetWidths(list);
      bool isDashed = this.ConvertToDashed(ref list, ref widths, deviceWidth, deviceHeight);
      for (int i = 0; i < (list.Count - 1); i++) {
        Vector3F width1 = this.GetWidth(widths[i], deviceWidth, deviceHeight);
        Vector3F width2 = this.GetWidth(widths[i+1], deviceWidth, deviceHeight);
       
        Vector3F difference = list[i + 1] - list[i];
        Vector2F direction = Vector2F.Normalize(new Vector2F(difference.X, difference.Y));
        Vector3F perpendicular1 = new Vector3F(-direction.Y * width1.X, direction.X * width1.Y, 0f);
        Vector3F perpendicular2 = new Vector3F(-direction.Y * width2.X, direction.X * width2.Y, 0f);
        this.Vertices.AddData(new Vertex.PositionOnly(list[i] + perpendicular1));
        this.Vertices.AddData(new Vertex.PositionOnly(list[i] - perpendicular1));
        this.Vertices.AddData(new Vertex.PositionOnly(list[i+1] + perpendicular2));
        this.Vertices.AddData(new Vertex.PositionOnly(list[i+1] - perpendicular2));
        int num2 = i*2;
        ushort[] data = new ushort[] { (ushort)num2, (ushort)(num2 + 2), (ushort)(num2 + 3), (ushort)num2, (ushort)(num2 + 3), (ushort)(num2 + 1) };
        this.Indices.AddDataUnsafe(data, PrimitiveType.TriangleList, 0);
        if (isDashed)
          i++;
      }
    }

    /// <summary>
    /// Creates a list of widths for the points passed in.
    /// </summary>
    /// <param name="list"></param>
    /// <returns></returns>
    private IList<float> GetWidths(IList<Vector3F> list) {
      float[] progressiveLength = new float[list.Count];
      float totalLength = 0;
      for (int i = 0; i < (list.Count-1); ++i) {
        progressiveLength[i] = totalLength;
        totalLength += Vector3F.Distance(list[i], list[i + 1]);
      }
      progressiveLength[list.Count - 1] = totalLength;

      if (totalLength > 0) {
        float widthDiff = lineEndWidth - lineStartWidth;
        float invTotalLength = 1f/totalLength;
        for (int i = 0; i < progressiveLength.Length; ++i) {
          progressiveLength[i] = lineStartWidth + widthDiff * progressiveLength[i] * invTotalLength;
        }
      }

      return progressiveLength;
    }

    /// <summary>
    /// Updates the colors, depth buffer, and alpha settings for rendering the mesh
    /// </summary>
    private void UpdateRenderState() {
      RenderState renderState = base.State.RenderState;
      if (this.alphaEnable) {
        renderState.Alpha.Enabled = true;
        renderState.Alpha.SourceBlend = Blend.SourceAlpha;
        renderState.Alpha.DestinationBlend = Blend.InvSourceAlpha;
      } else {
        renderState.Alpha.Enabled = false;
      }
      renderState.Lighting.Enabled = false;
      renderState.Cull.Enabled = false;
      renderState.Stages[0].Blending.AlphaArgument1 = TextureArgument.TFactor;
      renderState.Stages[0].Blending.AlphaOperation = TextureOperation.SelectArg1;
      renderState.Stages[0].Blending.ColorArgument1 = TextureArgument.TFactor;
      renderState.Stages[0].Blending.ColorOperation = TextureOperation.SelectArg1;
      renderState.Stages[1].Enabled = false;
      renderState.TextureFactor.Value = this.color;
      renderState.ZBuffer.Enabled = zBufferEnable;
      renderState.ZBuffer.ZBufferFunction = Compare.Less;
    }

    #endregion Private Methods

    // ************************************************************************
    // Properties
    // ************************************************************************
    #region Properties

    /// <summary>
    /// A list of points that make up the line to be drawn. Points are in
    /// Lat, Lon, Alt format.
    /// </summary>
    public List<Vector3F> Points {
      get {
        return points;
      }
    }

    /// <summary>
    /// Gets and Sets the starting width of the line in pixels
    /// </summary>
    public float LineStartWidth {
      get {
        return this.lineStartWidth;
      }
      set {
        if (value <= 0f) {
          throw new ArgumentOutOfRangeException("value");
        }
        this.lineStartWidth = value;
      }
    }

    /// <summary>
    /// Gets and Sets the end width of the line in pixels
    /// </summary>
    public float LineEndWidth {
      get {
        return this.lineEndWidth;
      }
      set {
        if (value <= 0f) {
          throw new ArgumentOutOfRangeException("value");
        }
        this.lineEndWidth = value;
      }
    }

    /// <summary>
    /// Gets and Sets the length of the dashes in pixels
    /// </summary>
    public float DashLength {
      get {
        return dashLength;
      }
      set {
        if (value < 0f) {
          throw new ArgumentOutOfRangeException("value");
        }
        dashLength = value;
      }
    }

    /// <summary>
    /// Gets and Sets the length of the gaps between the dashes in pixels
    /// </summary>
    public float GapLength {
      get {
        return gapLength;
      }
      set {
        if (value < 0f) {
          throw new ArgumentOutOfRangeException("value");
        }
        gapLength = value;
      }
    }

    /// <summary>
    /// Gets and Sets the offset of the dashes in pixels.
    /// </summary>
    /// <remarks>
    ///  Incrementing this by .5 every frame gives a moving ant effect.
    /// </remarks>
    public float DashOffset {
      get {
        return dashOffset;
      }
      set {
        if (value < 0f) {
          throw new ArgumentOutOfRangeException("value");
        }
        dashOffset = value;
      }
    }

    /// <summary>
    /// Gets and Sets if alpha blending is enabled for this line
    /// </summary>
    public bool AlphaEnable {
      get {
        return this.alphaEnable;
      }
      set {
        if (this.alphaEnable != value) {
          this.alphaEnable = value;
          this.UpdateRenderState();
        }
      }
    }

    /// <summary>
    /// Gets and Sets if the Depth Buffer is enabled for this mesh
    /// </summary>
    public bool ZBufferEnable {
      get {
        return this.zBufferEnable;
      }
      set {
        if (this.zBufferEnable != value) {
          this.zBufferEnable = value;
          this.UpdateRenderState();
        }
      }
    }

    /// <summary>
    /// Gets and Sets the color to draw this line in
    /// </summary>
    public System.Drawing.Color Color {
      get {
        return this.color;
      }
      set {
        if (this.color != value) {
          this.color = value;
          this.UpdateRenderState();
        }
      }
    }

    #endregion Properties
  }
}

Rendering an un-parented WPF Visual to a bitmap

The source code for this post can be found here ParentlessButton.zip

A co-worker had a problem today and asked for my help. He was using the Chart control from the WPF Toolkit and wanted to render it to a bitmap and save it to a file without actually adding the Chart to the visual tree. There are plenty of samples on how to render a WPF Visual to a bitmap, including This One that I wrote 4 years ago, but they don’t seem to render if the Visual isn’t a child Visual in a WPF visual tree. The reason for this failure to render is because the Visual isn’t told what size container it has to fill and so the ActualWidth and ActualHeight properties are zero.

So how to get around this? We need to perform the same setup that a panel does on the Visual. If we look at the MSDN Documentation For Custom Panels we will find this code:

public class PlotPanel : Panel
{
    // Default public constructor
    public PlotPanel()
        : base()
    {
    }

    // Override the default Measure method of Panel
    protected override Size MeasureOverride(Size availableSize)
    {
        Size panelDesiredSize = new Size();

        // In our example, we just have one child.
        // Report that our panel requires just the size of its only child.
        foreach (UIElement child in InternalChildren)
        {
            child.Measure(availableSize);
            panelDesiredSize = child.DesiredSize;
        }

        return panelDesiredSize ;
    }
    protected override Size ArrangeOverride(Size finalSize)
    {
        foreach (UIElement child in InternalChildren)
        {
            double x = 50;
            double y = 50;

            child.Arrange(new Rect(new Point(x, y), child.DesiredSize));
        }
        return finalSize; // Returns the final Arranged size
    }
}

Looking at the code above we can see that the methods Measure(Size size) and Arrange(Size size) are called on the child Visuals. Here is some sample code inside an event handler to show how it is done:

private void button1_Click(object sender, RoutedEventArgs e) {

  Button button = new Button();
  button.Content = "Parentless Button";
  double width = 200;
  double height = 200;
  button.Width = width;
  button.Height = height;

  // Cure for getting an ActualWidth and ActualHeight of 0
  button.Measure(new Size(Double.PositiveInfinity,
                         Double.PositiveInfinity));
  button.Arrange(new Rect(button.DesiredSize));

  RenderTargetBitmap rtb = new RenderTargetBitmap((int)width, (int)height, 96.0, 96.0, PixelFormats.Pbgra32);
  rtb.Render(button);
  this.image1.Source = rtb;
}

You will also find a link at the top to the source code for a sample application that demonstrates rendering an un-parented Button to a Bitmap, and then displaying that Bitmap. When you run the application and click the “Test” button it will look like this:

ParentlessButton

The “Parentless Button” isn’t a button, but just a Bitmap displayed in an image control.

Bing Maps 3D – Clipping shapes with an imposter Earth

The final source code for these Bing Maps 3D posts can be found here 3DViewerBingMaps.zip. You will also need to install Bing Maps 3D from Microsoft.

This is the 3rd part of my posts on Bing Maps 3D. The 3 parts are:

- Adding Arbitrary Shapes. Creating a Cube mesh and a Sphere mesh.
- Rendering shapes past the Far Clip. How to render shapes whose coordinates are outside the View Port.
- Clipping shapes with an imposter Earth.

In the previous Bing Maps 3D post we had overcome the problem of 3D objects being clipped by the far plane on the horizon, but now we had the problem that they weren’t being clipped by the earth as seen for the yellow cube in the picture below:

Untitled7

The solution to this is to create another sphere to imitate the earth, but smaller and closer to the camera. The sphere should be scaled down by the same factor that the cube is scaled down when it is brought closer to the camera. The sphere rendered for the Earth in Bing Maps 3D isn’t an exact sphere, it is a little bit squashed, so to make sure we get the same shape we measure the earth from within Bing Maps 3D and apply those measurements to our sphere. Here’s where we use the SetToReplaceEarth() method that I included in the Sphere Mesh code in the first post which looks like this:

/// <summary>
/// Scales and positions this sphere such that it replaces the earth
/// </summary>
public void SetToReplaceEarth() {
  Microsoft.MapPoint.Rendering3D.Cameras.GeodeticViewpoint view = new Microsoft.MapPoint.Rendering3D.Cameras.GeodeticViewpoint();
  Vector3D[] vecs = new Vector3D[6];
  view.Position.Location = LatLonAlt.CreateUsingDegrees(0, 0, 0);
  vecs[0] = view.Position.Vector;

  view.Position.Location = LatLonAlt.CreateUsingDegrees(0,180, 0);
  vecs[1] = view.Position.Vector;

  view.Position.Location = LatLonAlt.CreateUsingDegrees(-90, 0, 0);
  vecs[2] = view.Position.Vector;

  view.Position.Location = LatLonAlt.CreateUsingDegrees(90, 0, 0);
  vecs[3] = view.Position.Vector;

  view.Position.Location = LatLonAlt.CreateUsingDegrees(0, 90, 0);
  vecs[4] = view.Position.Vector;

  view.Position.Location = LatLonAlt.CreateUsingDegrees(0, -90, 0);
  vecs[5] = view.Position.Vector;

  Vector3D average = new Vector3D(0, 0, 0);
  Vector3D min = new Vector3D(double.MaxValue, double.MaxValue, double.MaxValue);
  Vector3D max = new Vector3D(double.MinValue, double.MinValue, double.MinValue);

  for (int i = 0; i < vecs.Length; ++i) {
    average.Add(vecs[i]);
    min = min.Min(vecs[i]);
    max = max.Max(vecs[i]);
  }
  average.MultiplyBy(1.0 / 6.0);

  this.Position = average;

  Vector3D diff = max.Subtract(min);
  diff.MultiplyBy(0.5 / radius);
  this.Scale = diff;

}

We put this sphere into the scene at 0,0,0, and to make it visible I will give it a very transparent yellow color. Here’s the result:

Untitled9

So it’s working now right? Well not quite, you see I’ve drawn the sphere above to have 20 stacks, 20 slices, so it’s not a very high resolution mesh, so when you get lower the clip is quite a bit under the surface. In the image below the yellow line should stop at the same place as the red cube does:

Untitled10

Easy fix right? Lets change the imitation Earth to a 200×200 mesh:

Untitled8

What happened? Well the old DirectX cards could only handle a 16 bit Index Buffer , which is what we set up our sphere mesh as when we declared it like this:

public class SphereMesh : MeshGraphicsObject<Vertex.PositionNormal, ushort>{
}

Fortunately for me my video card can handle a 32 bit index buffer, so I change the code throughout to have uint instead of ushort and now it works fine and the error in the clip at low altitude is improved:

Untitled11

This wraps up my 3 part series on adding 3D Meshes to Bing Maps 3D. Enjoy playing with the sample application attached at the top of all 3 articles.