Saturday, 8 November 2014

Unit converter data structures

This post will use Java's Map API extensively and assumes you are familiar with them. Read the Java tutorial on Maps if you think I'm talking about a street map or if you want to refresh your memory.

In this post, some code showing how TLCalc's unit converter works. If you read the preceding post to this, you might have suspected I would be using a map interface where the keys are the unit's name and the entry is the scaling factor. The final product is a bit more generic than that and encapsulates the scaling factor in an object called UnitInfo. This will allow us to handle non-scalar units in one go. Due to the long length of the post, only the data structure utilised by the unit converter and how the app creates those data structures is covered in this post.

UnitInfo


So first, here is the interface for UnitInfo:
public interface UnitInfo {
    public double forwardConvert(double a);
    public double reverseConvert(double b);
    public String getDimension();
    public boolean allowSIPrefix();
    public boolean isScalar();
}
The methods forwardConvert and reverseConvert performs the conversion of the quantity from the UnitInfo's represented quantity to the chosen common quantity. getDimension is the dimension of the unit as a string and I will explain this further later. allowSIPrefix indicates whether the unit can be prefixed with an SI prefix like "kilo" and isScalar is whether the unit is scalar. The scalar property is used to display a warning when compounding as non-scalar units will result in a conversion function that depends on the order the units are converted. In fact, non-scalar units should not be compounded as the result is most likely rubbish.

You may have noticed I have not included the unit's name in the data. That is because multiple names can be mapped to the same unit (most arising from an abbreviated and non-abbreviated version) and the name does not affect the conversion in any way. It would also be wasteful specifying the name in the map and the unit info at the same time (although with increasing RAM of phones nowadays, this is becoming less significant).

The properties dimension and allowsSIPrefix are simply properties that can be stored in variables to an abstract version of UnitInfo was created:
    public static abstract class AbstractUnitInfo implements UnitInfo {

        private final String dimension;
        private final boolean allowSIPrefix;

        protected AbstractUnitInfo(String dimension, boolean allowSIUnitPrefix) {
            this.dimension = dimension;
            this.allowSIPrefix = allowSIUnitPrefix;
        }

        @Override
        public String getDimension() {
            return dimension;
        }

        @Override
        public boolean allowSIPrefix() {
            return allowSIPrefix;
        }

        @Override
        public boolean isScalar() {
            return false;
        }
    }
The implementation of a scalar unit follows:
    public static class ScalarUnitInfo extends AbstractUnitInfo {

        private final double m;

        public ScalarUnitInfo(String dim, double m, boolean allowSIUnitPrefix) {
            super(dim, allowSIUnitPrefix);
            this.m = m;
        }

        @Override
        public double forwardConvert(double a) {
            return a*m;
        }

        @Override
        public double reverseConvert(double b) {
            return b/m;
        }

        @Override
        public boolean isScalar() {
            return true;
        }
    }
Attached at the end is the source code for all types of UnitInfo including linear units (LinearUnitInfo) such as temperature and even non-linear units (ExprUnitInfo) such as decibels. Non-linear units are specified as expressions where the input is variable x. You can view the source file but they are pretty easy to grasp. The ExprUnitInfo uses the expression parser of TLCalc but you could use your own expression parser.

