Interactive SVG on the iPad

August 30th, 2010 by Gert-Jan van der Wel

It’s difficult to find any decent samples about interactive SVG on the iPad and iPhone. SVG is used more and more these days, but it’s still rather unknown. The combination with touch and gestures on an iDevice is even rarer.

Since I’ve been playing around with it a bit, I’d like to share what I’ve learned. This post describes the steps involved to create a blue rectangle that can be dragged, rotated and scaled.

SVG

To get things started we need to create an SVG document. The shape, in this case a blue rectangle, will be added to the document. Create a file with the .svg extension and add the following lines:

<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
 
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
    <rect id="square" width="200" height="200" style="fill:#3399CC" x="20" y="20" />
</svg>

Touch

To make the rectangle interactive we need to listen to touch and gesture events. Gesture events are a special case of touch events. They are created to make it easier to handle multi touch events, like scaling and rotating.

For scripting in SVG you can use the script type “text/ecmascript” inside an SVG doc. Let’s start with dragging the rectangle by adding touch listeners to the rectangle.

<rect id="square" width="200" height="200" style="fill:#3399CC" x="20" y="20"
    ontouchstart="touchStart(event);" 
    ontouchmove="touchMove(event);" 
    ontouchend="touchEnd(event);" />

Then we add the script (after the rect tag) to handle the touch events and translate the rectangle.

<script type="text/ecmascript">
	var x;
	var y;
	var touchX;
	var touchY;
	var width;
	var height;
 
	function touchStart(event) {
		event.preventDefault();
 
		// one finger touch => start drag	
		if(event.touches.length == 1) {
			var touch = event.touches[0];
			var node = touch.target;
 
			x = parseInt(node.getAttributeNS(null, 'x'));
			y = parseInt(node.getAttributeNS(null, 'y'));
 
			touchX = touch.pageX;
			touchY = touch.pageY;
		}
	}
 
	function touchMove(event){ 
		event.preventDefault();
 
		// one finger touch => drag
		if(event.touches.length == 1) {
			var touch = event.touches[0];
			var node = touch.target;
 
			var dx = touch.pageX - touchX;
			var dy = touch.pageY - touchY;
 
			var newX = x + dx;
			var newY = y + dy;
 
			node.setAttributeNS(null, 'x', newX);
			node.setAttributeNS(null, 'y', newY);
		}
	}
 
	function touchEnd(event) {
	}			
</script>

The functions touchDown and touchMove both check if the touch is done by one finger. On touch down the start coordinates are recorded and on touch move the rectangle is moved to the new position. The function touchEnd is not used.

You can also see that the default browser action, panning the page, is prevented by calling event.preventDefault();

Gestures

Next up are handling the gestures to scale and rotate our rectangle. First add the listeners:

    ongesturestart="gestureStart(event);"
    ongesturechange="gestureChange(event);"
    ongestureend="gestureEnd(event);"

and then the functions:

function gestureStart(event) {
	event.preventDefault();
	var node = event.target;
 
	x = parseInt(node.getAttributeNS(null, 'x'));
	y = parseInt(node.getAttributeNS(null, 'y'));
 
	width = parseInt(node.getAttributeNS(null, 'width'));
	height = parseInt(node.getAttributeNS(null, 'height'));
 
	var transform = (node.getAttributeNS(null, 'transform'));
	rotation = parseInt(transform.split('rotate(')[1].split(' ')[0]);
 
	if(isNaN(rotation)) {
		rotation = 0;
	}
}
 
function gestureChange(event) {
	event.preventDefault();
	var node = event.target;
 
	// scale
	var newWidth = width * event.scale;
	var newHeight = height * event.scale;
 
	var newX = x - (newWidth - width)/2;
	var newY = y - (newHeight - height)/2; 
 
	node.setAttributeNS(null, 'width', newWidth);
	node.setAttributeNS(null, 'height', newHeight);
	node.setAttributeNS(null, 'x', newX);
	node.setAttributeNS(null, 'y', newY);
 
	// rotation
	var newRotation = rotation + event.rotation;
	var centerX = newX + newWidth/2;
	var centerY = newY + newHeight/2;
	setRotation(node, newRotation, centerX, centerY);
}
 
function gestureEnd(event) {
	rotation = rotation + event.rotation;
}
 
function setRotation(node, rotation, x, y) {
	var centerX = x + width/2;
	var centerY = y + height/2;
 
	node.setAttributeNS(null, 'transform', 'rotate('+ rotation +' '+ x +' '+ y +')');
}

