Posts on February 10, 2013

India, week 1
Mood: relaxed
Posted on 2013-02-10 17:54:00
Tags: pictures travel
Words: 1080

<- click for full album

Monday 2/4 evening

First day of work here! I had breakfast at the hotel and the owner came over and chatted with me a bit, which was neat. Then it was time to drive to work during what I can only hope was rush hour, because...wow. The office is less than 2 miles away and it took 20 minutes to get there and it seemed like we nearly got in 5 accidents. At tricky intersections you basically have to almost cause an accident to get anywhere. I was talking about this with people at work some - it sounds like it's mostly because Bangalore has grown so much in the past 7-10 years (there was very little SW industry here even in the 1990's) and the infrastructure hasn't scaled with it. Apparently the traffic is less crazy other places in India.


The NI office is nice - it's less than two floors of one building, but there are neat dance-related things on the walls, and the conference rooms are named after famous Indians, including Ramanujan! The power blipped out a few times during the day, but all the computers are on a UPS.



I had some problems with my work computer, but I did manage to get some work done. Unfortunately I haven't totally beaten jet lag and got pretty tired in the afternoon...

Now I'm back in the hotel room and in addition to the usual honking there's some seriously thumping bass that's been going on for an hour or so. Not sure what that's about. (Ed: I think there was a banquet going on downstairs or something. It stopped before I went to bed...)

Wednesday 2/6 evening

Yesterday was fairly boring: get up, have breakfast, nearly get in accidents on the way to work, do work, nearly get in accidents on the way to the hotel, eat, relax, sleep. I did give a talk at work which I think went decently, although I realized afterwards I omitted a key point. Oh well!



Today was mostly the same. After work, though, I walked to a nearby park. I've been planning to do that for a few days now, but my plan had been to wait until the traffic died down so crossing the street would be less scary. Of course, by the time that happened I would be tired and not excited about leaving the hotel. So, today I went out right after getting back. The traffic was heavy, but the only street I had to cross has a traffic light that is mostly obeyed, and I crossed with a crowd of people.

It took less than 10 minutes to get there (although I walked around three-quarters of it looking for the entrance), and it's a nice little park! I walked around it a bit and sat on a bench and read (on my phone Kindle, the best thing ever for reading on the go) for a bit. It's well-lit and there were lots of people walking and sitting down.



Today's random India topic: ever since I got here, everyone in the service industry (hotel, car, restaurant) has been extremely friendly, sometimes to the point of making me uncomfortable. The first time the driver met me at work to drive me back to the hotel, he took my backpack and carried it for me. All of the drivers I've had open the door for me when I enter and exit the car. The security guard at the hotel opens the door for me. After bringing me a bottle of water, the waiter at the restaurant pours some into my glass (and refreshes it when it gets low).

I'm not sure what to make of this. Is this a cultural thing? Am I getting special treatment because I'm American? (or, at least, clearly not Indian) I'd rather open my own car door, but I don't want to give offense and I'm new here and when in Rome, etc., etc.

Friday 2/8 late evening

Let's see: work over the last two days has been good. I feel pretty settled in now, and I was actually able to fix a bug this morning! (although the lag on remote desktop is somewhat painful) I gave a presentation today that went pretty well, and I had something that wasn't Pizza Hut for lunch. (ordered from a local Indian place, although it's possible I'm starting to get tired of Indian food...) Yesterday was their version of Snack Thursday, at which everyone had some Taco Bell.

Thursday evening I went out walking a different direction (south!) and almost had to turn back - I was waiting to cross the street to come back, but there was 0 chance I was going to do that at anything but a bonafide stoplight. Luckily I eventually encountered one and crossed, and my reward was eating at a KFC on the way. It was...well, a lot like American KFC's. Except the drink I got was tiny (which was fine!), and the menu seemed to have some vegetarian options.



Tonight I went out with Rakesh and played some pool, then went out to a nice restaurant. To get to these places I rode on the back of Rakesh's motorcycle. I was a bit hesitant about this, but the places weren't far away and he gave me a smooth ride!