The most complicated of the implementation of UnitInfo is the CompoundUnitInfo which allows one to build a UnitInfo from other UnitInfo. The compound version simply chains units together by chaining calls to convert so you can think if each unit as a function such as kg(x), lb(x), metre(x), etc and to compound these units together you apply them like so: metre(lb(kg(x))). CompoundUnitInfo also accepts ratio conversions implemented as compound of numerator units divided by compound of denominator units. CompoundUnitInfo is really only useful for scalar units.
    public static class CompoundUnitInfo implements UnitInfo {

        private final ArrayList<UnitInfo> uis = new ArrayList<UnitInfo>(10);
        private String dimCache;
        private int splitPoint;

        public void add(UnitInfo ui, boolean isNum) {
            dimCache = null;
            if (isNum) {
                uis.add(splitPoint, ui);
                splitPoint++;
            } else {
                uis.add(ui);
            }

        }

        public int getCount() {
            return uis.size();
        }

        public void clear() {
            dimCache = null;
            uis.clear();
            splitPoint = 0;
        }

        public boolean isEmpty() {
            return uis.isEmpty();
        }

        public void swap(CompoundUnitInfo cui) {
            final ArrayList<UnitInfo> tmp = new ArrayList<UnitInfo>(uis);
            String tmpDimCache = dimCache;
            int sp = splitPoint;
            uis.clear();
            uis.addAll(cui.uis);
            dimCache = cui.dimCache;
            splitPoint = cui.splitPoint;
            cui.uis.clear();
            cui.uis.addAll(tmp);
            cui.dimCache = tmpDimCache;
            cui.splitPoint = sp;
        }

        @Override
        public double forwardConvert(double a) {
            for (int i=0; i<splitPoint; i++) {
                a = uis.get(i).forwardConvert(a);
            }
            double b = 1;
            for (int i=splitPoint; i<uis.size(); i++) {
                b = uis.get(i).forwardConvert(b);
            }
            return a/b;
        }

        @Override
        public double reverseConvert(double b) {
            for (int i=0; i<splitPoint; i++) {
                b = uis.get(i).reverseConvert(b);
            }
            double a = 1;
            for (int i=splitPoint; i<uis.size(); i++) {
                a = uis.get(i).reverseConvert(a);
            }
            return b/a;
        }

        @Override
        public String getDimension() {
            if (dimCache == null) {
                int[] dims = new int[5];
                for (int i=0; i<uis.size(); i++) {
                    Utils.addDim(dims, uis.get(i).getDimension(), 
                            i < splitPoint ? 1 : -1);
                }
                dimCache = Utils.dimToString(dims);
            }
            return dimCache;
        }

        @Override
        public boolean allowSIPrefix() {
            return false;
        }

        @Override
        public boolean isScalar() {
            for (int i=0; i<uis.size(); i++) {
                if (!uis.get(i).isScalar()) return false;
            }
            return true;
        }
        
    }

Parsing CSV data to UnitInfo Map


The data for unit is stored in a map to allow fast and efficient lookup of unit names. Only the base units are stored in a map and prefixes are handled by building up upon the base unit. The same is done for powers. Two maps are used, one stores the base units and one stores the SI prefix.
    public static Map<String, UnitInfo> unitsMap, siMap;

The SI map is hard coded:
    private static void loadSIMap() {
        siMap = new ArrayMap<String, UnitInfo>(28 * 2);
        dualPut("yotta", "Y", 1E+24, siMap);
        dualPut("zetta", "Z", 1E+21, siMap);
        dualPut("exa", "E", 1E+18, siMap);
        dualPut("peta", "P", 1E+15, siMap);
        dualPut("tera", "T", 1E+12, siMap);
        dualPut("giga", "G", 1E+09, siMap);
        dualPut("mega", "M", 1E+06, siMap);
        dualPut("kilo", "k", 1E+03, siMap);
        dualPut("hecto", "h", 1E+02, siMap);
        dualPut("deca", "da", 1E+01, siMap);
        dualPut("deci", "d", 1E-01, siMap);
        dualPut("centi", "c", 1E-02, siMap);
        dualPut("milli", "m", 1E-03, siMap);
        dualPut("micro", "u", 1E-06, siMap);
        dualPut("nano", "n", 1E-09, siMap);
        dualPut("pico", "p", 1E-12, siMap);
        dualPut("fempto", "f", 1E-15, siMap);
        dualPut("atto", "a", 1E-18, siMap);
        dualPut("zepto", "z", 1E-21, siMap);
        dualPut("yocto", "y", 1E-24, siMap);

        dualPut("yobi", "Yi", Math.pow(2, 80), siMap);
        dualPut("zebi", "Zi", Math.pow(2, 70), siMap);
        dualPut("exbi", "Ei", Math.pow(2, 60), siMap);
        dualPut("pebi", "Pi", Math.pow(2, 50), siMap);
        dualPut("tebi", "Ti", Math.pow(2, 40), siMap);
        dualPut("gibi", "Gi", Math.pow(2, 30), siMap);
        dualPut("mebi", "Mi", Math.pow(2, 20), siMap);
        dualPut("kibi", "ki", Math.pow(2, 10), siMap);
    }

    /**
     * Helper method to add same unit under two names to the unit map.
     * This method is designed for ScalarUnitInfo only.
     * @param s1 name 1
     * @param s2 name 2
     * @param val value of ScalarUnitInfo
     * @param map map to add unit to
     */
    private static void dualPut(String s1, String s2, double val, 
            Map<String, UnitInfo> map) {
        UnitInfo ui = new UnitInfo.ScalarUnitInfo("", val, false);
        map.put(s1, ui);
        map.put(s2, ui);
    }