In general, these functions behave the same way as the touch functions. The values are recorded when the gesture starts and updated when the gesture changes.

Sample

That’s it. You can find a running version on the Floorplanner Labs and the whole code sample below. I added a toggle handlingGesture make sure the handling of the touch and gesture events don’t interfere.

I also added a bit of a hack to make the drag work when the rectangle is rotated. The rotation is recorded and set to 0 before the movement and re-applied after the movement. You don’t need this hack when you use SVG transformations, but that’s another story.

<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
 
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
 
	<rect id="square" width="200" height="200" style="fill:#3399CC" x="20" y="20" transform="rotate(0 110 110)"
 
		ontouchstart="touchStart(event);" ontouchmove="touchMove(event);" ontouchend="touchEnd(event);"
		ongesturestart="gestureStart(event);" ongesturechange="gestureChange(event);" ongestureend="gestureEnd(event);" />
 
 
	<script type="text/ecmascript">
		var x;
		var y;
		var touchX;
		var touchY;
		var width;
		var height;
		var rotation = 0;
		var handlingGesture = false;
 
		function touchStart(event) {
			event.preventDefault();
 
			if(!handlingGesture) {
				// one finger touch => start drag	
				if(event.touches.length == 1) {
					var touch = event.touches[0];
					var node = touch.target;
 
					x = parseInt(node.getAttributeNS(null, 'x'));
					y = parseInt(node.getAttributeNS(null, 'y'));
 
					touchX = touch.pageX;
					touchY = touch.pageY;
				}	
			}
		}
 
		function touchMove(event){ 
			event.preventDefault();
 
			if(!handlingGesture) {
				// one finger touch => drag
				if(event.touches.length == 1) {
					var touch = event.touches[0];
					var node = touch.target;
 
					var dx = touch.pageX - touchX;
					var dy = touch.pageY - touchY;
 
					var newX = x + dx;
					var newY = y + dy;
 
					// set rotation to 0 before moving
					setRotation(node, 0, newX+width/2, newY+height/2);
 
					node.setAttributeNS(null, 'x', newX);
					node.setAttributeNS(null, 'y', newY);
 
					// restore rotation
					setRotation(node, rotation, newX+width/2, newY+height/2);
				}
			}
		}
 
		function touchEnd(event) {
			if(event.touches.length == 0) {
				handlingGesture = false;
			}
		}			
 
		function gestureStart(event) {
			event.preventDefault();
			handlingGesture = true;
			var node = event.target;
 
			x = parseInt(node.getAttributeNS(null, 'x'));
			y = parseInt(node.getAttributeNS(null, 'y'));
 
			width = parseInt(node.getAttributeNS(null, 'width'));
			height = parseInt(node.getAttributeNS(null, 'height'));
 
			var transform = (node.getAttributeNS(null, 'transform'));
			rotation = parseInt(transform.split('rotate(')[1].split(' ')[0]); // ouch
 
			if(isNaN(rotation)) {
				rotation = 0;
			}
		}
 
		function gestureChange(event) {
			event.preventDefault();
			var node = event.target;
 
			// scale
			var newWidth = width * event.scale;
			var newHeight = height * event.scale;
 
			var newX = x - (newWidth - width)/2;
			var newY = y - (newHeight - height)/2; 
 
			node.setAttributeNS(null, 'width', newWidth);
			node.setAttributeNS(null, 'height', newHeight);
			node.setAttributeNS(null, 'x', newX);
			node.setAttributeNS(null, 'y', newY);
 
			// rotation
			var newRotation = rotation + event.rotation;
			var centerX = newX + newWidth/2;
			var centerY = newY + newHeight/2;
			setRotation(node, newRotation, centerX, centerY);
		}
 
		function gestureEnd(event) {
			rotation = rotation + event.rotation;
		}
 
		function setRotation(node, rotation, x, y) {
			var centerX = x + width/2;
			var centerY = y + height/2;
 
			node.setAttributeNS(null, 'transform', 'rotate('+ rotation +' '+ x +' '+ y +')');
		}
	</script>
</svg>

Rails and the Google Apps Marketplace

July 27th, 2010 by VvanGemert

For a couple of weeks we wanted to get Floorplanner on the Google Apps Marketplace. After my struggle with it I’m happy to say we succeeded. Before we get started you probably already found out that Ruby GData client library is very limited and proper documentation and examples are pretty hard to find. I will try to explain how the mechanism works and how you can use it in your Google Apps application.