Random: there's a mosque near to work, so we hear the calls to prayer around lunchtime and early evening. The sound is quite beautiful - reminds me of music from Battlestar Galactica.

Random India topic: I happened to see the new version of the Big Mac Index...and look, India's at the very bottom! According to the index, things should be around 60% cheaper here, and that seems about right. Entrees at the hotel restaurant are around $4. I can get a Coke from the minibar in my room for $1. Tomorrow we're going to Mysore, and we're renting a taxi to drive us around all day for a total of $60. I guess the price of travel and difference in GDP can keep things this way, but it's still surprising.

Somewhat related: apparently it's not uncommon for bachelors to have a part-time cook service, where someone comes and delivers you home-cooked food every day. One person even has a full-time cook who lives at his apartment (although he does live with several other people), and the cook does laundry, cleaning, etc. I'm guessing this is related to the Big Mac Index...

0 comments

Windows Phone: writing a pinch and zoom image control
Mood: happy
Posted on 2013-02-10 22:20:00
Tags: windowsphone wpdev
Words: 989

When I was working on FlightPredictor and was working on showing airport maps, I was surprised there was no builtin "pinch and zoom image control" in the Windows Phone SDK. (to be fair, there wasn't one in Android either, and I'm not sure about iOS) So I had to implement my own, with some help from the Internet.

If I were doing this today, I'd just use the PanAndZoom control from Telerik's RadControls for Windows Phone. (which comes with the Nokia Premium Developer Program! Just sayin') But I did go through the trouble to implement it, so hopefully it will help someone out. Ed: another good solution is the SharpGIS ImageViewer - I haven't tried it, but it looks like it works well and you don't have to type in a bunch of code :-)

To see an example of how this works, you can download a trial version of FlightPredictor, download the airport maps and then play with them. This code supports pinching to zoom, panning, a maximum zoom level, and double-tap to zoom in or out.


XAML code
Here's the relevant part of the XAML:


<Image x:Name="MapImage" Stretch="Uniform"
RenderTransformOrigin="0,0" CacheMode="BitmapCache"
SizeChanged="MapImage_SizeChanged">
<toolkit:GestureService.GestureListener>
<toolkit:GestureListener
PinchStarted="GestureListener_PinchStarted"
PinchDelta="GestureListener_PinchDelta"
DragDelta="GestureListener_DragDelta"
DoubleTap="GestureListener_DoubleTap"/>
</toolkit:GestureService.GestureListener>
<Image.RenderTransform>
<CompositeTransform
ScaleX="1" ScaleY="1"
TranslateX="0" TranslateY="0"/>
</Image.RenderTransform>
</Image>


Note that the GestureListener is from the Windows Phone Toolkit, which is a (free!) must-have. It also requires you to have this inside the PhoneApplicationPage XML element:

xmlns:toolkit=
"clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Toolkit"


C# code
First, some variables to declare in your PhoneApplicationPage:

private bool _needToUpdateMaxZoom = false;
private int _imageHeight = 0;
private int _imageWidth = 0;
// Reference
// these two fields fully define the zoom state:
private double _totalImageScale = 1.0;
private Point _imagePosition = new Point(0, 0);

private double _maxImageZoom = 1;
private Point _oldFinger1;
private Point _oldFinger2;
private double _oldScaleFactor;


Now you need to get a BitmapImage containing the image to display. How you do this depends on where you're getting the image from, but here's how I do it for files stored in IsolatedStorage:

byte[] data;

// Read the entire image in one go into a byte array
using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())
{
// Open the file - error handling omitted for brevity
// Note: If the image does not exist in isolated storage
// the following exception will be generated:
// System.IO.IsolatedStorage.IsolatedStorageException was unhandled
// Message=Operation not permitted on IsolatedStorageFileStream
using (IsolatedStorageFileStream isfs = isf.OpenFile("/airportMaps/" +
info.Url, FileMode.Open, FileAccess.Read))
{
// Allocate an array large enough for the entire file
data = new byte[isfs.Length];

// Read the entire file and then close it
isfs.Read(data, 0, data.Length);
isfs.Close();
}
}