To parse the CSV file described from the previous post, we parse the text file line by line and break up the line into tokens separated by commas. This was developed in Android so the file was retrieved using getAssets. The implementation of UnitInfo used is determine by whether the unit is scalar, linear or none of the above.
    /**
     * Loads units data from CSV file located in the assets folder. Will not
     * throw an error although if an error occurs from reading the file, the
     * routine will stop loading any more data and return what is has
     * successfully parsed.
     * @param c context to retrieve CSV asset.
     */
    private void loadUnits(Context c) {
        unitsMap = new ArrayMap<String, UnitInfo>(50);

        // Load CSV data file from Assets
        InputStream is = null;
        try {
            is = c.getResources().getAssets().open("unit_conversion_dat.csv");
        } catch (IOException e) {
            Log.e(UnitConverterActivity.class.getName(),
                    "Error opening unit conversion data file", e);
        }
        if (is == null) return;
        BufferedReader reader = new BufferedReader(new InputStreamReader(is));

        String line;
        String[] st, names;
        try {
            reader.readLine(); // Discard header line
            while ((line = reader.readLine()) != null) {
                st = line.split(",");
                // Name  | type  |  linear  |  conversion
                //                             Forward | Backward
                if (st.length < 6) {
                    Log.e(UnitConverterActivity.class.getName(),
                            "Skipped invalid line: " + line);
                    continue;
                }
                if (st[0].isEmpty()) continue; // Invalid name
                UnitInfo tmp;
                final boolean isScalar = st[5].charAt(0) == '1';
                if ("1".equals(st[2])) {
                    // Linear equation
                    if (st[4].isEmpty()) {
                        tmp = new UnitInfo.ScalarUnitInfo(st[1], 
                                Double.parseDouble(st[3]),
                                isScalar);
                    } else {
                        tmp = new UnitInfo.LinearUnitInfo(st[1], 
                                Double.parseDouble(st[3]),
                                Double.parseDouble(st[4]), isScalar);
                    }
                } else {
                    // Non-linear equation
                    tmp = new UnitInfo.ExprUnitInfo(st[1], st[3], st[4], isScalar);
                }

                // Split name up
                names = st[0].split(";");
                for (String name : names) {
                    unitsMap.put(name, tmp);
                }
            }
        } catch (Exception e) {
            Log.e(UnitConverterActivity.class.getName(),
                    "Error loading unit conversion data file");
        }
    }
That's it for now, the next post will cover parsing unit description strings using the data structures we have created above.

Resources: UnitInfo.java

The workings of TLCalc's unit converter

I'm sure what I present here is fairly obvious to most people. But then again, when I look through all the apps on Android and iOS, I don't see many apps offering the capability of TLCalc's unit converter. To keep this post short, we will only cover scalar units and its concept (non-scalar units and code will be left for another day).

Generic unit conversion


Its pretty simple to do conversion of units you are expecting such as metres to inches. You simply need to know the factor to multiply with and the inverse operation is as simple as dividing. But when you want to cover more complicated units such as energy, force and speed, you will find that doing things this way results in large permutations of units that you should cover. Take speed as an example, you can have multiple distance units over a common time unit such as m/s, in/s, cm/s, mm/s, ft/s, and etc. But you can also measure speed as a ratio over other time units such as hours, minutes, days and so on (I admit some of these are infrequently used but you never know). If you were to define the scaling constant between each unit like this, your conversion table would grow exponentially.