Necessary Gems/Plugins

1. Initial Setup

Get a Google Apps account, it doesn’t need to be a premier one. Create a marketplace enterprise account and setup a new application. Be sure to check “My product may be directly installed into Google Apps domains”. Fill in the manifest with the appropriate API’s permissions. An example manifest can be found below.

<?xml version="1.0" encoding="UTF-8" ?>
<ApplicationManifest xmlns="http://schemas.google.com/ApplicationManifest/2009">
 
 <Name>[ApplicationName]</Name>
 <Description>[Description]</Description>
 
 <!-- Administrators and users will be sent to this URL for application support -->
  <Support>
    <!-- URL for application setup as an optional redirect during the install -->
    <Link rel="setup" href="[ApplicationSetupUrl]?domain=${DOMAIN_NAME}" />
 
    <!-- URL for application configuration, accessed from the app settings page in the control panel -->
    <Link rel="manage" href="[ApplicationAdminUrl]?domain=${DOMAIN_NAME}" />
 
    <!-- URL explaining how customers get support. -->
    <Link rel="support" href="[ApplicationHelpUrl]" />
 
    <!-- URL that is displayed to admins during the deletion process, to specify policies such as data retention, how to claim accounts, etc. -->
    <Link rel="deletion-policy" href="[ApplicationPolicyUrl]" />
  </Support>
 
 <!-- Show this link in Google's universal navigation for all users -->
 <Extension id="navLink" type="link">
   <Name>[ApplicationName]</Name>
   <Url>[ApplicationLoginUrl]?domain=${DOMAIN_NAME}</Url>
   <!-- Used API's -->
   <Scope ref="contactFeed"/>
   <Scope ref="spreadsheetFeed"/>
   <Scope ref="doclistFeed"/>
 </Extension>
 
 <!-- Declare our OpenID realm so our app is white listed -->
 <Extension id="realm" type="openIdRealm">
   <Url>[ApplicationRealm]</Url>
 </Extension>
 
 <Scope id="doclistFeed">
   <Url>https://docs.google.com/feeds/</Url>
   <Reason>[Reason]</Reason>
 </Scope>
 
 <Scope id="contactFeed">
   <Url>https://www.google.com/m8/feeds/</Url>
   <Reason>[Reason]</Reason>
 </Scope>
 
</ApplicationManifest>

2. The setup page

After setting up your application and the manifest file we are going to add the application to your domain. Log into the Marketplace where you updated your manifest and click the blue “Add it now” button when you click on your application. Now the setup process begins and you have to follow the steps. On step 3 or 4 you will be redirected to your own domain to setup a Google Apps domain with your application. Below you can find a way to authenticate with Google’s OpenID server. This way you can get the e-mail and user information of the administrator of the Google Apps domain. After setting up the domain you can redirect back to Google and continue the setup process.

def login
 
  # The domain needs to be set. For example with params[:domain]
  authenticate_with_open_id("https://www.google.com/accounts/o8/site-xrds?hd=" + domain),
    { :required   => ["http://axschema.org/contact/email", :email], :return_to  => '/login/google'}) do |result, identity_url, registration|
 
      if result.successful?
        # Succesfully logged in
	email = get_email(registration)
      else
	# Failed to login
      end
  end
 
end
 
def get_email(registration)
 
  if !registration['email'].blank?
    registration['email']
  else
    ax_response = OpenID::AX::FetchResponse.from_success_response(
    request.env[Rack::OpenID::RESPONSE])
    ax_response.data["http://axschema.org/contact/email"].first
  end
 
end

3. Using the GDAta API’s – Retrieving contacts

After you installed the application, we can login by using the same method as described in step 2. After your logged we can try to use the GData API’s. For the GData API’s we need to use the OAuth authentication method instead of OpenID. All we need for this is the Marketplace Consumer Key, Consumer Secret and the email of the user we are trying retrieve information from. You can get these by visiting your application after you logged in with the Google Enterprise Marketplace. It’s on the listing page if you click on “View OAuth Consumer Key”. Below you can find how we get a contactlist from a user;

def get_contacts
 
  email = 'user@email.com'
   url = 'https://www.google.com/m8/feeds/contacts/default/full?xoauth_requestor_id=#{email}'
 
   client = GData::Client::Contacts.new({:version => '3.0'})
   client.headers["Authorization"] = get_authorization_header(url, :get)
 
   response = client.get(url)
   response.body
 
end
 