// Create memory stream and bitmap
MemoryStream ms = new MemoryStream(data);
BitmapImage bi = new BitmapImage();

// Set bitmap source to memory stream
bi.SetSource(ms);


After you've set up your BitmapImage, add the following code right afterwards:

_imageHeight = bi.PixelHeight;
_imageWidth = bi.PixelWidth;
_imagePosition = new Point(0, 0);
_totalImageScale = 1;

// set max zoom in
if (MapImage.ActualWidth == 0.0 || MapImage.ActualHeight == 0.0)
{
_needToUpdateMaxZoom = true;
}
else
{
UpdateMaxZoom();
UpdateImageScale(1.0);
UpdateImagePosition(new Point(0, 0));
}

// Assign the bitmap image to the image’s source
MapImage.Source = bi;


Now, all that's left is to implement the GestureListener events, as well as a few utility methods:

private void MapImage_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (_needToUpdateMaxZoom)
{
if (MapImage.ActualHeight != 0.0 && MapImage.ActualWidth != 0.0)
{
UpdateMaxZoom();
}
}
}

private void UpdateMaxZoom()
{
// this is already stretched, so this gets tricky
_maxImageZoom = Math.Min(_imageHeight / MapImage.ActualHeight,
_imageWidth / MapImage.ActualWidth);
_maxImageZoom *= Math.Max(1.0,
Math.Max(_imageHeight / MapImage.ActualHeight, _imageWidth / MapImage.ActualWidth));
const double MAX_ZOOM_FACTOR = 2;
_maxImageZoom *= MAX_ZOOM_FACTOR;
_maxImageZoom = Math.Max(1.0, _maxImageZoom);
_needToUpdateMaxZoom = false;
UpdateImageScale(1.0);
UpdateImagePosition(new Point(0, 0));
}

private void GestureListener_PinchStarted(object sender, PinchStartedGestureEventArgs e)
{
_oldFinger1 = e.GetPosition(MapImage, 0);
_oldFinger2 = e.GetPosition(MapImage, 1);
_oldScaleFactor = 1;
}

private void GestureListener_PinchDelta(object sender, PinchGestureEventArgs e)
{
var scaleFactor = e.DistanceRatio / _oldScaleFactor;
if (!IsScaleValid(scaleFactor))
return;

var currentFinger1 = e.GetPosition(MapImage, 0);
var currentFinger2 = e.GetPosition(MapImage, 1);

var translationDelta = GetTranslationDelta(currentFinger1, currentFinger2,
_oldFinger1, _oldFinger2, _imagePosition, scaleFactor);

_oldFinger1 = currentFinger1;
_oldFinger2 = currentFinger2;
_oldScaleFactor = e.DistanceRatio;

UpdateImageScale(scaleFactor);
UpdateImagePosition(translationDelta);
}

private void GestureListener_DragDelta(object sender, DragDeltaGestureEventArgs e)
{
var translationDelta = new Point(e.HorizontalChange, e.VerticalChange);

if (IsDragValid(1, translationDelta))
UpdateImagePosition(translationDelta);
}

private void GestureListener_DoubleTap(object sender, Microsoft.Phone.Controls.GestureEventArgs e)
{
if (Math.Abs(_totalImageScale - 1) < .0001)
{
const double DOUBLE_TAP_ZOOM_IN = 3;
double imageScale = Math.Min(DOUBLE_TAP_ZOOM_IN, _maxImageZoom);

Point imagePositionTapped = e.GetPosition(MapImage);
// we want this point to be centered.
double x = imagePositionTapped.X * imageScale - (MapImage.ActualWidth / 2);
double y = imagePositionTapped.Y * imageScale - (MapImage.ActualHeight / 2);
Point imageDelta = new Point(-1*x, -1*y);
// FFV - animation?
UpdateImageScale(imageScale);
UpdateImagePosition(imageDelta);
}
else
{
ResetImagePosition();
}
}