Flexible unit conversion


The solution is simple, convert each unit one by one. E.g. convert mi/hr to m/s
The conversion factor for mi to m is 1,609 and from hr to s is 3600 thus: mi/hr = 1,609/3600 m/s = 0.447 m/s (the precision used here is low but you get the point). Intuitively, this is how you would go about converting units anyway. 

To improve flexibility even more, converting should be done to a common unit first then to the desired unit. E.g. convert length to metres, time to seconds, mass to kilograms and so on. This will reduce the size of the conversion table even further and make updating your data much easier. The common unit chosen is not critical, as long as it is consistent but choosing SI units has a bonus of reducing errors as most conversion will be to or from SI units thus eliminating errors from multiplication of two possibly imprecise factors to one multiplication. 

As such, the table of factor I use in my app is very small. The following is an extract from the full data set. The name contains the unit name as well as its abbreviation (basically all the way you might name the unit when typing) separated by semicolons. The "type" and "allow prefix" columns will be covered later while "linear" indicates whether the unit is a linear conversion (this post only covers scalar units so this will be covered in a later post). m/forward contains the factor to convert from that unit to common unit. c/backward is for converting from common unit to that unit. It is left blank here as "c/backward" = 1/"m/forward" so it can be calculated at runtime to reduce file size. The "m/backward" column exists only for non-scalar units which is not covered in this post. The data is saved as a CSV file to allow easy importing into the app. 
 
 
 

Dimensionality analysis


Without placing constraints, it would be possible to convert metres to gram using the table above although the result would be nonsense. So to impose constraints, we only allow conversions if the units are equivalent in quantity, e.g. length to length, speed to speed but not length to time. The check that is used is something called dimensional analysis (something that I learnt in physics in year 12!). The analysis is to figure out how to break units down into the most basic units and compare the basic units. E.g. speed is a ratio of length over time so any units involving a length divided by time is equivalent. There are in total 4 quantities that cannot be decomposed further which are length (L), time (t), mass (M) and temperature (T). The rest are composed of combinations of these quantities. In the data table above, the type column defines the unit's dimensions.
 