def get_authorization_header(url, meth)
 
   consumer_key = 'Your-consumer-key'
   consumer_secret =  'Your-consumer-secret'
 
   consumer = OAuth::Consumer.new(consumer_key, consumer_secret, {
      :site => "https://google.com",
      :request_token_path => "/accounts/OAuthGetRequestToken",
      :access_token_path => "/accounts/OAuthGetAccessToken",
      :authorize_path => "/accounts/OAuthAuthorizeToken",
      :http_method => meth
    })
 
    access_token = OAuth::AccessToken.new consumer
 
    case meth
    when :post
       request = Net::HTTP::Post.new(url)
    else
       request = Net::HTTP::Get.new(url)
    end
 
    helper = OAuth::Client::Helper.new request, { :request_uri => url,
       :consumer         => consumer,
       :token            => access_token,
       :scheme           => 'header',
       :signature_method => "HMAC-SHA1",
       :method => meth
    }
 
    helper.header
 
end

Retrieving the contact list seems pretty straight forward. Our main issue was we had to change the http method when we use POST or GET. Be sure to set all the http methods correctly on the OAuth::Consumer, Net:HTTP, OAuth::Client and especially the GData::Client. Before you start creating your own code it would be wise to test your response by using Google’s OAuth Playground, which is a wonderful tool to use.

4. Using the GDAta API’s – Submitting an CSV/Excel to Google Docs

Submitting a CSV file to Google is almost as easy as retrieving your contact list. We are going to use the same method for retrieving the authorization header. To post a file to Google Docs we also need to add two more header attributes, named “Content-Type” and “Slug”. In the body we’ll post the CSV file. The Content-Type is to specify what kind of file we’re going to upload. The Slug attribute will be the name of the file we want to show up in Google Docs.

def submit_csv_to_gdocs
 
   email = 'user@email.com'
   url = 'https://docs.google.com/feeds/default/private/full?xoauth_requestor_id=#{email}'
 
   csv = StringIO.new
   CSV::Writer.generate(csv, ',') do |line|
      line << ["Name", "Value"]
   end
   csv.rewind
 
   client.headers["Authorization"] = get_authorization_header(url, :post)
   client.headers["Content-Type"] = "text/csv"
   client.headers["Slug"] = "Your-Document-Name.csv"
 
   feed = client.post(url, csv.read)
 
end

That is all. You can find more information on this in the Google Apps API Docs. If you have any question you can post them below. If I made any mistakes (which I probably have) please post them below. Have fun working with Google Apps!

Lessons Learned: Changing data storage

June 2nd, 2010 by Gert-Jan van der Wel

We’ve been in this internet startup game for a couple of years now. Every now and then we find ourselves in a situation that feels awfully familiar. Especially when we just broke something in the same way we’ve broken it before. Yes indeed… learning it the hard way. I thought it would be nice to write down a thing or two we have learned while running the Floorplanner business. The subjects of these stories are completely random and, as you might expect, tech orientated.

What?

In this case I’d like to talk a bit about changing the way we store our data. I don’t know if it’s just us, but for one reason or another, we changed it multiple times in the recent past. From storing it in a database, to file storage, to somewhere in the cloud, to a mixed setup. I can come up with two reasons why you want to switch to another storage solution. New technologies (or services like cloud services) or the notion that your initial assumptions aren’t correct anymore (because the world has changed while you weren’t paying attention).

The thing that makes changing the way you store your data difficult is not the technical implementation itself, but rolling it out in a running system. The end user doesn’t care and shouldn’t notice anything. I think we came up with a simple way of releasing such an update, but first let me tell you why we changed it in the first place.

Why?

In the beginning we stored everything in a relational database, users, floor plans, themes etc. We were familiar with working with a database so it was the easiest way to get the job done. Later on we opened up our service by building an API, which uses XML as it’s default format. When our API traffic grew, we noticed that a lot of recourses where used for parsing XML. Incoming requests where parsed to store the data from the XML in the database, and outgoing requests from database to XML.

We didn’t actually use the data for anything else then storing it. To free up the resources used for parsing, we figured that it would be much more efficient to store (parts of) the XML as files on the server. Instead of parsing from and to XML, we’d only have to save and serve the XML from the files.

How?

The implementation was a bit tricky because we had to touch every API method, but the really difficult part was rolling it out. Instead of switching everything in one big bang, we kept both storage solutions running. Whenever data was requested it was migrated from database to an XML file and served from the new file (if the XML file was already present, it would be served directly). New data was directly stored in files.