private Point GetTranslationDelta(Point currentFinger1, Point currentFinger2,
Point oldFinger1, Point oldFinger2, Point currentPosition, double scaleFactor)
{
var newPos1 = new Point(currentFinger1.X + (currentPosition.X - oldFinger1.X) * scaleFactor,
currentFinger1.Y + (currentPosition.Y - oldFinger1.Y) * scaleFactor);
var newPos2 = new Point(currentFinger2.X + (currentPosition.X - oldFinger2.X) * scaleFactor,
currentFinger2.Y + (currentPosition.Y - oldFinger2.Y) * scaleFactor);
var newPos = new Point((newPos1.X + newPos2.X) / 2, (newPos1.Y + newPos2.Y) / 2);
return new Point(newPos.X - currentPosition.X, newPos.Y - currentPosition.Y);
}

private void UpdateImageScale(double scaleFactor)
{
_totalImageScale *= scaleFactor;
ApplyScale();
}

private void ApplyScale()
{
((CompositeTransform)MapImage.RenderTransform).ScaleX = _totalImageScale;
((CompositeTransform)MapImage.RenderTransform).ScaleY = _totalImageScale;
}

private void UpdateImagePosition(Point delta)
{
var newPosition = new Point(_imagePosition.X + delta.X, _imagePosition.Y + delta.Y);
if (newPosition.X > 0) newPosition.X = 0;
if (newPosition.Y > 0) newPosition.Y = 0;

if ((MapImage.ActualWidth * _totalImageScale) + newPosition.X < MapImage.ActualWidth)
newPosition.X = MapImage.ActualWidth - (MapImage.ActualWidth * _totalImageScale);

if ((MapImage.ActualHeight * _totalImageScale) + newPosition.Y < MapImage.ActualHeight)
newPosition.Y = MapImage.ActualHeight - (MapImage.ActualHeight * _totalImageScale);

_imagePosition = newPosition;

ApplyPosition();
}

private void ApplyPosition()
{
((CompositeTransform)MapImage.RenderTransform).TranslateX = _imagePosition.X;
((CompositeTransform)MapImage.RenderTransform).TranslateY = _imagePosition.Y;
}

private void ResetImagePosition()
{
_totalImageScale = 1;
_imagePosition = new Point(0, 0);
ApplyScale();
ApplyPosition();
}

private bool IsDragValid(double scaleDelta, Point translateDelta)
{
if (_imagePosition.X + translateDelta.X > 0 || _imagePosition.Y + translateDelta.Y > 0)
return false;
if ((MapImage.ActualWidth * _totalImageScale * scaleDelta) +
(_imagePosition.X + translateDelta.X) < MapImage.ActualWidth)
return false;
if ((MapImage.ActualHeight * _totalImageScale * scaleDelta) +
(_imagePosition.Y + translateDelta.Y) < MapImage.ActualHeight)
return false;
return true;
}

private bool IsScaleValid(double scaleDelta)
{
return (_totalImageScale * scaleDelta >= 1) &&
(_totalImageScale * scaleDelta <= _maxImageZoom);
}

and that's it! Some things you can tweak:
- The maximum you can zoom in is 2x of the original image size. You can change this by modifying MAX_ZOOM_FACTOR in UpdateMaxZoom().
- When you double-tap on the image, if it's currently zoomed out it zooms in to 3x. You can change this by modifying DOUBLE_TAP_ZOOM_IN in GestureListener_DoubleTap.

I'm also not entirely sure the math is right in various places, but it works well enough on the size of images I tend to deal with. It would also be nice to add inertial scrolling...

Hope this is helpful! I took a lot of this code from this blog post.

--

See all my Windows Phone development posts.

I'm planning on writing more posts about Windows Phone development - what would you like to hear about? Reply here, on twitter at @gregstoll, or by email at ext-greg.stoll@nokia.com.

--

Interested in developing for Windows Phone? I'm the Nokia Developer Ambassador for Austin - drop me a line at ext-greg.stoll@nokia.com!

4 comments

This backup was done by LJBackup.