There is another category known as dimensionless quantities. These have no impact on the dimensions hence the name. Example of dimensionless units include radians and degrees. My implementation also includes bit, bytes, percentage and decibels as dimensionless as they can apply to any quantity (but I'm not quite sure if this is standard).
 
Note that in the system of SI units, there are in fact 7 fundamental quantities with current (amp), the amount of substance (mol) and luminous intensity (cd) being the remaining 3. The calculator at this stage does not include these units so they are ignored.

SI Prefix


The last section I will cover in this post are SI units. SI units are basically scalar units. E.g. kilo = 1000, milli = 0.001, etc. For programming, they were treated as such although parsing was a bit more strict than just allowing SI prefix to float anywhere. SI prefix can only be attached to the front of valid units and only one SI prefix is allowed per base unit so kilomilligram is not allowed but millimetres / kilogram is allowed. I was lax with the implementation of the unit converter so you can add SI prefix to non-SI units such as pounds although this is highly uncommon or never done. The data in the table above allows you to specify which units can have SI prefix although the calculator doesn't enforce this. The data is only used in the autocomplete functionality to make the results more relevant. E.g. TLCalc will never suggest klb (kilopound) although it will suggest kg (kilogram).
 
You can download the full data file here although the version linked will very likely be updated frequently. In the next post, I will post some code on how all this was implemented in TLCalc.

Monday, 3 November 2014

Syntax formatting for code in Blogger

There are quite a few sites out there that illustrate how to add syntax highlighting to code you post on Blogger. It however took me a while to find one that worked. The one that eventually worked for me was: http://www.stramaxon.com/2012/07/add-syntax-highlighter-to-blogger.html. The syntax highlighter used is called SyntaxHighlighter.

Adding syntax highlighting to your blog template


To do this, you simply go to templates and then edit the template's HTML file.




Find the </head> tag which should be near the top (use find if you can't find it) and paste the following code just before the </head> tag:

<!-- SyntaxHighlighter starts -->
<link href='http://alexgorbatchev.com/pub/sh/current/styles/shCore.css' rel='stylesheet' type='text/css'/>
<link href='http://alexgorbatchev.com/pub/sh/current/styles/shCoreMDUltra.css' rel='stylesheet' type='text/css'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shLegacy.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shCore.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushAS3.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushBash.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushCSharp.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushColdFusion.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushCpp.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushCss.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushDiff.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushErlang.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushGroovy.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushJScript.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushJava.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushJavaFX.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushPerl.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushPhp.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushPlain.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushPowerShell.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushPython.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushRuby.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushScala.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushSql.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushVb.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushXml.js' type='text/javascript'/>
<script language='javascript'>
SyntaxHighlighter.config.bloggerMode = true;
SyntaxHighlighter.defaults[&#39;auto-links&#39;] = false;
SyntaxHighlighter.config.clipboardSwf = &#39;http://alexgorbatchev.com/pub/sh/current/scripts/clipboard.swf&#39;;
SyntaxHighlighter.all();
</script>
<!-- SyntaxHighlighter ends -->

You should delete lines that contain syntax highlighting you don't need which for me meant I only kept: shBrushJava, shBrushXml, shBrushCss and shBrushPlain. You can also change the formatting style by replacing the highlighted portion of .../styles/shCoreMDUltra.css from line 3 with one of the following: shThemeRDark, shThemeMidnight, shThemeMDUltra, shThemeFadeToGrey, shThemeEmacs, shThemeEclipse, shThemeDjango, shThemeDefault, shCoreRDark, shCoreMidnight, shCoreMDUltra, shCoreFadeToGrey, shCoreEmacs, shCoreEclipse, shCoreDjango, shCoreDefault.

Using syntax highlighting


To use the syntax highlighting, you first need to escape special characters in your source code. There are plenty of on-line converters out there, this one is an example that I used. You then paste the code in between <pre class="brush:xml">...</pre> tags (where xml can be replaced with a different language style). You need to do this in HTML editing although you can switch back to composing mode when done pasting.

Note that you will need to publish the post before you can see the results (preview mode doesn't apply the formatting either unfortunately).

Sunday, 2 November 2014

Calculator widget tutorial - Part 1

I have decided to implement a widget for the unit converter and what former ideas I had for the widget were thrown out the window due to some catches with widgets. The process of developing GUI for widgets is very different from that used normally in activities. Android widgets documentation tries its best to explain how to implement the fairly convoluted code for a widget but I got somewhat lost in all the information so here's a tutorial that explains how to receive click events from a widget. Luckily, Android Studio generates a template that provides a useful baseline. The tutorial is based on Android Studio 0.8.14 (Beta). This tutorial will display 4 pseudo text fields (you will see what I mean later on) and they change their content randomly when clicked on.


First off, create a new App Widget using Ctrl+N in an existing project and fill in the wizard which is straight forward.



This generates a couple of files and modifies a couple of files including the Android Manifest and strings.xml. First we will start with the Android manifest:
        <receiver android:name="com.thompson.example.CalculatorWidget" >
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            </intent-filter>

            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/calculator_widget_info" />
        </receiver>
The addition to the manifest lets the Android OS knows the app can generate a widget and provides information about the widget. The name on line 1 is the class implementing AppWidgetProvider which interfaces with the widget to receive events and dispatch drawing updates. Line 3 lists the action the widget can receive through Intents. Line 7 can be left alone and line 8 is a resource file to display to the user when the user is browsing through widgets to add. The default is worthless so should be changed to one of your own image resources or removed entirely (in this case, the app icon is used which is pretty unimaginative). I used a screenshot of the layout file for mine:


A couple of files are also produced which are: calculator_widget_info.xml, RelativeLayout.xml, CalculatorWidget.java and calculator_widget_info.png as a "nodpi" image resource. The first contains information you specified in the wizard outlining how big the widget should be and so on. You can customise the information in this file if you need to. The layout file and widget provider class we will go into later. Finally, the preview image is junk so just delete it (make sure you specify your own image in the manifest as mentioned above or delete the android:resource attribute).

Note that in my case, the wizard also inserted the following permissions into the Android manifest file. I deleted this code as I didn't need the permissions and they were also causing errors:
    <android:uses-permission
        android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="18" />
    <android:uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <android:uses-permission
        android:name="android.permission.READ_EXTERNAL_STORAGE"
        android:maxSdkVersion="18" />
Next we go into the layout. Widgets operate differently from activities by using remote views and this limits what you can do enormously. The only UI components allowed are:
And you can't use descendants of these component either. The most glaring omission is EditText which was why my plans were thrown out the window.  On top of this. only certain layouts are allowed (see Android docs for more info). Anyway, the following layout code should be copied and pasted into RelativeLayout.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:dividerPadding="2dp" >


    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textIsSelectable="true"
            android:singleLine="true"
            android:ems="8"
            android:id="@+id/ucQtyAInput"
            android:hint="Quantity A"
            android:nextFocusForward="@+id/ucUnitAInput"
            style="@android:style/Widget.EditText"
            android:layout_weight="1"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:singleLine="true"
            android:id="@id/ucUnitAInput"
            android:nextFocusForward="@+id/ucQtyBInput"
            android:hint="Unit A"
            style="@android:style/Widget.EditText"
            android:layout_weight="1"/>

    </LinearLayout>

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textIsSelectable="true"
            android:singleLine="true"
            android:ems="8"
            android:id="@id/ucQtyBInput"
            android:nextFocusForward="@+id/ucUnitBInput"
            android:hint="Quantity B"
            style="@android:style/Widget.EditText"
            android:layout_weight="1"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:singleLine="true"
            android:nextFocusForward="@id/ucQtyAInput"
            android:id="@id/ucUnitBInput"
            android:hint="Unit B"
            style="@android:style/Widget.EditText"
            android:layout_weight="1"/>

    </LinearLayout>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Status label"
        android:layout_margin="2dp"
        android:paddingLeft="4dp"
        android:paddingRight="4dp"
        android:textColor="@color/errorText"
        android:id="@+id/ucStatusLabel"  />

</LinearLayout>
The layout file is like any normal layout file except for the restrictions mentioned earlier. Two things to note, I have styled the TextView to look like EditText (to create pseudo text fields but this is optional) and I have not used String resources to make it easier to copy and paste (although you should extract those strings to strings.xml to allow for possibility of internationalisation). 

Finally, we will edit the widget provider class. First, add the following constants:
    private static String
            ucUpdateAction = "ucUpdateAction",
            extraKey = "com.thompson.example.CalculatorWidget.fieldPressed";
And then add code to the updateAppWidget method as well as a helper method:
    static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
                                int appWidgetId) {
        // Construct the RemoteViews object
        RemoteViews views = new RemoteViews(context.getPackageName(), 
                R.layout.unit_converter_widget);
        addClickIntent(context, views, R.id.ucQtyAInput);
        addClickIntent(context, views, R.id.ucQtyBInput);
        addClickIntent(context, views, R.id.ucUnitAInput);
        addClickIntent(context, views, R.id.ucUnitBInput);

        // Instruct the widget manager to update the widget
        appWidgetManager.updateAppWidget(appWidgetId, views);
    }

    private static void addClickIntent(Context c, RemoteViews rv, int compID) {
        Intent intent = new Intent(c, CalculatorWidget.class);
        intent.setAction(ucUpdateAction);
        intent.putExtra(extraKey, compID);
        PendingIntent pi = PendingIntent.getBroadcast(c, compID, intent,
                PendingIntent.FLAG_UPDATE_CURRENT);
        rv.setOnClickPendingIntent(compID, pi);
    }
The helper method initialises the components with click listeners but this is done differently in widgets. The click listener instead triggers an intent which is listened to by the widget provider. In this example, we pass around the ID of the component that was clicked so we know which one to update (line 17). The PendingIntent wraps around the intent and has a couple of properties when set in line 18. You provide the context followed by a sender's code, the intent to wrap around and then some flags. The sender's code is used to allow you to retrieve the intent later on or to cancel it. If you require different intents for multiple component (like this example), make sure the sender's code is unique or they will replace each other. I have used the component's ID number as they are guaranteed to be unique. The PendingIntent.FLAG_UPDATE_CURRENT flag indicates that the most up to date intent data should be passed on to the receiver in the Intent's bundle. This sets the widget to send intents when the text fields are clicked on. 

We finally receive the events by overriding onReceive
    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals(ucUpdateAction)) {
            int compID = intent.getIntExtra(extraKey, -1);
            if (compID == -1) return;
            RemoteViews rv = new RemoteViews(context.getPackageName(),
                    R.layout.calculator_widget);
            int r = (int) (Math.random() * 100);
            rv.setTextViewText(compID, "Pressed... " + r);
            ComponentName cn = new ComponentName(context, CalculatorWidget.class);
            AppWidgetManager.getInstance(context).updateAppWidget(cn, rv);
        } else {
            super.onReceive(context, intent);
        }
    }
The method simply filters out events that we are interested in and updates the text field with randomish text when clicked. Note lines 10 and 11 is how you should update the component for redraw. For each of the components you are allowed to use in widgets, there are convenience methods in AppWidgetManager to update those components accordingly. That's it, you should be able to run the code now. You should see the text field update with random numbers as you tap them. In the next part, we will receive events from buttons and update the text fields based on what buttons were pressed.



The full CalculatorWidget.java source file can be downloaded here.

Update:
A very hard to notice bug in the code above occurs when the widget is reloaded from memory. The RemoteViews loads the configuration last written to it so if you update the widget but do not update everything else, the widget will be loaded incorrectly. In this case, you need to reapply the Pending click events to all 4 fields for each update. The better approach is to create a data object and save all information you need to the data object and then save the data object to persistent memory (e.g. using shared preferences). You then update the entire widget using the data object ensuring nothing is missed when the widget is updated. I.e. the widget loads the last RemoteViews you supplied so if you do not specify everything in the RemoteViews, then when the widget is reloaded, only the functionality you made to the last provided RemoteViews gets applied. 

Monday, 27 October 2014

How to use TLCalc's unit converter

If you have used some of the command line based unit converters, you should find this implementation of unit converters very intuitive to use. The difference is that the command line is split into 4 boxes.

Unit converter is accessed from the the menu on the main calculator screen

User interface of unit converter

Fields for quantity and units

The left column is the quantity and the right column is the unit. The unit represent the scaling to use for the number on that row. In the example, to convert 5 in/min^3 to m/s^3 results in 588E-9 m/s^3 or 588 nm/s^3. The converter is very flexible and you can use any of the built in units like m, kg, lb and so on or you can make up your own like shown in the example.

Built-In Units

The basic units include second (s), metre (m), mile (mi), pound (lb) and so on and are available automatically in the calculator. Basically, the built-in units tries to cover as many of the units that are used to quantify the four natural quantities as much as possible which are time, length, mass and temperature. Other units composed of natural quantities (referred to as compound units in this post) that are frequently used, such as joule and pascal, are also included built into the calculator. You are also free to choose whether to use the abbreviation or full name for most units. Note that all units are case sensitive so joUle is considered an unknown unit. Do not use plurals, none of the units are specified with plurals.

Below the input fields is the auto-complete functionality. This area updates with units you can insert into the unit field as you type. Just tap a box to add the unit into the field. You can also start typing the beginning of the unit name to narrow the list down.

SI Prefixes

You can append prefixes to any of the built-in units (even on imperial units which is probably wrong). Prefixes are definitely case sensitive since mg (milligram) is not the same as Mg (megagram). Note that micro is specified as u and not μ for compatibility with keyboard. The full 20 SI prefixes are supported and can be typed in as the shortened form (kg) or the long form (kilogram). In addition, the binary prefixes are also accepted and are intended for use with bits (b) and bytes (B), e.g. MiB.

Powers

Use ^ to apply a power to the unit. You would normally use this for squared or cubed quantities such as cubic metres (m^3) although any power that is integer is allowed. 

Compound Units and Rates

When making up units, you use the built in units and build them up with spaces in between. E.g. kg L, miles lb, g ft s. You are also able to use rates which are specified using the forward slash '/'. E.g. kg/m. Rates must be specified by one / only. Any units after the / are implied to be on the bottom of the fraction. This is different to how the calculator normally interpret expressions where A/BC is interpreted as (A/B)C in the main calculator instead of A/(BC) as in the case of the unit converter. Do not use brackets to change this, the calculator will not accept it. Note that each sub-unit that make up the compound unit must be valid standalone units so you can give each sub-unit prefixes and powers. It is also possible to use ^-1 instead of /, e.g. m s^-1

Also note that using non-scalar units such as temperature in compound units results in a conversion that is affected by the order you specify the units. It is advised you do not use non-scalar units in conversions and a warning will be displayed if the conversion of compound non-scalar units is detected. 

Scaling Factors and Reciprocals

You can scale a unit by a positive whole number. Like other units, ensure you separate the scaling factor from other units using spaces. This feature can be useful if you would like to compare rates of uneven quantities such as g/50. An example of where a scaling factor is used is fuel mileage:


The unit converter will also automatically reciprocate the conversion process if the quantities are reciprocals of each other. In the fuel mileage example above, the top unit is a distance divided by volume whereas the bottom unit is a volume divided by distance.

Equivalence

The units you convert between must be equivalent. E.g. you cannot convert metres into seconds. This also applies to compound units so you cannot convert kg/s to N/s although Pa m^2/s to kN/s is valid. The way the calculator checks this is to decompose the unit into powers of natural quantities which will be a mix of length (L), mass (M), time (t) and temperature (T). The power of each quantity must match up (remember dimensional analysis in physics?). Some units don't have dimensions such as %, rad and deg and these don't have any effect on the equivalence of a unit with another unit. 

When the units don't match up, you will get a message indicating what unit A has too much of compared to unit B. E.g. converting m^3 to km gives dimensional mismatch L2 meaning unit A has the power of length that is greater than unit B by 2.

Summary

In summary:
  • Quantities go in the left column, units on the right column
  • All units are case sensitive and do not use plurals
  • You can use SI prefix on any unit but note that micro is typed as 'u' and not μ
  • Use spaces between each compound unit. E.g. ms is treated as millisecond and not metre seconds... be careful. Syntax highlighting will highlight in green the base unit, everything just before is prefix and just after is power so use this to check.
  • Use only one '/' sign to specify rates (all units to the right of / will have their powers multiplied by -1)
  • Use ^ to apply integer powers to units
  • Units must be equivalent for conversion to occur

Extras

  • SI units can be used in the input quantity by adding them as a suffix. E.g. 3000=3k. This is useful to allow you to type in large range of values independent of the unit. You can choose to have the output also use SI prefix by ticking the check box. 
  • You can edit the quantity on either rows. The conversion is updated on the other row

Blog for my android calculator: TLCalc

I have created this blog to document the development of TLCalc and also to explain how some parts of the calculator work. Anyone who reads this is welcome to use the ideas I have posted in their own projects. I most definitely welcome anyone to use the ideas here on Windows Phone calculator since I am looking at getting a Windows phone next but would be hard pushed to go through with it if there isn't a decent scientific calculator on it. At the time I ported the calculator, the ones available were rather limiting but in the year that followed, the situation has now improved. There are probably 3-8 apps I would call acceptable and even surpass what I need for a calculator but bear in mind there are also hundreds of junk calculators flooding the store so finding a good one is rather difficult.

For now, I will introduce you to my calculator... TLCalc


The app is available for free on the Google Play Store at: https://play.google.com/store/apps/details?id=com.thompson.lam783.testtlcalc  

Basically, the calculator allows you to type in equations and the calculator will evaluate them at the end, very similar to a TI-84 graphics calculator. It supports 24 variables, numerous functions, complex numbers, graphing, basic algebraic manipulation and unit conversions. I believe it is self-explanatory to use except for the advance features which I will discuss in future posts.