This was supposed to be a temporary solution. We didn’t want to keep working with two storage solutions for the same data. However, after a while we noticed that the migration was taking too long. Instead of migrating ‘on demand’, we decided to write an script to migrate everything at once. That worked.

Lesson learned

After changing our data storage for several times, we saw a pattern emerge. We never used the on demand method, because it just takes too long. Instead we did the following:

  1. Copy the data from the old source to the new source
  2. Update the application so that it can work with both sources. If the new source isn’t available, it uses the old one.
  3. Sync the new data that was stored while copying the data and patching the app
  4. Update the application so that it only uses the new data source
  5. Sync again (just to be sure)
  6. Clean / remove the old source

I hope this post is useful to anyone struggling with the same problem. I bet we will have to look at it once or twice in the near future. :)

Blender 2.5 alpha 2 – Collada export bug

March 27th, 2010 by Tim

Blender 2.5 is a great application to create 3D stuff. Best of all: its free!

Even better: Blender 2.5 alpha 2 seems to have a much enhanced Collada importer/exporter. Blender versions before 2.5 used a python script for import and export. That script was okay, but failed on more complex scenes using rigs / animations. I believe Blender now uses Collada DOM or OpenCOLLADA.

But after trying the Collada export I found a bug in Blender 2.5 alpha 2 r27226 which I filed in Blender’s bugtracker here (you need an account there). Seems export of “translation” goes sour.

@collada asked me to describe the bug in public, so here is the bugreport I filed:


Blender 2.5 alpha 2 - 2.52.0 - r27226
Mac OSX 10.5.8

The .blend is a simple cube animation with 2 keys, translation and rotation only.

A typical exported translation channel looks like this (see attached collada):
<channel source="#Cube_location_Y-sampler" target="Cube/location"/>

According to the Collada 1.4.1 spec the @target atrribute here says:the locY-sampler targets *all* values of the element
with @sid="location".
In this case however we need to target only *one* value.
See "Address syntax" in Chapter 3 of the Collada 1.4.1 spec.

The targeted scene graph node looks like this (see attached collada)
<node id="Cube" type="NODE">
<translate sid="location">6.423927 -0.08766067028 1.473982</translate>
[...]

A channel targets a "sid" => see the @sid attribute on the node's translation element.

the @target property of the channel should be like:
Cube/location.X
Cube/location.Y
Cube/location.Z
or
Cube/location(0)
Cube/location(1)
Cube/location(2)

When only specifying Cube.location importers can't determine which value to target of the translation element (x, y or z).

I should note that target=”Cube/location” IS valid in theory (when the channel’s sampler specifies triplets of values). But in the case described the target is not valid afaik.

Here are the files: blender25_collada_bug

UPDATE : the bug has been fixed. Thank you Arystanbek Dyussenov! Guess you have to compile Blender 2.5 from source until the bug fix makes it into a release.

getDefinitionByName error: “Variable … is not defined”

March 14th, 2010 by Gert-Jan van der Wel

I wrote this post mainly as a reminder for myself, but it might be useful for anybody dealing with the same issue. In ActionScript3 you can get a class by its name like this:

import flash.utils.getDefinitionByName;
...
var myClass = getDefinitionByName("MyClass") as Class;

When you only use the name of the class, you will get an error: “Variable … is not defined”, because the Flash Player can’t find it at run time. There are several ways to make it work.

1. You can import the class and create a dummy variable. The dummy is needed, otherwise the compiler won’t add the class.

import domain.project.MyClass;
...
private var _dummyClass:MyClass;

2. Instead of a variable you can also use the following notation.

import domain.project.MyClass;
...
MyClass;

3. You can also use the Frame metadata tag:

import domain.project.MyClass;
...
[Frame(extraClass="MyClass")]

All these solutions work, but you need to specify the classes you want to use up front.

4. Far more easy, elegant and flexible is to include the path of the class when calling getDefinitionByName. You won’t need to import anything and you don’t have to create dummy vars or use extra metadata tags.

var myClass = getDefinitionByName("domain.project.MyClass") as Class;

Ant script to build Flex Library project

March 5th, 2010 by Gert-Jan van der Wel

Ever wondered how to create an Ant build script for a Flex Library project? I did, and although it’s very easy (when you know how) it’s very poorly documented. This is the solution Dusan and I found after some trail and error. Save this script as build.xml in the root of your project, run ant and your project will be build.

<?xml version="1.0" encoding="utf-8"?>
<project name="floorplanner_as3_core" basedir="." default="build">
  <property name="FLEX_HOME" value="your-flex-sdk-dir-here"/>
  <property name="src.dir" value="src"/>
  <property name="release.dir" value="bin"/>
 
  <taskdef resource="flexTasks.tasks" classpath="${FLEX_HOME}/ant/lib/flexTasks.jar"/> 
 
  <target name="compile">
    <compc output="bin/your-library-project.swc">
      <source-path path-element="${src.dir}" />
      <include-sources dir="${src.dir}" includes="*"/>	
    </compc>
  </target>
 
  <target name="clean">
    <delete>
      <fileset dir="${release.dir}" includes="your-library-project.swc"/>
    </delete>
  </target>
 
  <target name="build" depends="clean, compile" />
 
</project>

Flex Formatter

February 24th, 2010 by Gert-Jan van der Wel

I use Eclipse 3.4.2 in combination with Flex Builder 3.0. An important feature that is missing in FB3 is a formatter. I’m very glad I found this project on SourceForge by Ernest Pasour: Flex Formatter.

Installing it is easy. Download it, unzip it, move the files to the plugins folder of Eclipse and restart Eclipse. You now see a couple of new icons in your Flex perspective, with the most important being the “Format Flex code” icon. The plugin has a huge number of settings which can be adjusted in the general Properties menu of Eclipse by selecting Flex Formatter.

No more excuses for messy Flex / AS3 code!

RESTful uploading of files using XML

February 15th, 2010 by Willem van Bergen

After searching for too long to find any documentation on this topic, this is just as a reminder for myself. Hopefully, it’ll solve somebody else’s problem as well.

To send a file to a Rails application, using a RESTful XML API call, use the following XML snippet:

<field type="file" name="filename.ext" content-type="mime/type">
  base64-encoded file contents
</field>

The file contents should be base64 encoded, and may be encapsulated in a CDATA block:

<greeting type="file" name="greeting.txt" content-type="text/plain">
  <![CDATA[SGVsbG8gd29ybGQh]]>
</greeting>

You can test this by opening a console and feed the XML to Hash.from_xml:

>> greeting = <<-EOXML
    <greeting type="file" name="greeting.txt" content-type="text/plain">
      <![CDATA[SGVsbG8gd29ybGQh]]>
    </greeting>
  EOXML
 
>> data = Hash.from_xml(greeting)
=> {"greeting"=>#<StringIO:0x10617d9d0>}
>> data['greeting'].content_type
=> "text/plain"
>> data['greeting'].original_filename
=> "greeting.txt"
>> data['greeting'].read
=> "Hello world!"

Now you can handle files using your XML API just like you would use file uploads in your HTML forms.

Flex SDK 4.0.0.13875

February 4th, 2010 by Gert-Jan van der Wel

On Friday 29 January Adobe released a new ‘stable’ version of the Flex SDK, version 4.0.0.13875. One of the nice things about it was the addition of the Flex 4 TitleWindow. Peter deHaan uses the new TitleWindow in one of his articles on his excellent blogConstraining the movement on a Spark TitleWindow container in Flex 4

When using the new SDK we encountered quite an annoying issue. Since Flex 4 is still in beta, we use Flex Builder 3 in combination with a Flex 4 SDK. The problem was that after upgrading to the latest SDK, default flash and mx classes weren’t recognised anymore and couldn’t be imported. When added manually they gave an error.

Luckily Tim found a workaround. Go to your project’s Properties, click on Flex Build Path and  select Library path. First open up the Flex 4.0.0.13875, select the playerglobal.swc and remove it. Then click on Add SWC… and select the playerglobal.swc from the SDK (in my case /Developer/SDKs/flex_sdk_4.0.0.13875/frameworks/libs/player/10.0/playerglobal.swc).

I hope this little workaround is useful for everybody out there working with the latest Flex SDK.

PS. the workaround only seems to work for Flex/AS3 projects, but not for Library projects…

Televize your Twitter

January 31st, 2010 by Gert-Jan van der Wel

A former colleague of ours and brilliant coder, Jaap van der Meer, has started his own business: Ape55. Together with co-founder Ernest Mistiaen, the month old company already released their first product: Tweetz.tv.

Tweetz.tv shows tweeted videos. Three different channels are offered: My TV, Public TV and Search TV. My TV shows the videos that are posted by the people you follow, Public TV shows all videos posted in the public timeline and with Search TV you can look for tweeted videos on a specific topic.

We wish Jaap and Ernest all the best and we hope that Ape55, Tweetz.tv and all their future products become a huge success!