tag:blogger.com,1999:blog-70247292024-03-14T08:22:43.585+11:00SuccesslessnessTomhttp://www.blogger.com/profile/06694547908603725128noreply@blogger.comBlogger451125tag:blogger.com,1999:blog-7024729.post-11098523378283948302014-05-20T21:51:00.000+10:002014-05-20T21:51:18.094+10:00A Better Bank StatementRegular bank statements are presented in a very unhelpful way - simply sorted by date, with no analysis of the items to assist you to understand them.<br />
<br />
View my <a href="http://nbviewer.ipython.org/url/tompaton.com/resources/Better%20Bank%20Statement.ipynb">Better Bank Statement.ipynb</a> in <a href="http://nbviewer.ipython.org/">nbviewer</a> or download <a href="http://tompaton.com/resources/Better%20Bank%20Statement.ipynb">the .ipynb file</a>.<br />
<br />
The <a href="http://ipython.org/notebook.html">iPython Notebook</a> uses <a href="http://pandas.pydata.org/">Pandas</a> to analyse your bank statement (downloaded as a .csv) and produces output like the following.<br />
<br />
New items that haven't been seen before are separated out so they can be checked easily:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://4.bp.blogspot.com/-WA-PT2-nNJ4/U3s9Npx1Z2I/AAAAAAAAAWw/Hrhww2NqXCk/s1600/new-items.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://4.bp.blogspot.com/-WA-PT2-nNJ4/U3s9Npx1Z2I/AAAAAAAAAWw/Hrhww2NqXCk/s1600/new-items.png" height="98" width="320" /></a></div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
Items to payees that have been seen before but with amounts that are different are displayed graphically to let you see at a glance which are truly problematic and which are normal variations.</div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="http://4.bp.blogspot.com/-DVmLrvA72ow/U3s9N27yY2I/AAAAAAAAAW4/Gcimym1FQTk/s1600/varying-items.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://4.bp.blogspot.com/-DVmLrvA72ow/U3s9N27yY2I/AAAAAAAAAW4/Gcimym1FQTk/s1600/varying-items.png" height="139" width="320" /></a></div>
<br />
Recurring items are usually going to be fine, but you may want to check that an automatic payment hasn't been missed:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://2.bp.blogspot.com/-U5tQbTBOUaA/U3s9N3AXwJI/AAAAAAAAAW0/TptK_XmMLvI/s1600/recurring-items.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://2.bp.blogspot.com/-U5tQbTBOUaA/U3s9N3AXwJI/AAAAAAAAAW0/TptK_XmMLvI/s1600/recurring-items.png" height="66" width="320" /></a></div>
<br />
The same breakdown is done for credits.<br />
<br />
You have to ask yourself: why can't the bank do something like this? Until then, see how you go playing with this notebook.Tomhttp://www.blogger.com/profile/06694547908603725128noreply@blogger.com1tag:blogger.com,1999:blog-7024729.post-31024570241727696272013-11-25T20:50:00.002+11:002013-11-25T20:50:19.488+11:00Adidas Adipure Adapt 2 review<p>I've had a pair of <a href="http://www.adidas.com.au/mens-adipure-adapt-2-shoes/Q21484_590.html">Adidas Adipure Adapt 2</a> shoes for a couple of months now and they instantly became my everyday regular running shoes.</p><div class="separator" style="clear: both; text-align: center;"><img border="0" height="240" src="http://1.bp.blogspot.com/-yRRabG3dY98/UpMaDo4HmeI/AAAAAAAAAVg/UrpcgbRIRpc/s320/IMG_4424.JPG" style="cursor: move;" width="320" /></div><p>Zero drop, light weight and minimal, they're pretty much perfect. The neoprene(?) uppers slip on easily and are the first shoes I've ever had that didn't rub in the slightest. I wear all my running shoes without socks & the first day I wore them I ran 25km (in 2 sessions) without a single bit of skin feeling anything at all.</p><p>The upper is a bit thicker than the Hattoris, so I suspect they'll be a little sweatier when the weather warms up, but they're not smelling too bad so far and wash easily.</p><p>The sole, like the <a href="http://successlessness.blogspot.com.au/2011/12/saucony-hattori-review.html">Saucony Hattoris</a>, is much more robust than it appears. No noticeable wear after 500km.</p><p>I liked them so much I bought a second pair pretty much straight away. Saucony had ceased production of the Hattoris by the time I'd done 2200km and started to wear them out, so I didn't want that to happen again!</p><p>And, they're nice and cheap too. I can't recommend them highly enough.</p>Tomhttp://www.blogger.com/profile/06694547908603725128noreply@blogger.com3tag:blogger.com,1999:blog-7024729.post-57406066064134600322013-02-12T22:23:00.003+11:002013-02-12T22:23:55.991+11:00iPhone and Garmin GPS accuracy testsFollowing on from my <a href="http://successlessness.blogspot.com.au/2013/01/magellan-switch-up-gps-watch-accuracy.html">accuracy testing of the Magellan Switch Up</a>, I've done a couple more tests that show some interesting results.<br />
<br />
First, in response to mjs's query about Wi-Fi assisted GPS affecting the iPhone results, I ran the same route with Wi-Fi on (green track) and off (red track):<br />
<br />
<a href="http://3.bp.blogspot.com/-fjLLabhrY6c/URoebPRSA1I/AAAAAAAAARk/E6ox2gYl_6A/s1600/GPSLog%25C2%25A0Labs%2B-%2BSelection-%2BiPhone%2Btest.png" imageanchor="1"><img border="0" src="http://3.bp.blogspot.com/-fjLLabhrY6c/URoebPRSA1I/AAAAAAAAARk/E6ox2gYl_6A/s400/GPSLog%25C2%25A0Labs%2B-%2BSelection-%2BiPhone%2Btest.png" height="357" width="400" /></a><br />
<br />
This is really interesting as it's clear that with the Wi-Fi on, the track "snaps" to the middle of the street, which is where the Google street view car (or similar) would have driven when scanning access points to generate <a href="http://en.wikipedia.org/wiki/Wi-Fi_positioning_system">Wi-Fi positioning system</a> data. (This is using my iPhone 3GS, so newer iPhones might behave differently.)<br />
<br />
Interesting as this is, the accuracy isn't improved sufficiently with Wi-Fi off to make the iPhone competitive with a dedicated GPS logger.<br />
<br />
The second thing I've noticed is the difference in behaviour between my Locosys Genie GT-31 and my Garmin Forerunner 210 when the GPS signal is blocked or poor.<br />
<br />
This map shows 10 tracks recorded with the GT-31 along a running track that is <a href="http://ajft.org/2003/01/15/103-0399_img">suspended below an 8 lane freeway</a>. The concrete roof severely limits the amount of sky that can be seen and the track jumps about and takes a while to get back to normal:<br />
<br />
<a href="http://3.bp.blogspot.com/-YDjZvRVgqao/URofabc5KnI/AAAAAAAAARw/nmN0Em79A-Q/s1600/GPSLog%25C2%25A0Labs%2B-%2BSelection-%2Btunnel%2Btest%2B-%2Bgt31.png" imageanchor="1"><img border="0" src="http://3.bp.blogspot.com/-YDjZvRVgqao/URofabc5KnI/AAAAAAAAARw/nmN0Em79A-Q/s400/GPSLog%25C2%25A0Labs%2B-%2BSelection-%2Btunnel%2Btest%2B-%2Bgt31.png" height="300" width="400" /></a><br />
<br />
The behaviour of the Garmin is vastly different (the following map shows another 10 tracks):<br />
<br />
<a href="http://2.bp.blogspot.com/-Vb2V25RcJ94/URof_wsyxRI/AAAAAAAAAR8/SBf_X6QTLJ4/s1600/GPSLog%25C2%25A0Labs%2B-%2BSelection-%2Btunnel%2Btest%2B-%2Bgarmin.png" imageanchor="1"><img border="0" src="http://2.bp.blogspot.com/-Vb2V25RcJ94/URof_wsyxRI/AAAAAAAAAR8/SBf_X6QTLJ4/s400/GPSLog%25C2%25A0Labs%2B-%2BSelection-%2Btunnel%2Btest%2B-%2Bgarmin.png" height="300" width="400" /></a><br />
<br />
Garmin has obviously tuned their GPS error correction algorithms specifically for running, and if the GPS data is varying wildly they can assume you're generally going to keep heading in the same direction at the same speed.<br />
<br />
The Magellan Switch Up actually performs pretty well in this regard too, I only have 4 logs to map here, but aside from it's "wobble", it doesn't do too badly with a poor signal:<br />
<br />
<a href="http://4.bp.blogspot.com/-pcfU8xnkIKM/URoi4IlTrlI/AAAAAAAAASQ/HazH2lLAGhg/s1600/GPSLog%25C2%25A0Labs%2B-%2BSelection-%2Btunnel%2Btest%2B-%2Bmagellan.png" imageanchor="1"><img border="0" src="http://4.bp.blogspot.com/-pcfU8xnkIKM/URoi4IlTrlI/AAAAAAAAASQ/HazH2lLAGhg/s400/GPSLog%25C2%25A0Labs%2B-%2BSelection-%2Btunnel%2Btest%2B-%2Bmagellan.png" height="300" width="400" /></a><br />
<br />
These maps were all produced with my site <a href="http://gpsloglabs.com/">GPSLog Labs</a>, which has the ability to <a href="http://blog.gpsloglabs.com/using-the-position-filter-to-clean-tracks-thr">filter and clean up GPS data</a>, though I'll be needing that feature much less now I'm getting good quality data from the Garmin!Tomhttp://www.blogger.com/profile/06694547908603725128noreply@blogger.com0tag:blogger.com,1999:blog-7024729.post-68839151192966424272013-01-06T22:05:00.001+11:002013-02-01T22:46:26.726+11:00Magellan Switch Up GPS watch accuracy comparison [UPDATED]My <a href="http://www.locosystech.com/product.php?id=30">Locosys Genie GT-31</a> died after logging nearly 100 days of data in 2.5 years.<br />
<br />
For a few weeks I used my iPhone and was very unhappy with the accuracy of the resulting logs, having been spoiled by the extremely accurate GT-31.<br />
<br />
So it was time to get a running watch and the choice was between a Garmin Forerunner 210 and the <a href="http://www.magellangps.com.au/Products/Fitness/Switch_Series/Switch_Up">Magellan Switch Up</a>. The Switch Up has a lot more features than the Forerunner but is much newer and less tried and true.<br />
<br />
<a href="http://www.dcrainmaker.com/2012/07/magellan-switch-up-in-depth-review.html">DC Rainmaker did a full review of the Switch Up</a>, so I'll only look at a few tests that show the accuracy of the resulting logs, which was my main concern when buying it and as it happens, not really a strong point of the Switch Up.<br />
<br />
Unfortunately, without another GPS watch to compare it to directly, it's hard to know if this is a failure of the Switch Up in particular or running watches in general.<br />
<br />
The best I could do was to run two laps of the same route, the first with the Switch Up in a shoulder pouch, the second with it worn as a watch. <br />
<br />
I then compared these two logs against one recorded by the iPhone and one from the GT-31. All maps and graphs were generated with my site, <a href="http://gpsloglabs.com/">GPSLog Labs</a>.<br />
<br />
<b>MAPS</b><br />
<br />
The route I ran is all straight segments up and down a series of increasingly steep streets. This makes a good comparison as it's obvious if the logger is recording the straight path. There aren't too many trees along the streets and no tall buildings to block the GPS signal.<br />
<br />
<i>GT-31</i><br />
<a href="http://1.bp.blogspot.com/-P8v88J-Zo_A/UOki-p2XoEI/AAAAAAAAANk/7fck2TfIXTs/s1600/1.%2BMap%2B-%2BGT31.png" imageanchor="1" style=""><img border="0" height="295" width="400" src="http://1.bp.blogspot.com/-P8v88J-Zo_A/UOki-p2XoEI/AAAAAAAAANk/7fck2TfIXTs/s400/1.%2BMap%2B-%2BGT31.png" /></a><br />
The GT-31 records the streets as nice straight lines, doesn't cut off the corners and the segments that are re-run generally overlap. The south side of each street seems to waver a little more than the north, possibly because when running west with the logger on my left shoulder, there are less satellites in view.<br />
<br />
<i>iPhone</i><br />
<a href="http://2.bp.blogspot.com/-n3v_jecHr34/UOkjGTn1lSI/AAAAAAAAANw/7Xb1NpZl50o/s1600/1.%2BMap%2B-%2BiPhone.png" imageanchor="1" style=""><img border="0" height="295" width="400" src="http://2.bp.blogspot.com/-n3v_jecHr34/UOkjGTn1lSI/AAAAAAAAANw/7Xb1NpZl50o/s400/1.%2BMap%2B-%2BiPhone.png" /></a><br />
The iPhone really "rounds" out the log, cutting the turnarounds short and generally doing a very rough job. It almost certainly isn't getting a position fix every second like the other loggers. It's only a 3GS and suspect a newer iPhone would probably do better.<br />
<br />
<i>Switch Up (shoulder pouch)</i><br />
<a href="http://2.bp.blogspot.com/-WFwZg9FsLiw/UOkjMpp4XWI/AAAAAAAAAN8/kzSVpa0rNmo/s1600/1.%2BMap%2B-%2BShoulder.png" imageanchor="1" style=""><img border="0" height="295" width="400" src="http://2.bp.blogspot.com/-WFwZg9FsLiw/UOkjMpp4XWI/AAAAAAAAAN8/kzSVpa0rNmo/s400/1.%2BMap%2B-%2BShoulder.png" /></a><br />
The Switch Up records a very wobbly path. The turnarounds are at the right points, but repeated segments don't overlap, and again, the north side of the streets seems to be slightly straighter than the south.<br />
<br />
<i>Switch Up (watch)</i><br />
<a href="http://1.bp.blogspot.com/-EUdOOI4YByA/UOkjRI7xHFI/AAAAAAAAAOI/XgompEOxzG4/s1600/1.%2BMap%2B-%2BWrist.png" imageanchor="1" style=""><img border="0" height="295" width="400" src="http://1.bp.blogspot.com/-EUdOOI4YByA/UOkjRI7xHFI/AAAAAAAAAOI/XgompEOxzG4/s400/1.%2BMap%2B-%2BWrist.png" /></a><br />
Thankfully, when the Switch Up is worn as a watch it seems that it actually performs slightly better, probably due to a more all round view of the sky, otherwise many of it's useful features would be negated.<br />
<br />
<br />
<b>DISTANCE</b><br />
<br />
This inaccuracy in the recorded position has a fairly negative effect on the resulting recorded distance, all those little wobbles increase the distance by about 2%. If this was your only watch then you wouldn't care, but if you've got a few thousands of kilometers of running logged already, it's annoying to no longer be able to compare them easily. <br />
<br />
The following table compares the iPhone and the Switch Up to the 29 logs of this route I got with the GT-31:<br />
<br />
<table><thead>
<tr> <th></th> <th>Count</th> <th>Average Distance</th> <th>Standard Dev</th> <th>Difference</th> </tr>
</thead> <tbody>
<tr> <td>Locosys Genie GT-31</td> <td style="text-align: right;">29</td> <td style="text-align: right;">6067.6 m</td> <td style="text-align: right;">33.13</td> <td style="text-align: right;"></td> </tr>
<tr> <td>iPhone 3GS</td> <td style="text-align: right;">3</td> <td style="text-align: right;">5973.3 m</td> <td style="text-align: right;">70.24</td> <td style="text-align: right;">98.45%</td> </tr>
<tr> <td>Magellan Switch Up</td> <td style="text-align: right;">2</td> <td style="text-align: right;">6190.0 m</td> <td style="text-align: right;">42.43</td> <td style="text-align: right;">102.02%</td> </tr>
</tbody> </table><br />
Obviously, with only 2 logs from the Switch Up it's a bit early to nail down the extra distance, but in both cases the measured distance would be a far outlier for the GT-31:<br />
<br />
<a href="http://3.bp.blogspot.com/-zjukeUTMDgY/UOkje39sdkI/AAAAAAAAAOU/xhw_WiN8IQU/s1600/2.%2BDistances.png" imageanchor="1" style=""><img border="0" height="221" width="300" src="http://3.bp.blogspot.com/-zjukeUTMDgY/UOkje39sdkI/AAAAAAAAAOU/xhw_WiN8IQU/s400/2.%2BDistances.png" /></a><br />
<br />
<b>SPEED</b><br />
<br />
<i>GT-31</i><br />
<a href="http://3.bp.blogspot.com/-19uo8od2VkY/UOkjnlYF8JI/AAAAAAAAAOg/m3rw2CH5C68/s1600/3.%2BFilters%2B-%2BGT31.png" imageanchor="1" style=""><img border="0" height="347" width="400" src="http://3.bp.blogspot.com/-19uo8od2VkY/UOkjnlYF8JI/AAAAAAAAAOg/m3rw2CH5C68/s400/3.%2BFilters%2B-%2BGT31.png" /></a><br />
The GT-31 sets the baseline, but even it's recorded speed bounces around a bit. The dark blue line shows the median over a 25 second window, the pale blue line is the actual speed from the logger.<br />
<br />
The histogram has a fairly tight cluster of speeds, bearing in mind that this route is up and down hills so the speed should vary a bit.<br />
<br />
<i>iPhone</i><br />
<a href="http://1.bp.blogspot.com/-rc4b3et9I0A/UOkjsoBH30I/AAAAAAAAAOs/cEErRWvaTe8/s1600/3.%2BFilters%2B-%2BiPhone.png" imageanchor="1" style=""><img border="0" height="356" width="400" src="http://1.bp.blogspot.com/-rc4b3et9I0A/UOkjsoBH30I/AAAAAAAAAOs/cEErRWvaTe8/s400/3.%2BFilters%2B-%2BiPhone.png" /></a><br />
The worst case is the iPhone, with the speeds varying by more than 10km/h in a few seconds. Even after taking the median the speed still jumps around wildly and the histogram is a mess.<br />
<br />
<i>Switch Up (shoulder pouch)</i><br />
<a href="http://1.bp.blogspot.com/-Jop5rTUfW9M/UOkjxI65vqI/AAAAAAAAAO4/Z3l56D7-tcI/s1600/3.%2BFilters%2B-%2BShoulder.png" imageanchor="1" style=""><img border="0" height="354" width="400" src="http://1.bp.blogspot.com/-Jop5rTUfW9M/UOkjxI65vqI/AAAAAAAAAO4/Z3l56D7-tcI/s400/3.%2BFilters%2B-%2BShoulder.png" /></a><br />
The Switch Up's speed is much better than the iPhone, especially once an average is taken.<br />
<br />
<i>Switch Up (watch)</i><br />
<a href="http://1.bp.blogspot.com/-PfovVRRQ64Q/UOkj1VSMWaI/AAAAAAAAAPE/jeI74tnBZOo/s1600/3.%2BFilters%2B-%2BWrist.png" imageanchor="1" style=""><img border="0" height="346" width="400" src="http://1.bp.blogspot.com/-PfovVRRQ64Q/UOkj1VSMWaI/AAAAAAAAAPE/jeI74tnBZOo/s400/3.%2BFilters%2B-%2BWrist.png" /></a><br />
<br />
There's no appreciable difference between wearing it on your wrist or in a shoulder pouch, but in practice it still means that if you look at the instantaneous pace readout you're likely to be seeing a value that's incorrect by anywhere up to 1 min/km. This means that the only way to really pace yourself is to use the auto-lap timer and use the lap averages.<br />
<br />
<b>HEADING</b><br />
<br />
The Heading vs Time graphs really should be alternating between steady east and steady west and the Distance vs Heading should be a nice sharp elongated cross.<br />
<br />
<i>GT-31</i><br />
<a href="http://2.bp.blogspot.com/-2GjgFIcWnbg/UOkj8xjz4wI/AAAAAAAAAPQ/8ssNUxQm3tU/s1600/4.%2BHeading%2B1%2B-%2BGT31.png" imageanchor="1" style=""><img border="0" height="117" width="400" src="http://2.bp.blogspot.com/-2GjgFIcWnbg/UOkj8xjz4wI/AAAAAAAAAPQ/8ssNUxQm3tU/s400/4.%2BHeading%2B1%2B-%2BGT31.png" /></a><br />
<a href="http://2.bp.blogspot.com/-oRYzPT1BOnk/UOkkCHnmfUI/AAAAAAAAAPc/B5eIUD14cr8/s1600/4.%2BHeading%2B2%2B-%2BGT31.png" imageanchor="1" style=""><img border="0" height="400" width="393" src="http://2.bp.blogspot.com/-oRYzPT1BOnk/UOkkCHnmfUI/AAAAAAAAAPc/B5eIUD14cr8/s400/4.%2BHeading%2B2%2B-%2BGT31.png" /></a><br />
<br />
<i>iPhone</i><br />
<a href="http://4.bp.blogspot.com/-YrwyXng423o/UOkkJJv8KXI/AAAAAAAAAPo/_369whPZnsw/s1600/4.%2BHeading%2B1%2B-%2BiPhone.png" imageanchor="1" style=""><img border="0" height="116" width="400" src="http://4.bp.blogspot.com/-YrwyXng423o/UOkkJJv8KXI/AAAAAAAAAPo/_369whPZnsw/s400/4.%2BHeading%2B1%2B-%2BiPhone.png" /></a><br />
<a href="http://2.bp.blogspot.com/-k373_E_vuTE/UOkkNRvBxBI/AAAAAAAAAP0/WG2WvCr8GVQ/s1600/4.%2BHeading%2B2%2B-%2BiPhone.png" imageanchor="1" style=""><img border="0" height="400" width="393" src="http://2.bp.blogspot.com/-k373_E_vuTE/UOkkNRvBxBI/AAAAAAAAAP0/WG2WvCr8GVQ/s400/4.%2BHeading%2B2%2B-%2BiPhone.png" /></a><br />
The iPhone actually performs quite well on this measure because it's obviously taking less frequent readings and doing some kind of interpolation between them.<br />
<br />
<i>Switch Up (shoulder pouch)</i><br />
<a href="http://2.bp.blogspot.com/-MPElwT-i9pI/UOkkTJ-2hOI/AAAAAAAAAQA/hMOafyjlAFA/s1600/4.%2BHeading%2B1%2B-%2BShoulder.png" imageanchor="1" style=""><img border="0" height="118" width="400" src="http://2.bp.blogspot.com/-MPElwT-i9pI/UOkkTJ-2hOI/AAAAAAAAAQA/hMOafyjlAFA/s400/4.%2BHeading%2B1%2B-%2BShoulder.png" /></a><br />
<a href="http://1.bp.blogspot.com/-Tf8a9RDl9U8/UOkkZjafF-I/AAAAAAAAAQM/MB_XE0JaEAg/s1600/4.%2BHeading%2B2%2B-%2BShoulder.png" imageanchor="1" style=""><img border="0" height="400" width="392" src="http://1.bp.blogspot.com/-Tf8a9RDl9U8/UOkkZjafF-I/AAAAAAAAAQM/MB_XE0JaEAg/s400/4.%2BHeading%2B2%2B-%2BShoulder.png" /></a><br />
<br />
<i>Switch Up (watch)</i><br />
<a href="http://2.bp.blogspot.com/-WobQJv_02nc/UOkkdxQ_i-I/AAAAAAAAAQY/7P58TnFUbY0/s1600/4.%2BHeading%2B1%2B-%2BWrist.png" imageanchor="1" style=""><img border="0" height="117" width="400" src="http://2.bp.blogspot.com/-WobQJv_02nc/UOkkdxQ_i-I/AAAAAAAAAQY/7P58TnFUbY0/s400/4.%2BHeading%2B1%2B-%2BWrist.png" /></a><br />
<a href="http://4.bp.blogspot.com/-mEQtH1ChHjA/UOkkhRl3D3I/AAAAAAAAAQk/R9-h0dI4tIU/s1600/4.%2BHeading%2B2%2B-%2BWrist.png" imageanchor="1" style=""><img border="0" height="400" width="393" src="http://4.bp.blogspot.com/-mEQtH1ChHjA/UOkkhRl3D3I/AAAAAAAAAQk/R9-h0dI4tIU/s400/4.%2BHeading%2B2%2B-%2BWrist.png" /></a><br />
<br />
<b>ALTITUDE</b><br />
Thankfully, there is one area where the Switch Up shines due to it's barometric altitude sensor.<br />
<br />
<a href="http://4.bp.blogspot.com/-MRW8SD2JRHY/UOlHNvMO2sI/AAAAAAAAAQ4/ekD9gH2oaWc/s1600/5.%2BAlts.png" imageanchor="1" style=""><img border="0" height="205" width="400" src="http://4.bp.blogspot.com/-MRW8SD2JRHY/UOlHNvMO2sI/AAAAAAAAAQ4/ekD9gH2oaWc/s400/5.%2BAlts.png" /></a><br />
<br />
The green and red lines are from the Switch Up, both laps are nearly identical and the hills look nice and symmetrical.<br />
<br />
The yellow line is from the GT-31, and the blue from the iPhone.<br />
<br />
When all records from the GT-31 are plotted together, the overall shape becomes clearer, though it seems that the Switch Up is still only providing consistent relative altitudes rather than an accurate absolute altitude:<br />
<a href="http://2.bp.blogspot.com/-Juiy470ie7c/UOlHqDuK26I/AAAAAAAAARE/eUXchB7ETXQ/s1600/5.%2BAlts%2B-%2Ball%2BGT31.png" imageanchor="1" style=""><img border="0" height="205" width="400" src="http://2.bp.blogspot.com/-Juiy470ie7c/UOlHqDuK26I/AAAAAAAAARE/eUXchB7ETXQ/s400/5.%2BAlts%2B-%2Ball%2BGT31.png" /></a><br />
<br />
There aren't many laps recorded on the iPhone, but the altitude is all over the place anyway:<br />
<a href="http://3.bp.blogspot.com/-vJbxo3Fp29g/UOlIG16wFhI/AAAAAAAAARU/J0WY6NHF1xA/s1600/5.%2BAlts%2B-%2Ball%2BiPhone.png" imageanchor="1" style=""><img border="0" height="204" width="400" src="http://3.bp.blogspot.com/-vJbxo3Fp29g/UOlIG16wFhI/AAAAAAAAARU/J0WY6NHF1xA/s400/5.%2BAlts%2B-%2Ball%2BiPhone.png" /></a><br />
<br />
<b>CONCLUSION</b><br />
<br />
Without using another GPS watch in a direct comparison, it's hard to really rate the accuracy of the Switch Up. My only other experience with GPS watches is with the <a href="http://www.globalsat.com.tw/products-page.php?menu=2&gs_en_product_id=5&gs_en_product_cnt_id=21">GlobalSat GH-615M</a>, which created a very similar log to that of the GT-31 when I did a run with them together a couple of years ago (both use the same SiRF Star III chipset).<br />
<br />
Feature and price-wise the Switch Up is fine, so maybe the accuracy can be improved with a firmware update since it's using the SiRF Star IV chipset which is hopefully better than the III. (Note that these logs were created with the version 19.48 firmware.)<br />
<br />
YMMV.<br />
<br />
<b>UPDATE</b> 1-Feb-2013<br />
<br />
Unfortunately there was something faulty with my Switch Up and I've returned it. It was freezing up, sometimes after only a few hundred meters, and had to be turned on and off and the activity reset before it would record again. This was pretty frustrating, but before it died, I made a few more tests and did find I was able to improve the accuracy some what by ensuring I wore the watch on the hand that was going to "see the most sky" on the particular route I was running.<br />
<br />
I decided to replace the Switch Up with a <a href="https://buy.garmin.com/shop/shop.do?cID=142&pID=83280">Garmin Forerunner 210</a>, which I'm very happy with. The Garmin has fewer features than the Switch Up, but it's very easy to use and reliable. I also purchased another <a href="http://www.locosystech.com/product.php?id=30">Locosys Genie GT-31</a> for use as a non-running logger, since it's accuracy and long battery life are fantastic.<br />
<br />
The accuracy of the Garmin has been very good: The distances are pretty much spot on what I was getting from the GT-31, the instantaneous pace is pretty good. It's altitude readings can't compare to the Switch Up, and the heart rate logs seem a little less detailed but overall it's great value.<br />
<br />
One thing I've found is that it pays to wait around a bit after the satellite signal has been acquired before you start (e.g. until it displays a zero speed) as it's pretty optimistic about when it has a "lock".<br />
Tomhttp://www.blogger.com/profile/06694547908603725128noreply@blogger.com6tag:blogger.com,1999:blog-7024729.post-85840409767410820422012-03-17T18:49:00.006+11:002012-03-17T19:34:01.002+11:00Adding "Search at point" function to Notepad++<p><a href="http://notepad-plus-plus.org/">Notepad++</a> with the <a href="http://npppythonscript.sourceforge.net/">Python Script</a> plugin is a great code editor for windows.</p>
<p>After adding the following two files to your Notepad++ scripts folder you can make it even better with what I'm quickly finding to be an indispensable feature:</p>
<p>By using the <kbd>Alt+left</kbd> and <kbd>Alt+right</kbd> keys you can move to the previous and next occurrences of the symbol under the cursor. e.g. you can move quickly between all usages of a variable or function name in a file.</p>
<pre># goto_next_occurrence.py
"""Move cursor to next occurrence of the current word in the file, wrap around if possible."""
from Npp import editor, FINDOPTION
symbol = editor.getCurrentWord()
editor.wordRight()
editor.searchAnchor()
pos = editor.searchNext(FINDOPTION.MATCHCASE+FINDOPTION.WHOLEWORD+FINDOPTION.WORDSTART, symbol)
editor.scrollCaret()
</pre>
<pre># goto_prev_occurrence.py
"""Move cursor to previous occurrence of the current word in the file, wrap around if possible."""
from Npp import editor, FINDOPTION
symbol = editor.getCurrentWord()
editor.wordLeft()
editor.searchAnchor()
pos = editor.searchPrev(FINDOPTION.MATCHCASE+FINDOPTION.WHOLEWORD+FINDOPTION.WORDSTART, symbol)
editor.scrollCaret()
</pre>
<p>Then add them to the menu:</p>
<p><a href="http://4.bp.blogspot.com/-p7cuFwKL4Y0/T2RDe7mOpbI/AAAAAAAAAJA/mihu92TiwgM/s1600/image002.jpg"><img style="cursor:pointer; cursor:hand;width: 400px; height: 395px;" src="http://4.bp.blogspot.com/-p7cuFwKL4Y0/T2RDe7mOpbI/AAAAAAAAAJA/mihu92TiwgM/s400/image002.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5720771625340544434" /></a></p>
<p>And bind them to keys, <kbd>Alt-right</kbd> and <kbd>Alt-left</kbd> work well for me:</p>
<p><a href="http://2.bp.blogspot.com/-0xytBri3zCM/T2RDXXOGWdI/AAAAAAAAAI0/iaVyvaeHr9s/s1600/image004.jpg"><img style="cursor:pointer; cursor:hand;width: 400px; height: 399px;" src="http://2.bp.blogspot.com/-0xytBri3zCM/T2RDXXOGWdI/AAAAAAAAAI0/iaVyvaeHr9s/s400/image004.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5720771495316576722" /></a></p>
<p>Back in Linux-land, there are a <a href="http://www.emacswiki.org/emacs/SearchAtPoint">bunch of ways to add similar functionality to Emacs</a>, I like <a href="https://github.com/mitsuo-saito/auto-highlight-symbol-mode/wiki/">this one</a>.</p>Tomhttp://www.blogger.com/profile/06694547908603725128noreply@blogger.com0tag:blogger.com,1999:blog-7024729.post-70936597636469931432011-12-22T20:57:00.005+11:002013-06-07T20:12:41.701+10:00Saucony Hattori reviewI've just bought a pair of <a href="http://www.saucony.com/store/SiteController/saucony/productdetails?catId=cat10004&productId=4-106900">Saucony Hattoris</a> and they really good!<br />
<br />
<a href="http://2.bp.blogspot.com/-nMDqOd_uO8M/TvL_qqKvP8I/AAAAAAAAAIY/UxerWT3R-V8/s1600/photo.JPG"><img alt="" border="0" src="http://2.bp.blogspot.com/-nMDqOd_uO8M/TvL_qqKvP8I/AAAAAAAAAIY/UxerWT3R-V8/s400/photo.JPG" id="BLOGGER_PHOTO_ID_5688890387661275074" style="cursor: hand; cursor: pointer; height: 300px; width: 400px;" /></a><br />
<br />
They're minimal, but have a lot more cushioning than Vibrams or the <a href="http://successlessness.blogspot.com/2011/08/terra-plana-evo-review.html">Evos</a> since the EVA+ sole is quite soft and about 10mm thick. They're zero-drop and ultra light weight (250g/pair), about half the weight of the Evos and lighter than Vibram Sprints.<br />
<br />
They're a really comfy fit, with no laces they are held on by a tight, thin "lycra" upper. Mine are maybe a little on the small side, so the velcro straps don't do anything useful.<br />
<br />
The tight fitting upper has another advantage that no other shoes I've got come close too: It's virtually impossible to get sand in them, which is great as that's extra annoying when running sock-less.<br />
<br />
Time will tell how the sole wears, but the hard reinforcements are in all the places I usually wear through, so I can't see why they won't last a fair while.<br />
<br />
<b>[Update, 7-June-2013]</b> It slipped my mind to do an update on these, but I've been wearing them on my shorter runs pretty much all the time over the last 18 months. They've racked up more than 1,800km and though getting quite worn I'm sure they'll go past 2,000km without any problems.<br />
<br />
As I suspected, I bought them a tiny bit too small, which has occasionally given me a blister on the tips of my toes and I've now got a couple of holes in the upper where my big toe sticks out. This doesn't cause any problems however and the extra ventilation can't hurt!<br />
<br />
They get smelly fairly quickly, but washing them in warm water with a few tablespoons of white vinegar seems to clean them up nicely.<br />
<br />
They're a great shoe & fantastic value for money.Tomhttp://www.blogger.com/profile/06694547908603725128noreply@blogger.com0tag:blogger.com,1999:blog-7024729.post-91940515136807075092011-08-12T12:09:00.002+10:002011-08-12T12:11:43.858+10:00Please unsubscribe me from the World Vision newsletter<p>God knows I've tried enough times, maybe posting it to a blog is how you get off the list?</p>
<p><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://3.bp.blogspot.com/-Z-dnq-zBLoc/TkSLr231vgI/AAAAAAAAAHw/y99GWcD1VPE/s1600/world-vision-unsubscribe%2521.png"><img style="cursor:pointer; cursor:hand;width: 320px; height: 172px;" src="http://3.bp.blogspot.com/-Z-dnq-zBLoc/TkSLr231vgI/AAAAAAAAAHw/y99GWcD1VPE/s320/world-vision-unsubscribe%2521.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5639786218955062786" /></a></p>Tomhttp://www.blogger.com/profile/06694547908603725128noreply@blogger.com0tag:blogger.com,1999:blog-7024729.post-74515223767010946512011-08-07T17:18:00.006+10:002011-08-07T17:56:27.684+10:00Terra Plana Evo review<p>My Vibrams are worn out (or through) so after having a good experience with the <a href="http://successlessness.blogspot.com/2011/02/merrell-trail-gloves-review.html">Merrell Trail Gloves</a> I decided to get a pair of <a href="http://www.vivobarefoot.com/uk/mens/evo-m-8.html/">Terra Plana Evos</a>.</p>
<p><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/-3ncBEeMVpgg/Tj5DJ8aVM0I/AAAAAAAAAHo/nIH0XpGHHPs/s1600/IMG_3103.JPG"><img style="cursor:pointer; cursor:hand;width: 320px; height: 240px;" src="http://1.bp.blogspot.com/-3ncBEeMVpgg/Tj5DJ8aVM0I/AAAAAAAAAHo/nIH0XpGHHPs/s320/IMG_3103.JPG" border="0" alt=""id="BLOGGER_PHOTO_ID_5638017621628891970" /></a></p>
<p>It's a pain to get Vibram FiveFingers in Australia at the moment. They're hard to find in shops, can't be shipped here from the USA and they're about twice the price even though our dollar is currently trading higher than the USD.</p>
<p>The Evos are more expensive than the Vibrams but a little less "freaky" and provide a similar minimal experience to the Merrells. That is, your toes are free, there's no support or cushioning and you get a fair degree of "ground feel".</p>
<p>I considered a pair of Merrell True Gloves (the "road" equivalent of the Trail Gloves) but they had too much cushioning for my liking and unlike the Evos, they didn't have removable inner soles.</p>
<p>I bought them in the black/red style, which was probably not that smart. They contrast too much against my skin and tend to look a bit odd, they're a very small shoe and black emphasises that.</p>
<p>One thing to note, is that the women's model is identical to the men's, but quite a bit cheaper! Unfortunately they only go up to size 41, but it's something to take advantage of if you have small feet. Speaking of which, the Evos are sized pretty small, the 42 is plenty big enough for me, which is a size or two smaller than my regular shoes.</p>
<p>The lacing is the same as for the Merrell Trail Gloves, holding snuggly to the middle of the foot and leaving your toes free to splay. They don't lock onto my heel as nicely however and they tend to feel a little loose after running a few kms.</p>
<p>They are a minimal shoe and still a long way from barefoot, with less ground feel than Vibrams, even wearing them without socks and taking out the inner soles. Still, you feel pebbles and the contrast between concrete, asphalt, gravel, dirt, grass surfaces well enough and it doesn't seem to interfere with my stride. They don't have a lot of grip however and slip pretty easily if it gets muddy. While they'd probably do fine on dry trails, I don't think they feel very suitable for general trail running.</p>
<p>Not wearing socks did cause some significant rubbing issues however. That was partly due to me jumping straight into 12km runs with them, and I have to resort to prepping with vaseline. It's definitely something to be aware of as they press on different parts of your foot than the Five Fingers do.</p>
<p>I've done about 85km in them now and they feel good. They don't feel like they'll wear out too quickly, aren't too cold (though our winter has been fairly mild so far), and don't soak up water between the toes like the Five Fingers. They feel like they'll be fine in warmer weather too, the upper is a very thin and breathable mesh.</p>
<p>Overall, other than the price, they're great and I'd recommend them to any experienced minimalist shoe runners.</p>Tomhttp://www.blogger.com/profile/06694547908603725128noreply@blogger.com0tag:blogger.com,1999:blog-7024729.post-81877145377824791622011-06-22T20:14:00.004+10:002011-06-22T21:25:57.733+10:00No, wait, that's not right, here's how I really solved it.<p>Yesterday I posted the <a href="http://successlessness.blogspot.com/2011/06/sql-3x3-square-picture-puzzle-solution.html">solution to a puzzle</a> in response to <a href="http://multipolygon.com/prolog-3x3-square-picture-puzzle-solution/">one created by Reilly</a>. The solution is fine, but I explained it badly. The explanation just describes the finished code, which is all well and good, but it doesn't elucidate the steps I took to get there and isn't likely to help anyone as a result.</p>
<p>The following steps are much closer to what I actually did:</p>
<p><b>1.</b> While reading <a href="http://multipolygon.com/prolog-3x3-square-picture-puzzle-solution/">Reilly's Prolog version</a> I realized that SQL could probably solve the puzzle by generating a list of all the possible combinations of pieces (either by a join or in a temporary table) and then filtering those in some way to remove invalid solutions.</p>
<p><b>2.</b> I left it at that for a while, but a few thoughts popped into my head during the day on the structure of a "pieces" table and an INSERT statement that could generate the 4 possible rotations of each piece. I also ran the numbers in my head and realized there were 4^9 = 2^18 = 262,144 combinations which is not many at all really.</p>
<p><b>3.</b> I initially ran into a bit of a dead end imagining how a SELECT statement would generate all the combinations of pieces, but decided to just start writing it out and see where that led.</p>
<p>This was the crucial step: <em>Starting with the desired result and working backwards</em>.</p>
<p>So, I started with what I wanted: A list of piece numbers and rotations in each position of the puzzle:</p>
<pre>SELECT A.piece_id AS A, A.rotation AS Ar, B.piece_id AS B, B.rotation AS Br, C.piece_id AS C, C.rotation AS Cr,
D.piece_id AS D, D.rotation AS Dr, E.piece_id AS E, E.rotation AS Er, F.piece_id AS F, F.rotation AS Fr,
G.piece_id AS G, G.rotation AS Gr, H.piece_id AS H, H.rotation AS Hr, I.piece_id AS I, I.rotation AS Ir</pre>
<p>Then I started joining tables together to match the constraints of the puzzle (i.e. the animals on the adjacent edges match head-to-tail.) At this point I didn't know what fields were going to be in the table, or what it would be called, so I just made them up so the SELECT statement read nicely. First, I'll show the overall structure and come back to the "matches" bits:</p>
<pre>FROM rotated_pieces A
INNER JOIN rotated_pieces B ON -- A.right matches B.left --
INNER JOIN rotated_pieces C ON -- B.right matches C.left --
INNER JOIN rotated_pieces D ON -- A.bottom matches D.top --
INNER JOIN rotated_pieces E ON -- D.right matches E.left AND B.bottom matches E.top --
INNER JOIN rotated_pieces F ON -- E.right matches F.left AND C.bottom matches F.top --
INNER JOIN rotated_pieces G ON -- D.bottom matches G.top --
INNER JOIN rotated_pieces H ON -- G.right matches H.left AND E.bottom matches H.top --
INNER JOIN rotated_pieces I ON -- H.right matches I.left AND F.bottom matches I.top --</pre>
<p>My "matches" criteria was initially something like:</p>
<pre>( (A.right = B.left AND A.right_head = 'head' AND B.left_head = 'tail')
OR (A.right = B.left AND A.right_head = 'tail' AND B.left_head = 'head'))</pre>
<p>This makes sure the animals are the same and it's either head-tail or tail-head.</p>
<p>That seemed like it might work which was a pretty good sign I was on the right track. I wasn't sure how many combinations this would match, so decided to leave a WHERE clause for later. I was hoping this would be a unique solution, or there may be just a couple and I could pick the proper solution out by hand. Either way, that was a problem for later.</p>
<p><b>4.</b> From this sketch of a SELECT statement I was able to come up with an appropriate table structure for rotated_pieces:</p>
<pre>CREATE TABLE rotated_pieces (
piece_id INTEGER, # unique piece id
rotation INTEGER, # 0 - top, 1 - left, 2 - bottom, 3 - right
top CHAR(10), # animal at top edge
top_head CHAR(10), # head or tail at top edge
`right` CHAR(10),
right_head CHAR(10),
bottom CHAR(10),
bottom_head CHAR(10),
`left` CHAR(10),
left_head CHAR(10));</pre>
<p>And then I started on an INSERT statement to populate the table with the piece layouts:</p>
<pre>INSERT INTO rotated_pieces
(piece_id, rotation,
top, top_head, `right`, right_head, bottom, bottom_head, `left`, left_head)
VALUES (1, 0, 'cheetah', 'tail', 'tiger', 'tail', 'lion', 'head', 'tiger', 'head'),
(2, 0, 'lion', 'tail', 'lion', 'head', 'tiger', 'tail', 'cheetah', 'head'),
(3, 0, 'tiger', 'tail', 'lion', 'head', 'panther', 'head', 'tiger', 'tail'),
(4, 0, 'panther', 'tail', 'cheetah', 'head', 'panther', 'tail', 'lion', 'tail'),
(5, 0, 'tiger', 'head', 'tiger', 'head', 'cheetah', 'head', 'lion', 'tail'),
(6, 0, 'panther', 'tail', 'panther', 'head', 'cheetah', 'tail', 'tiger', 'head'),
(7, 0, 'panther', 'head', 'cheetah', 'tail', 'cheetah', 'head', 'lion', 'tail'),
(8, 0, 'panther', 'tail', 'lion', 'head', 'panther', 'head', 'cheetah', 'tail'),
(9, 0, 'cheetah', 'head', 'tiger', 'head', 'panther', 'head', 'lion', 'tail');</pre>
<p>While I was doing this I copy/pasted Reilly's equivalent bit of Prolog code and it was a simple text transformation between the two languages, another very good sign:</p>
<pre>Prolog:
tile(1, cheetah_tail, tiger_tail, lion_head, tiger_head).
SQL:
(1, 0, 'cheetah','tail', 'tiger','tail', 'lion','head', 'tiger','head'),</pre>
<p><b>5.</b> At this point, I realized that the "matches" criteria could be simplified a lot:</p>
<pre>A.right = B.left AND A.right_head != B.left_head</pre>
<p><b>6.</b> Now I was able to write out the "rotation" function I'd envisioned earlier in the day and realized I wasn't going to need a "pieces" table at all, this program was only going to need the 36 rotated_pieces records, another good sign that I was on the right track.</p>
<pre># rotate (top --> left)
INSERT INTO rotated_pieces (piece_id, rotation, top, top_head, `right`, right_head, bottom, bottom_head, `left`, left_head)
SELECT piece_id, rotation+1,
`right` AS top, right_head AS top_head,
bottom AS `right`, bottom_head AS right_head,
`left` AS bottom, left_head AS bottom_head,
top AS `left`, top_head AS left_head
FROM rotated_pieces;
# rotate (top --> bottom, left --> right)
INSERT INTO rotated_pieces (piece_id, rotation, top, top_head, `right`, right_head, bottom, bottom_head, `left`, left_head)
SELECT piece_id, rotation+2,
bottom AS top, bottom_head AS top_head,
`left` AS `right`, left_head AS right_head,
top AS bottom, top_head AS bottom_head,
`right` AS `left`, right_head AS left_head
FROM rotated_pieces;</pre>
<p><b>7.</b> Finally, I had all the code sketched out. I checked the syntax for a few things since I'd just been doing it off the top of my head and then executed it. After escaping `right` and `left` as they're keywords (not necessarily the best choice of field names...) the CREATE TABLE and INSERT statements ran. I did a sanity check of the rotated_pieces table and it looked good.</p>
<p>The result of my SELECT statement was disappointing though. As I sort of suspected, there are lots of solutions if you allow a piece to be used more than once and the SELECT returned 1272 rows.</p>
<p><b>8.</b> I couldn't think of a neat way to check for duplicates so ended up just writing a WHERE clause to do it by brute force:</p>
<pre>WHERE A.piece_id != B.piece_id AND A.piece_id != C.piece_id AND A.piece_id != D.piece_id
AND A.piece_id != E.piece_id AND A.piece_id != F.piece_id AND A.piece_id != G.piece_id
AND A.piece_id != H.piece_id AND A.piece_id != I.piece_id
AND B.piece_id != C.piece_id AND B.piece_id != D.piece_id AND B.piece_id != E.piece_id
AND B.piece_id != F.piece_id AND B.piece_id != G.piece_id AND B.piece_id != H.piece_id
AND B.piece_id != I.piece_id AND C.piece_id != D.piece_id AND C.piece_id != E.piece_id
AND C.piece_id != F.piece_id AND C.piece_id != G.piece_id AND C.piece_id != H.piece_id
AND C.piece_id != I.piece_id
AND D.piece_id != E.piece_id AND D.piece_id != F.piece_id AND D.piece_id != G.piece_id
AND D.piece_id != H.piece_id AND D.piece_id != I.piece_id
AND E.piece_id != F.piece_id AND E.piece_id != G.piece_id AND E.piece_id != H.piece_id
AND E.piece_id != I.piece_id
AND F.piece_id != G.piece_id AND F.piece_id != H.piece_id AND F.piece_id != I.piece_id
AND G.piece_id != H.piece_id AND G.piece_id != I.piece_id
AND H.piece_id != I.piece_id;</pre>
<p><b>9.</b> This had the desired result and I got back 4 rows.</p>
<pre>'A', 'Ar', 'B', 'Br', 'C', 'Cr', 'D', 'Dr', 'E', 'Er', 'F', 'Fr', 'G', 'Gr', 'H', 'Hr', 'I', 'Ir'
2, 1, 1, 0, 6, 0, 8, 3, 9, 3, 7, 2, 5, 3, 3, 0, 4, 0
6, 1, 7, 3, 4, 1, 1, 1, 9, 0, 3, 1, 2, 2, 8, 0, 5, 0
4, 2, 3, 2, 5, 1, 7, 0, 9, 1, 8, 1, 6, 2, 1, 2, 2, 3
5, 2, 8, 2, 2, 0, 3, 3, 9, 2, 1, 3, 4, 3, 7, 1, 6, 3</pre>
<p>The first row clearly matched Reilly's solution, but it wasn't immediately obvious what the other 3 rows were. After reformatting them into a 3x3 grid and staring for a little while I picked out the pattern of pieces 2-1-6 around the edges and realized it was the whole board being rotated. So there was a single solution after all.</p>
<pre>2, 1, 6,
8, 9, 7,
5, 3, 4
6, 7, 4,
1, 9, 3,
2, 8, 5
4, 3, 5,
7, 9, 8,
6, 1, 2
5, 8, 2,
3, 9, 1,
4, 7, 6</pre>
<p><b>10.</b> The next step was to pretend I knew all that from the beginning and write it up in the previous blog post. Later that evening I thought again and realized that wasn't so truthful and that I should have another shot at writing it up.</p>
<p><b>Postscript</b>: The ugly WHERE clause was still bugging me and I realized that it could be improved by making some kind of function involving all of the piece ids and checking the answer e.g. if they all add to 45 then that's a solution. Unfortunately, addition isn't a uniquely identifying function, and neither is multiplication or addition of 2^piece_id. What will work though is changing the piece ids to prime numbers and multiplying them all together. Keeping the first digit the same the ids are now:</p>
<pre>11, 23, 31, 41, 53, 61, 71, 83, 97</pre>
<p>The product of these primes has no other factors and we don't care about the order since multiplication is cummutative, so to ensure each piece appears only once the WHERE can now simply be:</p>
<pre>WHERE A.piece_id * B.piece_id * C.piece_id
* D.piece_id * E.piece_id * F.piece_id
* G.piece_id * H.piece_id * I.piece_id
= 11*23*31*41*53*61*71*83*97;</pre>Tomhttp://www.blogger.com/profile/06694547908603725128noreply@blogger.com1tag:blogger.com,1999:blog-7024729.post-87389967269467022422011-06-21T18:53:00.007+10:002011-06-22T21:25:27.075+10:00SQL: 3x3 Square Picture-Puzzle Solution<p>Update: <a href="http://successlessness.blogspot.com/2011/06/no-wait-thats-not-right-heres-how-i.html">A second attempt at a write up</a>.</p>
<p>After <a href="http://multipolygon.com/prolog-3x3-square-picture-puzzle-solution/">Reilly solved a 3x3 square picture puzzle using Prolog</a> I made a flippant remark that it would be interesting to solve using SQL and well, here goes...</p>
<p><img src="http://multipolygon.com/prolog-3x3-square-picture-puzzle-solution/numbered.jpg" width="400"/></p>
<p>First, create a table and populate it with the pieces:</p>
<pre># table of all pieces in all possible rotations
CREATE TABLE rotated_pieces (
piece_id INTEGER, # unique piece id
rotation INTEGER, # 0 - top, 1 - left, 2 - bottom, 3 - right
top CHAR(10), # animal at top edge
top_head CHAR(10), # head or tail at top edge
`right` CHAR(10),
right_head CHAR(10),
bottom CHAR(10),
bottom_head CHAR(10),
`left` CHAR(10),
left_head CHAR(10));
# unrotated pieces (top)
INSERT INTO rotated_pieces
(piece_id, rotation,
top, top_head, `right`, right_head, bottom, bottom_head, `left`, left_head)
VALUES (1, 0, 'cheetah', 'tail', 'tiger', 'tail', 'lion', 'head', 'tiger', 'head'),
(2, 0, 'lion', 'tail', 'lion', 'head', 'tiger', 'tail', 'cheetah', 'head'),
(3, 0, 'tiger', 'tail', 'lion', 'head', 'panther', 'head', 'tiger', 'tail'),
(4, 0, 'panther', 'tail', 'cheetah', 'head', 'panther', 'tail', 'lion', 'tail'),
(5, 0, 'tiger', 'head', 'tiger', 'head', 'cheetah', 'head', 'lion', 'tail'),
(6, 0, 'panther', 'tail', 'panther', 'head', 'cheetah', 'tail', 'tiger', 'head'),
(7, 0, 'panther', 'head', 'cheetah', 'tail', 'cheetah', 'head', 'lion', 'tail'),
(8, 0, 'panther', 'tail', 'lion', 'head', 'panther', 'head', 'cheetah', 'tail'),
(9, 0, 'cheetah', 'head', 'tiger', 'head', 'panther', 'head', 'lion', 'tail');</pre>
<p>Then, rotate each of those pieces so we have all 36 variations:</p>
<pre># rotate (top --> left)
INSERT INTO rotated_pieces (piece_id, rotation, top, top_head, `right`, right_head, bottom, bottom_head, `left`, left_head)
SELECT piece_id, rotation+1,
`right` AS top, right_head AS top_head,
bottom AS `right`, bottom_head AS right_head,
`left` AS bottom, left_head AS bottom_head,
top AS `left`, top_head AS left_head
FROM rotated_pieces;
# rotate (top --> bottom, left --> right)
INSERT INTO rotated_pieces (piece_id, rotation, top, top_head, `right`, right_head, bottom, bottom_head, `left`, left_head)
SELECT piece_id, rotation+2,
bottom AS top, bottom_head AS top_head,
`left` AS `right`, left_head AS right_head,
top AS bottom, top_head AS bottom_head,
`right` AS `left`, right_head AS left_head
FROM rotated_pieces;</pre>
<p>Then, it's a pretty simple query to return the result. The code is messed up by the WHERE clause which ensures that each piece is used only once in the solution, if there's a neater way to do this I'd like to know.</p>
<pre># find solution
SELECT A.piece_id AS A, A.rotation AS Ar, B.piece_id AS B, B.rotation AS Br, C.piece_id AS C, C.rotation AS Cr,
D.piece_id AS D, D.rotation AS Dr, E.piece_id AS E, E.rotation AS Er, F.piece_id AS F, F.rotation AS Fr,
G.piece_id AS G, G.rotation AS Gr, H.piece_id AS H, H.rotation AS Hr, I.piece_id AS I, I.rotation AS Ir
FROM rotated_pieces A
INNER JOIN rotated_pieces B ON (A.`right` = B.`left` AND A.right_head != B.left_head)
INNER JOIN rotated_pieces C ON (B.`right` = C.`left` AND B.right_head != C.left_head)
INNER JOIN rotated_pieces D ON (A.bottom = D.top AND A.bottom_head != D.top_head)
INNER JOIN rotated_pieces E ON (D.`right` = E.`left` AND D.right_head != E.left_head
AND B.bottom = E.top AND B.bottom_head != E.top_head)
INNER JOIN rotated_pieces F ON (E.`right` = F.`left` AND E.right_head != F.left_head
AND C.bottom = F.top AND C.bottom_head != F.top_head)
INNER JOIN rotated_pieces G ON (D.bottom = G.top AND D.bottom_head != G.top_head)
INNER JOIN rotated_pieces H ON (G.`right` = H.`left` AND G.right_head != H.left_head
AND E.bottom = H.top AND E.bottom_head != H.top_head)
INNER JOIN rotated_pieces I ON (H.`right` = I.`left` AND H.right_head != I.left_head
AND F.bottom = I.top AND F.bottom_head != I.top_head)
WHERE A.piece_id != B.piece_id AND A.piece_id != C.piece_id AND A.piece_id != D.piece_id
AND A.piece_id != E.piece_id AND A.piece_id != F.piece_id AND A.piece_id != G.piece_id
AND A.piece_id != H.piece_id AND A.piece_id != I.piece_id
AND B.piece_id != C.piece_id AND B.piece_id != D.piece_id AND B.piece_id != E.piece_id
AND B.piece_id != F.piece_id AND B.piece_id != G.piece_id AND B.piece_id != H.piece_id
AND B.piece_id != I.piece_id AND C.piece_id != D.piece_id AND C.piece_id != E.piece_id
AND C.piece_id != F.piece_id AND C.piece_id != G.piece_id AND C.piece_id != H.piece_id
AND C.piece_id != I.piece_id
AND D.piece_id != E.piece_id AND D.piece_id != F.piece_id AND D.piece_id != G.piece_id
AND D.piece_id != H.piece_id AND D.piece_id != I.piece_id
AND E.piece_id != F.piece_id AND E.piece_id != G.piece_id AND E.piece_id != H.piece_id
AND E.piece_id != I.piece_id
AND F.piece_id != G.piece_id AND F.piece_id != H.piece_id AND F.piece_id != I.piece_id
AND G.piece_id != H.piece_id AND G.piece_id != I.piece_id
AND H.piece_id != I.piece_id;</pre>
<p>This returns 4 rows, the first is our solution:</p>
<pre>2,1, 1,0, 6,0,
8,3, 9,3, 7,2,
5,3, 3,0, 4,0</pre>
<p><img src="http://multipolygon.com/prolog-3x3-square-picture-puzzle-solution/solved_annotated.jpg" width="400"/></p>
<p>The remaining 3 are the same solution with the entire board rotated:</p>
<pre>6,1, 7,3, 4,1,
1,1, 9,0, 3,1,
2,2, 8,0, 5,0
4,2, 3,2, 5,1,
7,0, 9,1, 8,1,
6,2, 1,2, 2,3
5,2, 8,2, 2,0,
3,3, 9,2, 1,3,
4,3, 7,1, 6,3</pre>
<p>I would argue the SQL version is clearer, but the <a href="http://multipolygon.com/prolog-3x3-square-picture-puzzle-solution/">Prolog version</a> is slightly terser and should be easier to extend to a 4x4 puzzle.</p>Tomhttp://www.blogger.com/profile/06694547908603725128noreply@blogger.com1tag:blogger.com,1999:blog-7024729.post-29368116011874698272011-02-21T13:31:00.004+11:002011-02-21T14:22:16.271+11:00Merrell Trail Gloves review<p>A quick review of the <a href="http://www.merrell.com/US/en-US/Product.mvc.aspx/22875M/0/Mens/Mens-Barefoot-Trail-Glove?dimensions=0">Merrell Trail Glove</a> minimalist trail running shoes.</p>
<p><a href="http://www.flickr.com/photos/tom-paton/5443959835/" target="_blank"><img style="cursor: pointer; width: 320px; height: 240px;" src="http://1.bp.blogspot.com/-Ut4vdLp-F2c/TWHQQT8QwMI/AAAAAAAAAG4/JDCYTOI6YRI/s320/5443959835_45b2d03395.jpg" alt="" id="BLOGGER_PHOTO_ID_5575966792310898882" border="0" /></a></p>
<p>After reading a few reviews of these shoes I was curious enough to fork out the money for them (<a href="http://barefootrunninguniversity.com/2011/01/04/merrell-trail-glove-review/">Barefoot Running University</a> and <a href="http://www.irunfar.com/2011/02/merrell-trail-glove-vs-new-balance-minimus-trail-review.html">I run far</a>.)</p>
<h3>On your foot</h3>
<p>The fit is snug in the heel and the laces really grip around the mid-foot and arch. I only have the Vibram FiveFingers and walking shoes to compare to, maybe regular running shoes are more like this too, but they are different to anything I have run in before. The toes have tonnes of room and are completely free to move, but don't slide around at all because the midfoot is held solidly.</p>
<p>They breath pretty well, a lot of the upper is mesh/fabric, the tongue's a bit thick so they're pretty warm but regular shoes+socks would be worse.</p>
<p>They let a bit of dirt in around the ankle and I think through the mesh/fabric (I wear them without socks), but because it can move around inside the shoe easily I don't think it's as annoying as when wearing Vibrams.</p>
<p>Most of my shoes are 43 or 44 (Vibrams are 42), and I got the Merrells in size 44 to make sure my toes would have room to splay and wouldn't hit the end heading downhill. This was a bit of a mistake, the way they fit means you won't slide inside them and the bit of extra length means I catch my toe on steps a bit more than I'd like. Not a show stopper, I just have to lift my feet higher.</p>
<h3>On the ground</h3>
<p>The sole has a bit of cushioning and feels a little soft on the footpath, but you don't notice it as soon as you get off road. It's much thicker and stiffer than the Vibrams, with less ground feel, but still very flexible and nothing like a normal running/trail shoe.</p>
<p>The tread has maybe 2-3mm deep chunks, bigger on the toe and heel. It's very curved and foot shaped, no attempts to control side ways stability or pronation, which is great and they are a tiny bit lighter than Vibram KSOs.</p>
<p>They feel bombproof, no fear treading on rocks/stones/sticks and I'm definitely not going to catch my little toe again like I did wearing Vibrams.</p>
<h3>Conclusion</h3>
<p>So, these are pretty much exactly what I've been waiting for. I'll only wear them on rough/rocky trails, and they're not "barefoot" by any stretch of the imagination, but they are great minimal shoes and the biggest problem I can see is that I'm going to be exhausted, the Vibrams give you an excuse to slow down occasionally "to negotiate tricky terrain" :)</p>
<p><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://3.bp.blogspot.com/-BoYD9KL6H60/TWHQedm2xhI/AAAAAAAAAHA/Yd8F_ilRymM/s1600/IMG_2745.JPG"><img style="cursor: pointer; width: 240px; height: 320px;" src="http://3.bp.blogspot.com/-BoYD9KL6H60/TWHQedm2xhI/AAAAAAAAAHA/Yd8F_ilRymM/s320/IMG_2745.JPG" alt="" id="BLOGGER_PHOTO_ID_5575967035423639058" border="0" /></a><br>Dandenong Ranges</p>Tomhttp://www.blogger.com/profile/06694547908603725128noreply@blogger.com0tag:blogger.com,1999:blog-7024729.post-22507293725885769772011-02-04T12:45:00.004+11:002011-02-04T13:15:16.979+11:00LED Bike Light update<p>My <a href="http://successlessness.blogspot.com/2007/05/led-bike-light-project.html">home built LED bike light</a> performed well for many years, but when I got my <a href="http://www.flickr.com/photos/tom-paton/4458720361/">new bike</a>, the battery holder and handlebar mounting solutions no longer functioned adequately so it was time for a redesign.</p>
<p>I had been planning on a new simplified version, eliminating the micro-controller and putting in a simple dimmer potentiometer. It turned out I never really used the battery monitoring features and simply used a multimeter to decide when to recharge, and using a dimmer would allow setting the brightness easily and quickly and also allow the "high beam" mode to remain on through hazardous sections when necessary.</p>
<p>I was still pretty keen on building this myself as all it really required was cannibalising my old light for parts and making a new case. Commercial solutions had dropped a lot in price but are still many times more expensive than DIY (a few friends have these <a href="http://www.ayup-lights.com/">AYUP lights</a> for example and they seem great).</p>
<p>Then I discovered semi-cheap LED torches as an option and ended up buying a <a href="http://www.ledtorches.com.au/index.php?act=viewProd&productId=170">Fenix LD20 LED torch</a> and a small clamp from Ebay (that I can't find again, glad I bought 2...)</p>
<p><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://3.bp.blogspot.com/_yvQpQviYWT0/TUtgrvS7ozI/AAAAAAAAAGg/8zvBwKyoAH4/s1600/2011_02_04-11_44_11.jpg"><img style="cursor:pointer; cursor:hand;width: 320px; height: 234px;" src="http://3.bp.blogspot.com/_yvQpQviYWT0/TUtgrvS7ozI/AAAAAAAAAGg/8zvBwKyoAH4/s320/2011_02_04-11_44_11.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5569651668720263986" /></a> <a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://2.bp.blogspot.com/_yvQpQviYWT0/TUtgr1K5FSI/AAAAAAAAAGo/AJYBaoLyg7s/s1600/2011_02_04-11_46_24.jpg"><img style="cursor:pointer; cursor:hand;width: 236px; height: 320px;" src="http://2.bp.blogspot.com/_yvQpQviYWT0/TUtgr1K5FSI/AAAAAAAAAGo/AJYBaoLyg7s/s320/2011_02_04-11_46_24.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5569651670297154850" /></a></p>
<p>This puts out almost exactly the same amount of light as my old version but is in a small waterproof and easy to use package. As a bonus, it's a torch I can take camping etc. too.</p>
<p>I'm simply rotating through the NiMH AA batteries from the old light and am a bit disappointed with the battery life, seeming to have to change them more often than I'd like, but that's pretty quick and an extra set of batteries isn't too much of a struggle to carry around.</p>
<p>All in all, for $80 you can't go too wrong.</p>Tomhttp://www.blogger.com/profile/06694547908603725128noreply@blogger.com1tag:blogger.com,1999:blog-7024729.post-21987698229352082462010-12-16T15:08:00.003+11:002010-12-16T15:21:53.877+11:00Preparing an audiobook for listening on an mp3 player<p>I recently ripped a 7-CD audiobook to listen to on my <a href="http://successlessness.blogspot.com/2010/07/using-sansa-clip-mp3-player-for.html">Sansa Clip+ MP3 player</a>. After a bit of fiddling around with bash I was able to end up with a collection of 183 chapters named <tt>1_01.mp3</tt> through to <tt>7_26.mp3</tt> (the first bit was the disc number, then the track number.)</p>
<p>Simply copying these onto my player didn't work as the tracks were all out of sequence. In the past, with smaller books, I've simply skipped around between the sections to listen in order, but that wasn't going to be an option here.</p>
<p>After some experimenting I discovered the Sansa orders the tracks by the Track Number in the ID3 tag, so I fired off the following bash command to set up the ID3 details appropriately:</p>
<pre>n=0 ;
for f in *.mp3 ;
do n=$((n+1)) ;
id3tag --artist="author name" --album="book name" --song="${f/.mp3/}" --track=$n $f ;
done</pre>
<p>This numbers all the tracks from 1 to 183 in order and also sets the artist and album so I can find it easily in the Audiobook list. The .mp3 is stripped from the displayed filename too.</p>
<p>The <tt>id3tag</tt> program came from the <tt>libid3-3.8.3-dev</tt> package, not sure why/when I installed that one...</p>Tomhttp://www.blogger.com/profile/06694547908603725128noreply@blogger.com0tag:blogger.com,1999:blog-7024729.post-90928559499071607092010-12-04T11:08:00.003+11:002010-12-04T11:19:10.277+11:00Python+Emacs made easy with emacs-for-python<p>I don't know where this has been all my life but Gabriele Lanaro has put together a really <a href="http://gabrielelanaro.github.com/emacs-for-python/">easy to use package for adding lots of Python goodies to Emacs</a>.</p>
<p>Just unzip the archive into <tt>~/.emacs.d/</tt> and add one line to your <tt>.emacs</tt> and your done (well, also remove all the now unnecessary random cargo-cult prior additions to your <tt>.emacs</tt>):</p>
<pre>(load-file "~/.emacs.d/emacs-for-python/epy-init.el")</pre>
<p>I had also previously installed a handful of Python/Emacs packages via Synaptic that may or may not be required to make it all work: <tt>python-rope</tt>, <tt>python-ropemacs</tt>, <tt>pyflakes</tt>.</p>Tomhttp://www.blogger.com/profile/06694547908603725128noreply@blogger.com0tag:blogger.com,1999:blog-7024729.post-33115323132254026102010-07-16T19:02:00.006+10:002011-11-04T23:13:40.801+11:00Increasing podcast tempo (playback speed) with Mplayer and Lame<p>The following commands will re-encode an mp3 file at a faster speed without increasing the pitch and making it sound like a chipmunk. <a href="http://www.surina.net/article/time-and-pitch-scaling.html">How this actually works</a> is pretty neat.</p>
<pre>mplayer -vo null -vc null \
-speed 1.33 \
-af scaletempo,volume=0,resample=44100:0:1 \
-ao pcm:fast:waveheader:file=temp.wav \
source.mp3
lame -b 64 --resample 22.05 temp.wav faster.mp3</pre>
<p>It will also set the bitrate to 64kbit which was for compatibility with my old player and to reduce the filesize for some podcasts which are unnecessarily big.</p>
<p>There should be a way to use <tt>mkfifo</tt> instead of <tt>temp.wav</tt>, and run the <tt>mplayer</tt> process and <tt>lame</tt> at the same time, I'll update this when I figure it out.</p>
<p><b>Update</b>: The above method broke after upgrading to Ubuntu 11.10 (one or both of mplayer or lame changed something I suspect...), so I swapped it out for the simpler and better <a href="http://sox.sourceforge.net/sox.html">SoX</a>:</p>
<pre>sox --show-progress --norm source.mp3 dest.mp3 \
tempo -s 1.33 channels 1 rate 22050</pre>
<p>This version is easier to understand, faster, doesn't leave need a temp file and also, as a bonus, normalizes the audio to prevent clipping and not hurt my ears.</p>Tomhttp://www.blogger.com/profile/06694547908603725128noreply@blogger.com0tag:blogger.com,1999:blog-7024729.post-26267771056421724062010-07-16T18:53:00.007+10:002010-07-27T12:24:30.852+10:00Using Sansa Clip+ MP3 Player for Podcasts<p style="float:left;margin-right:2em;margin-bottom:2em;"><a href="http://www.sandisk.com/products/sansa-music-and-video-players/sandisk-sansa-clipplus-mp3-player-.aspx" title="SanDisk Sansa Clip+"><img style="cursor:pointer; cursor:hand;width: 130px; height: 189px;" src="http://www.sandisk.com/media/292833/clipplusbig.jpg" border="0" alt="" /></a></p>
<p>A mini-review after buying the 2GB model to replace my old mp3 player. I'll only consider it for the purpose of listening to podcasts, other music related features can be found on the <a href="http://www.sandisk.com/products/sansa-music-and-video-players/sandisk-sansa-clipplus-mp3-player-.aspx">SanDisk site</a> or other reviews easily enough.</p>
<p>3 key features for listening to podcasts:</p>
<ul><li>No software required, just plug in as a USB drive and copy files across (using a script or manually.)</li>
<li>Ability to easily delete files from the player after listening to them.</li>
<li>Increase the tempo/speed of playback.</li></ul>
<p>The Sansa Clip+ does all of these, however the fast playback speed is pretty useless as the pitch is increased giving "chipmunk" effects. See the <a href="http://successlessness.blogspot.com/2010/07/increasing-podcast-tempo-playback-speed.html">next post</a> for a work-around.</p>
<p>The player remembers the position in each track, which is very nice.</p>
<p>A couple of other issues I've noticed immediately are that it is very small, making one-handed operation difficult, and it has no "hold" button to prevent inadvertent bumping of the buttons. Hopefully being able to clip it outside clothing/bags will alleviate this, time will tell.</p>
<p>It also has a built in battery so you can't carry a spare and must plug it in to charge.</p>
<p><b>Update:</b> So far, everything is working well. I'm not used to the built in battery, and waiting for it to recharge if I let it go flat is a major pain, but I'll just have to keep it topped up and an hour of charging goes a long way.</p>
<p>One other minor annoyance is that you can't quickly see the duration of a podcast when skipping through them.</p>
<p><b>Update:</b> One issue that I initially didn't notice, as the ID3 tags were stripped during my reencode process, is that the podcasts are organised by show and there is no "play all" option. This is a pain, I'm used to just listening to them in a more or less random order and not having to stop and select a new show when one finishes.</p>
<p>To quickly fix this, use the <tt>id3v2</tt> utility to delete all ID3 tags from the podcast before copying them to the player. This way they are all categorised under a single "Unknown" show menu and the filename is usually sufficient to identify the program.</p>Tomhttp://www.blogger.com/profile/06694547908603725128noreply@blogger.com0tag:blogger.com,1999:blog-7024729.post-24217494087421299232010-05-12T19:10:00.003+10:002010-05-12T19:17:47.402+10:00Making use of all the space on a hard drive<p>If you have a big data drive, there's a good chance that Linux is reserving 5% of it's available space for "emergencies".</p>
<p>This is necessary for your root filesystem as if the disk fills up there may be no way for root to log in and clean things up without this buffer of space for log files etc. to be written into.</p>
<p>However, I have a couple of large drives that store only data and 5% is 100GB I'd rather be able to use.</p>
<p>Fortunately, it's easy to reclaim the space, the following will remove all "reserved blocks":</p>
<pre>sudo tune2fs -m 0 /dev/sdXX</pre>
<p>This can be reset back to the default 5% (or any percentage) if you need to:</p>
<pre>sudo tune2fs -m 5 /dev/sdXX</pre>Tomhttp://www.blogger.com/profile/06694547908603725128noreply@blogger.com0tag:blogger.com,1999:blog-7024729.post-58667964659854288372010-01-22T20:05:00.004+11:002010-01-22T20:38:34.369+11:00iPhone woes<p>My iPhone 3GS died 2 days into our holiday. Something like an IMEI/ICCID not found error, and the only thing it would display was a screen asking to be connected to iTunes to be restored.</p>
<p>Connecting to iTunes wouldn't let me restore as it said my phone was locked with a passcode. Using the forced restore mode, iTunes wouldn't restore without an Internet connection.
Once I got a connection, the restore failed with an "Error 23" and the log file contained "radio" errors. Didn't sound good. Tried a DFU mode restore, same "Error 23".</p>
<p>So, it was bricked. My last backup was in October. My bad I guess, but I can only use iTunes via my partner's laptop and usually I don't need to. Thankfully it was still covered by the 1 year warranty.</p>
<p>Calling Apple went smoothly. They acknowledged that I'd done everything I could and booked me an appointment to take the phone into the Apple store.</p>
<p>The Apple store is crazy, there were more staff than customers and the Genius bar was booked out for 2 days. That's where your extra $ go when you buy a Mac, buying blue t-shirts...</p>
<p>The Genius replaced my handset with no troubles, which was great.</p>
<p>Restoring from my backup didn't work very well.</p>
<p>My Apps weren't restored at all, only the links to Web Apps I'd saved to the home screen were restored. This is possibly because I hadn't "authorised" iTunes with my account. Hard to recall if I was warned about that, I didn't know it was necessary in any case.</p>
<p>Restoring Apps manually is a pain, trawling through iTunes receipt emails, figuring out which I hadn't uninstalled and which I wanted to keep using.</p>
<p>Additionally, you have to buy the App again, only once that's done does it confirm that you're not going to have to pay for it again (and gives you a cancel button, God knows why.) If you choose the wrong App then bad luck, the one-click purchase will install it with no cancel button.</p>
<p>One tiny bit of good news: Many App's settings were backed up, and the settings were restored after re-purchasing the App. This wasn't clear initially, but obviously it backs up and restores all the files in the "home" folder, whether the App is present or not.</p>
<p>The music wasn't synced at all as I had iTunes set to manage it manually. Fair enough in that case, but still tedious to get it back.</p>
<p>The lesson to be learnt is that an iPhone backup isn't really a backup at all. It apparently backs up your "home" folder and relies on the Sync to do the rest. This is sort of obvious in hindsight, as it's not like there's going to be room for a 32gb backup, and theoretically that's mostly copies of files (music) that's on the computer anyway.</p>
<p>Just because I don't want everything to be automatically Synced doesn't mean I don't want it backed up.</p>
<p>So, I'll be changing my habits from now on. I won't be switching to using iTunes, that's not an option on Linux, but I'll definitely treat the iPhone as a terminal only and try to keep as much as possible "in the cloud". Using Google Sync for my contacts in addition to my mail and calendars from now on for example.</p>Tomhttp://www.blogger.com/profile/06694547908603725128noreply@blogger.com0tag:blogger.com,1999:blog-7024729.post-11328217923441755042009-09-27T18:37:00.003+10:002009-09-27T18:43:05.100+10:00Canvas Game on iPhone<p>I just ported my <a href="http://successlessness.blogspot.com/2008/09/simple-game-using-javascript-and-canvas.html">little canvas game</a> to the iPhone:</p>
<p><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://2.bp.blogspot.com/_yvQpQviYWT0/Sr8lPAssisI/AAAAAAAAAGI/aIPKrpfB0Uc/s1600-h/IMG_0455.PNG"><img style="cursor:pointer; cursor:hand;width: 267px; height: 400px;" src="http://2.bp.blogspot.com/_yvQpQviYWT0/Sr8lPAssisI/AAAAAAAAAGI/aIPKrpfB0Uc/s400/IMG_0455.PNG" border="0" alt=""id="BLOGGER_PHOTO_ID_5386064619174136514" /></a></p>
<p><a href="http://tompaton.com/canvasgame1.html">Go play!</a></p>Tomhttp://www.blogger.com/profile/06694547908603725128noreply@blogger.com0tag:blogger.com,1999:blog-7024729.post-43587708420671111792009-08-13T15:30:00.002+10:002009-08-13T15:41:23.608+10:00GPSLog Labs<p><a href="http://gpsloglabs.com/">GPSLog Labs</a> is a site I've been working on where you can upload logs from your GPS tracking device and map, graph and analyse them.</p>
<p>It will let you track your exercise and training whether your
a cyclist or runner, and can also be used to track your mileage
and keep a diary of your activity.</p>
<p>It builds on a few of the things I've <a href="http://successlessness.blogspot.com/search/label/gps">posted about here before</a>, and there's some more information on the <a href="http://blog.gpsloglabs.com">GPSLog Labs blog</a>.</p>
<p>Signing up is trivial as it supports <a href="http://openid.net">OpenID</a>, I'd appreciate any of your comments and suggestions and hope the site interests some of you.</p>Tomhttp://www.blogger.com/profile/06694547908603725128noreply@blogger.com0tag:blogger.com,1999:blog-7024729.post-77186891022139055202009-07-14T16:34:00.002+10:002009-07-14T16:48:03.912+10:00Hopeless Exetel customer service<p>I got the following message regarding a line fault issue we have with our telephone service:</p>
<blockquote>Please note that the supplier technician who has attended to your service issue has confirmed to us that there is no issue with in the infrastructure/network boundary point and or main distribution frame (MDF). Please re-check your equipment.</blockquote>
<p>That was a surprise to me, as on Saturday the technician had found a fault in the line but had been unable to pull the cable and had said that Optus would have to dig it up and we'd find out more later.</p>
<p>Calling <a href="http://www.exetel.com.au/">Exetel</a> help was a struggle, after waiting on hold and explaining my issue, the call dropped out when they seemed to put me back on hold. Twice.</p>
<p>Thoroughly pissed off I spent another 20 minutes on hold and listening to complete silence while the help desk person was "just one second" (turns out he was talking to Optus, would have been nice to be told why I was waiting).</p>
<p>So, at this point, 50 wasted minutes later, I find out that Optus is going to fix the line after all, and that the message I received was due to Exetel "closing" the initial fault ticket and was completely wrong.</p>
<p>Idiots.</p>Tomhttp://www.blogger.com/profile/06694547908603725128noreply@blogger.com0tag:blogger.com,1999:blog-7024729.post-69883521347783533542008-10-11T12:04:00.000+11:002008-10-11T12:21:58.629+11:00Merging GPS logs and mapping them all<p>Inspired by <a href="http://cabspotting.org/">cabspotting</a> and <a href="http://www.openstreetmap.org/">Open Street Map</a>, I wanted to merge all my GPS logs and create a map showing all the routes I've logged lately.</p>
<p>This is pretty easy using <a href="http://www.gpsbabel.org">gpsbabel</a>, but I needed to use a little Python to get the list of input log files. (I'm sure there's a way to do it in bash but that's beyond me for now.) My GPS stores files in nmea format, and the directory structure/purpose of my Python script should hopefully be apparent.</p>
<pre>>>> import os
>>> from path import path
>>> logs = " ".join([" ".join(["-i nmea -f %s"%log
for log in sorted((raw/"raw").files("GPS_*.log"))])
for raw in path("/home/tom/docs/gpslogs").dirs()
if raw.namebase.isdigit()])
>>> logs
'-i nmea -f /home/tom/docs/gpslogs/200810/raw/GPS_20080930_221152.log -i nmea -f /home/tom/docs/gpslogs/200810/raw/GPS_20081001_071234.log ...'
>>> os.system("gpsbabel %s -o kml,points=0,labels=0,trackdata=0 -F /home/tom/docs/gpslogs/all200810.kml" % logs)</pre>
<p>The result of that is a 36.5 MB kml file I could load into Google Earth:</p>
<p><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/_yvQpQviYWT0/SO_-6iOhgAI/AAAAAAAAAD0/Hf7AgEgTmtk/s1600-h/zoom0.jpg"><img style="cursor:pointer; cursor:hand;" src="http://1.bp.blogspot.com/_yvQpQviYWT0/SO_-6iOhgAI/AAAAAAAAAD0/Hf7AgEgTmtk/s400/zoom0.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5255699571738247170" /></a></p>
<p><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://4.bp.blogspot.com/_yvQpQviYWT0/SO__CruMyvI/AAAAAAAAAD8/CPry4aqOFj0/s1600-h/zoom1.jpg"><img style="cursor:pointer; cursor:hand;" src="http://4.bp.blogspot.com/_yvQpQviYWT0/SO__CruMyvI/AAAAAAAAAD8/CPry4aqOFj0/s400/zoom1.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5255699711725980402" /></a></p>
<p><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://2.bp.blogspot.com/_yvQpQviYWT0/SO__LebPNMI/AAAAAAAAAEE/p3et198CXEw/s1600-h/zoom2.jpg"><img style="cursor:pointer; cursor:hand;" src="http://2.bp.blogspot.com/_yvQpQviYWT0/SO__LebPNMI/AAAAAAAAAEE/p3et198CXEw/s400/zoom2.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5255699862775608514" /></a></p>
<p><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://4.bp.blogspot.com/_yvQpQviYWT0/SO__UzfTu7I/AAAAAAAAAEM/EVKOoacCF_w/s1600-h/zoom3.jpg"><img style="cursor:pointer; cursor:hand;" src="http://4.bp.blogspot.com/_yvQpQviYWT0/SO__UzfTu7I/AAAAAAAAAEM/EVKOoacCF_w/s400/zoom3.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5255700023048649650" /></a></p>
<p><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://2.bp.blogspot.com/_yvQpQviYWT0/SO__c8ClIdI/AAAAAAAAAEU/FJOIgijhriQ/s1600-h/perspective1.jpg"><img style="cursor:pointer; cursor:hand;" src="http://2.bp.blogspot.com/_yvQpQviYWT0/SO__c8ClIdI/AAAAAAAAAEU/FJOIgijhriQ/s400/perspective1.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5255700162783027666" /></a></p>
<p>There was one spurious point somewhere in the log file at 0° E, 0° N, and the log has a lot of jitter when I'm walking near home.</p>Tomhttp://www.blogger.com/profile/06694547908603725128noreply@blogger.com0tag:blogger.com,1999:blog-7024729.post-51295010118371548432008-09-18T16:01:00.008+10:002009-09-27T18:44:37.079+10:00A simple game using JavaScript and the CANVAS element<p>This should work in Opera and Firefox (though really slowly, why I don't know) and probably Safari. It's not going to work in IE because I can't be bothered getting the iecanvas thingi into the blog code.</p>
<p><b>Update: There is now an <a href="http://tompaton.com/canvasgame1.html">iPhone version</a>.</b></p>
<p>The only thing to note is less clicks == better score.</p>
<canvas id="board" width="500" height="400" style="border: 1px solid black; background-color: #808080;">Canvas not supported...</canvas>
<table width="500"><tr><td valign="top">
<div><input type="button" value="New Game" onclick="newgame('board');" style="font-size:1.2em;"></div>
<div>Clicks <input type="text" id="clicks" value="0" size="4" style="text-align:right;font-size:1em;"></div>
</td><td valign="top" align="right">
<table id="tally" border="1" cellspacing="0" style="margin:4 0px;" width="200">
<tfoot><tr><th colspan="4">0</th></tr></tfoot>
<tbody><tr><th>0</th><th>0</th><th>0</th><th>0</th></tr></tbody>
</table>
</td></tr></table>
<div id="history" style="width:492px; border:1px solid black;background-color:#888;padding:4px;margin:4 0px;"></div>
<script type="text/javascript">
function initialize(id)
{
var canvas = document.getElementById(id);
if (canvas.getContext)
{
canvas.addEventListener('click', canvas_click, false);
canvas.pieces = new Array( new Piece("#FF9900", "#B36B00", "#FFE6BF", "#FFCC80"),
new Piece("#0033CC", "#00248F", "#BFCFFF", "#809FFF"),
new Piece("#400099", "#2D006B", "#DABFFF", "#B580FF"),
new Piece("#FFE500", "#B3A000", "#FFF9BF", "#FFF280") );
var tally = document.getElementById("tally").tBodies[0];
for(var i=0; i < canvas.pieces.length; i++)
tally.rows[0].cells[i].style.backgroundColor = canvas.pieces[i].colors[3];
}
}
function newgame(id)
{
var canvas = document.getElementById(id);
if (canvas.getContext)
{
canvas.board = {
rows: 8, cols: 10, cells: new Array()
};
canvas.height = 400;
canvas.width = canvas.board.cols * canvas.height/canvas.board.rows;
Piece.prototype.sx = canvas.width / canvas.board.cols;
Piece.prototype.sy = canvas.height / canvas.board.rows;
Piece.prototype.canvasheight = canvas.height;
var ctx = canvas.getContext('2d');
for(var i=0; i < canvas.pieces.length; i++)
canvas.pieces[i].makeGrads(ctx);
for(var i=0; i < canvas.board.cols; i++)
canvas.board.cells.push(new Array(canvas.board.rows));
var pool = new Array(canvas.board.rows * canvas.board.cols);
for(var i=0; i < pool.length; i++)
pool[i] = i % canvas.pieces.length;
pool = shuffle(pool);
for(var r=0; r < canvas.board.rows; r++)
for(var c=0; c < canvas.board.cols; c++)
canvas.board.cells[c][r] = pool.pop();
var tally = draw(canvas);
updateTally(tally, document.getElementById("tally"));
document.getElementById("clicks").value = 0;
}
}
//+ Jonas Raoni Soares Silva
//@ http://jsfromhell.com/array/shuffle [v1.0]
function shuffle(o)
{ //v1.0
for(var j, x, i = o.length; i; j = parseInt(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x);
return o;
};
function canvas_click(e)
{
var canvas = this;
var pos = findPositionWithScrolling(canvas);
// pos[0] -= document.body.scrollLeft;
// pos[1] -= document.body.scrollTop;
var c = Math.floor((e.pageX-pos[0]) / canvas.pieces[0].sx),
r = Math.floor((canvas.height - (e.pageY-pos[1])) / canvas.pieces[0].sy);
if(canMove(canvas,c,r)) {
updateClicks(1);
processMove(canvas,c,r);
collapse(canvas);
var tally = draw(canvas);
updateTally(tally, document.getElementById("tally"));
}
}
// @http://www.howtocreate.co.uk/tutorials/javascript/browserspecific
function findPositionWithScrolling( oElement ) {
function getNextAncestor( oElement ) {
var actualStyle;
if( window.getComputedStyle ) {
actualStyle = getComputedStyle(oElement,null).position;
} else if( oElement.currentStyle ) {
actualStyle = oElement.currentStyle.position;
} else {
//fallback for browsers with low support - only reliable for inline styles
actualStyle = oElement.style.position;
}
if( actualStyle == 'absolute' || actualStyle == 'fixed' ) {
//the offsetParent of a fixed position element is null so it will stop
return oElement.offsetParent;
}
return oElement.parentNode;
}
if( typeof( oElement.offsetParent ) != 'undefined' ) {
var originalElement = oElement;
for( var posX = 0, posY = 0; oElement; oElement = oElement.offsetParent ) {
posX += oElement.offsetLeft;
posY += oElement.offsetTop;
}
if( !originalElement.parentNode || !originalElement.style || typeof( originalElement.scrollTop ) == 'undefined' ) {
//older browsers cannot check element scrolling
return [ posX, posY ];
}
oElement = getNextAncestor(originalElement);
while( oElement && oElement != document.body && oElement != document.documentElement ) {
posX -= oElement.scrollLeft;
posY -= oElement.scrollTop;
oElement = getNextAncestor(oElement);
}
return [ posX, posY ];
} else {
return [ oElement.x, oElement.y ];
}
}
function draw(canvas)
{
var ctx = canvas.getContext('2d');
var tally = new Array(canvas.pieces.length);
tally.connected = new Array(canvas.pieces.length);
for(var i=0; i < tally.length; i++)
{
tally[i] = 0;
tally.connected[i] = 0;
}
ctx.clearRect(0,0,canvas.width,canvas.height);
for(var r=0; r < canvas.board.rows; r++)
for(var c=0; c < canvas.board.cols; c++)
{
var p = canvas.board.cells[c][r];
if(p != null)
{
var con = getConnections(canvas, c, r);
canvas.pieces[p].draw(ctx, c, r, con);
tally[p] += 1;
tally.connected[p] += (con.left||con.up) ? 1 : 0;
}
}
return tally;
}
function updateTally(tally, table)
{
var sum = 0, con = 0;
for(var i=0; i<tally.length; i++)
{
var c = table.tBodies[0].rows[0].cells[i];
c.replaceChild(document.createTextNode(tally[i]), c.lastChild);
sum += tally[i];
con += tally.connected[i];
}
var c = table.tFoot.rows[0].cells[0];
c.replaceChild(document.createTextNode(sum), c.lastChild);
if(con > 0)
c.style.backgroundColor = "#8f8";
else {
c.style.backgroundColor = "#f44";
updateClicks(sum);
updateHistory(1*document.getElementById("clicks").value);
}
}
function Piece(color1, color2, color3, color4)
{
this.colors = [color1, color2, color3, color4];
}
Piece.prototype.makeGrads = function(ctx)
{
function addColorStops(p, g) {
g.addColorStop(0, p.colors[2]);
g.addColorStop(0.1, p.colors[3]);
g.addColorStop(0.8, p.colors[0]);
g.addColorStop(1, p.colors[1]);
}
this.gradient = ctx.createRadialGradient(this.sx/3,this.sx/3,this.sx/12,this.sx/2,this.sy/2,0.9*this.sx/2);
addColorStops(this, this.gradient);
this.gradienth = ctx.createLinearGradient(0,this.sy/4,0,3*this.sy/4);
addColorStops(this, this.gradienth);
this.gradientv = ctx.createLinearGradient(this.sx/4,0,3*this.sx/4,0);
addColorStops(this, this.gradientv);
}
Piece.prototype.draw = function(ctx, x, y, connected)
{
ctx.save();
ctx.translate(x*this.sx, this.canvasheight-(y+1)*this.sy);
ctx.globalCompositeOperation = "destination-over";
if(connected.left)
{
ctx.fillStyle = this.gradienth;
ctx.fillRect(-this.sx/2,this.sy/2-this.sy/4,this.sx,this.sy/2);
}
if(connected.up)
{
ctx.fillStyle = this.gradientv;
ctx.fillRect(this.sx/2-this.sx/4,this.sy/2,this.sx/2,this.sy);
}
if(connected.block)
{
ctx.fillStyle = this.colors[1];
ctx.fillRect(-this.sx/3,this.sy-this.sy/3,2*this.sx/3,2*this.sy/3);
}
ctx.fillStyle = this.gradient;
ctx.globalCompositeOperation = "source-over";
ctx.beginPath();
ctx.arc(this.sx/2,this.sy/2,0.9*this.sx/2,0,Math.PI*2,true);
ctx.fill();
ctx.restore();
}
function getConnections(canvas, c, r)
{
var up = false, left = false, block = false;
if(c>0 && canvas.board.cells[c][r]==canvas.board.cells[c-1][r]) left = true;
if(r>0 && canvas.board.cells[c][r]==canvas.board.cells[c][r-1]) up = true;
if(up && left && canvas.board.cells[c][r]==canvas.board.cells[c-1][r-1]) block = true;
return { up: up, left: left, block: block };
}
function canMove(canvas, c, r)
{
var p = canvas.board.cells[c][r];
if(p == null) return false;
return (c > 0 && canvas.board.cells[c-1][r]==p
|| r > 0 && canvas.board.cells[c][r-1]==p
|| c+1 < canvas.board.cols && canvas.board.cells[c+1][r]==p
|| r+1 < canvas.board.rows && canvas.board.cells[c][r+1]==p);
}
function processMove(canvas, c, r)
{
var p = canvas.board.cells[c][r];
canvas.board.cells[c][r] = null;
if(c > 0 && canvas.board.cells[c-1][r]==p) processMove(canvas, c-1, r);
if(r > 0 && canvas.board.cells[c][r-1]==p) processMove(canvas, c, r-1);
if(c+1 < canvas.board.cols && canvas.board.cells[c+1][r]==p) processMove(canvas, c+1, r);
if(r+1 < canvas.board.rows && canvas.board.cells[c][r+1]==p) processMove(canvas, c, r+1);
}
function collapse(canvas)
{
for(var c=0; c < canvas.board.cols; c++)
{
for(var r=0, b=0; r < canvas.board.rows; r++)
if(canvas.board.cells[c][r] != null)
canvas.board.cells[c][b++] = canvas.board.cells[c][r];
for(;b < canvas.board.rows;b++)
canvas.board.cells[c][b] = null;
}
for(var c=0, b=0; c < canvas.board.cols; c++)
if(canvas.board.cells[c][0] != null)
{
for(var r=0; r < canvas.board.rows; r++)
canvas.board.cells[b][r] = canvas.board.cells[c][r];
b++;
}
for(;b < canvas.board.cols;b++)
for(var r=0; r < canvas.board.rows; r++)
canvas.board.cells[b][r] = null;
}
function updateClicks(clicks)
{
document.getElementById("clicks").value = 1*document.getElementById("clicks").value + clicks;
}
function updateHistory(clicks)
{
var hist = document.getElementById("history");
var row = document.createElement("div");
row.style.fontSize = "0.8em";
row.style.border = "1px solid gray";
row.style.lineHeight = "1em; overflow: hidden";
row.style.width = 0.25*clicks+"em";
row.appendChild(document.createTextNode(clicks));
hist.insertBefore(row, hist.firstChild);
// update history colours
var min = 1000, max = 0, sum = 0;
for(var i=0; i < hist.childNodes.length; i++)
{
var s = 1*hist.childNodes[i].textContent;
min = min < s ? min : s;
max = max > s ? max : s;
sum += s;
}
var avg = sum/hist.childNodes.length;
for(var i=0; i < hist.childNodes.length; i++)
{
var s = 1*hist.childNodes[i].textContent;
if(s>=avg) {
var p = (s-avg)/(1+max-avg);
hist.childNodes[i].style.backgroundColor = "rgb(255,"+Math.floor(255*(1-p))+",0)";
} else {
var p = (s-min)/(1+avg-min);
hist.childNodes[i].style.backgroundColor = "rgb("+Math.floor(255*p)+",255,0)";
}
}
}
initialize('board'); newgame('board');
</script>Tomhttp://www.blogger.com/profile/06694547908603725128noreply@blogger.com1tag:blogger.com,1999:blog-7024729.post-87169942970462298542008-08-25T18:56:00.003+10:002008-08-25T20:38:46.020+10:00Analysing GPS Logs with Awk<p>This post describes the first two "chop" functions that fit into the <a href="http://successlessness.blogspot.com/2008/08/automatically-partitioning-gps-logs.html">partitioning framework outlined last post</a>.</p>
<pre>def chopToSpeedHistogram(dest, p):
# create histogram of speeds from nmea written to stdout
os.system("cat "+sh_escape(dest)+".log"
+ " | awk -F , '{if($1==\"$GPVTG\" && int($8)!=0){count[int($8+0.5)]++}}"
+ " END {for(w in count) printf(\"[%d,%d],\\n\", w, count[w]);}'"
# sort it
+ " | sort -g -k 1.2"
# output json of histogram
+ " > "+sh_escape(dest)+".hist")
def chopToHeadingHistogram(dest, p):
# create histogram of headings from nmea written to stdout (ignore heading when stopped)
os.system("cat "+sh_escape(dest)+".log"
+ " | awk -F , '{if($1==\"$GPVTG\" && int($8)!=0){count[5.0*int($2/5.0+0.5)]++;}}"
+ " END {for(w in count) printf(\"[%d,%d],\\n\", w, count[w]);}'"
# sort it
+ " | sort -g -k 1.2"
# output json of histogram
+ " > "+sh_escape(dest)+".head")</pre>
<p>Both functions use awk to create a histogram from the speed (in km/h) and heading (or bearing, in degrees) from the <a href="http://www.gpsinformation.org/dale/nmea.htm#VTG">NMEA VTG</a> sentences. The speed is rounded to an integer, and the bearing to the nearest 5 degrees. The data logger records on reading per second, so this gives a measure of how much time was spent at each speed/bearing.</p>
<p>The histogram is output in a "json" array format that can be inserted straight into a webpage where the <a href="http://code.google.com/p/flot/">flot</a> library is used to generate some graphs.</p>
<h4>Speed Histogram</h4>
<p><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/_yvQpQviYWT0/SLKEQ87J2UI/AAAAAAAAADs/BKJS4AM0b1s/s1600-h/speed-histogram-1.png"><img style="cursor:pointer; cursor:hand;" src="http://1.bp.blogspot.com/_yvQpQviYWT0/SLKEQ87J2UI/AAAAAAAAADs/BKJS4AM0b1s/s400/speed-histogram-1.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5238394743352318274" /></a></p>
<p>The average and standard deviation (shaded at ±0.5σ) are indicated on the graph for two bike rides along the same route, and match pretty closely with that recorded by my bike computer:</p>
<table border="1" cellspacing="0">
<tr><th></th><th colspan="3">GPS log</th><th colspan="3">Bike computer</th></tr>
<tr><th>Ride 1 (brown)</th>
<td>2hrs 59 min, minus 41 min stopped</td><td>63.4km</td><td>27.7km/h</td>
<td>2 hrs 16 min</td><td>64.01km</td><td>28.00km/h</td></tr>
<tr><th>Ride 2 (dark green)</th>
<td>2 hrs 25 min, minus 13 min stopped</td><td>63.4km</td><td>29.0km/h</td>
<td>2 hrs 10 min</td><td>63.85km</td><td>29.30km/h</td></tr>
</table>
<p>The two rides went in different directions, the first in the "uphill" direction and the second with a bit of a tail wind. I got a flat tire on the first ride too, hence the extra time spent stopped.</p>
<h4>Heading Histogram</h4>
<p><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://4.bp.blogspot.com/_yvQpQviYWT0/SLKEBd-q2QI/AAAAAAAAADc/9kFtoCEJHi8/s1600-h/heading-histogram-1.png"><img style="cursor:pointer; cursor:hand;" src="http://4.bp.blogspot.com/_yvQpQviYWT0/SLKEBd-q2QI/AAAAAAAAADc/9kFtoCEJHi8/s400/heading-histogram-1.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5238394477347526914" /></a></p>
<p>Up is north and the radius represents the time spent heading in that direction (normalized during the plotting process and "expanded" by taking the square root to show a little more detail.)</p>Tomhttp://www.blogger.com/profile/06694547908603725128noreply@blogger.com0tag:blogger.com,1999:blog-7024729.post-9838175265121570562008-08-21T14:47:00.002+10:002008-08-21T16:30:12.971+10:00Automatically Partitioning GPS Logs with gpsbabel<p>My <a href="http://successlessness.blogspot.com/2008/08/amod-agl3080-gps-data-logger.html">GPS logger</a> is capturing lots of useful information but it's difficult to efficiently capture data for regular activities. Geotagging photos is easy, and manually working with the logs for a special event is possible, but it's not feasible to put in that much work to analyze commutes for example.</p>
<p>The logger creates a separate log file each time it's switched on and off, and while these logs could be sorted into categories for analysis, it's easy to forget to turn it on and off at the start and end of a section of interest and activities are then merged in the logs. In addition, there is often "junk" data at start and end of logs while leaving or arriving at a destination.</p>
<p>I wanted to be able to automatically capture the information about my daily activities by simply switching on the logger and carrying it around with me. I then simply want to plug the logger into the computer and have the logs automatically chopped into segments of interest that can be compared to each other over time.</p>
<p>The rest of this post roughly outlines the Python script I created to perform this task, minus some of the hopefully irrelevant details.</p>
<p>Firstly, I collect the lat/long coordinates of places that I am interested in collecting data while I'm there and traveling between them. These include my home, work, the climbing gym and so on. Each point has a radius within which any readings will be considered to be in that place.</p>
<pre># id: name lat long radius
places = { 1: ("A", -37.123456, 145.123456, 0.050),
2: ("B", -37.234567, 145.234567, 0.050),
3: ("C", -37.345678, 145.345678, 0.050) }
otherid = 4</pre>
<p>For each of these places of interest, I then use <a href="http://www.gpsbabel.org/">gpsbabel</a>'s <a href="http://www.gpsbabel.org/htmldoc-1.3.3/filter_radius.html">radius filter</a> to find all the times where I was within that zone:</p>
<pre># create a list of all raw log files to be processed
from path import path
month = path("/gpslogs/200808")
logs = " ".join(["-i nmea -f %s"%log
for log in sorted((month/"raw").files("GPS_*.log"))])
for (id,(place,lat,lon,radius)) in places.items():
os.system("gpsbabel "
# input files
+ logs
# convert to waypoints
+ " -x transform,wpt=trk,del"
# remove anything outside place of interest
+ (" -x radius,distance=%.3fK,lat=%.6f,lon=%.6f,nosort"%(radius,lat,lon))
# convert back to tracks
+ " -x transform,trk=wpt,del"
# output nmea to stdout
+ " -o nmea -F -"
# filter to just GPRMC sentences
+ " | grep GPRMC"
# output to log file
+ (" > %s/processed/place%d.log"%(month,id)))</pre>
<p>And all points outside any of the specific places of interest are sent into an "other" file:</p>
<pre>os.system("gpsbabel "
# input files
+ logs
# convert to waypoints
+ " -x transform,wpt=trk,del"
# remove anything in a place of interest
+ "".join([" -x radius,distance=%.3fK,lat=%.6f,lon=%.6f,nosort,exclude"%(radius,lat,lon)
for (id,(place,lat,lon,radius)) in places.items()])
# convert back to tracks
+ " -x transform,trk=wpt,del"
# output nmea to stdout
+ " -o nmea -F -"
# filter to just GPRMC sentences
+ " | grep GPRMC"
# output to log file
+ (" > %s/processed/place%d.log" % (month, otherid)))</pre>
<p>These files are filtered with grep to contain only minimal data as we only require the timestamps for this part of the process. Specifically only the <a href="http://www.gpsinformation.org/dale/nmea.htm#RMC">NMEA GPRMC</a> sentences are kept.</p>
<p>To provide a brief illustration, the following picture shows two log files of data, a blue and a green, between three points of interest:</p>
<p><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/_yvQpQviYWT0/SKz007zje4I/AAAAAAAAADU/UwQP80k_m7Q/s1600-h/partition+diagram+1a.png"><img style="cursor: pointer;" src="http://1.bp.blogspot.com/_yvQpQviYWT0/SKz007zje4I/AAAAAAAAADU/UwQP80k_m7Q/s400/partition+diagram+1a.png" alt="" id="BLOGGER_PHOTO_ID_5236829656969345922" border="0" /></a></p>
<p>The above process would create four files, one for each point A, B and C and one for "Other" points that would contain something like the following information, where the horizontal axis represents time:</p>
<p><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://4.bp.blogspot.com/_yvQpQviYWT0/SKz0LRQQZBI/AAAAAAAAADM/Z4frACNyxwA/s1600-h/partition+diagram+1b.png"><img style="cursor: pointer;" src="http://4.bp.blogspot.com/_yvQpQviYWT0/SKz0LRQQZBI/AAAAAAAAADM/Z4frACNyxwA/s400/partition+diagram+1b.png" alt="" id="BLOGGER_PHOTO_ID_5236828941172368402" border="0" /></a></p>
<p>I then read all those log files back in to create a "time line" that for each timestamp stores my "location" in the sense that it knows whether I was "home", at "work" or somewhere between the two.</p>
<pre># dict of timestamp (seconds since epoch, UTC) to placeid
where = {}
for placeid in places.keys()+[otherid,]:
for line in (month/"processed"/("place%d.log"%placeid)).lines():
fields = line.split(",")
# convert date/time to seconds since epoch (UTC)
t, d = fields[1], fields[-3]
ts = calendar.timegm( (2000+int(d[4:6]), int(d[2:4]), int(d[0:2]),
int(t[0:2]), int(t[2:4]), int(t[4:6])) )
where[ts] = placeid</pre>
<p>This is then summarised from one value per second to a list of "segments" with a start and end time and a location. Unlogged time segments are also inserted at this point whenever there are no logged readings for 5 minutes or more.</p>
<pre># array of tuples (placeid, start, end, logged)
# placeid = 0 indicates "unknown location", i.e. unlogged
summary = []
current, start, stop, last_ts = 0, 0, 0, None
for ts in sorted(where.keys()):
# detect and insert "gaps" if space between logged timestamps is greater than 5 minutes
if last_ts and ts-last_ts > 5*60:
if current:
summary.append( [current, start, stop, True] )
current, start, stop = where[ts], ts, ts
summary.append( [0, last_ts, ts, False] )
last_ts = ts
if where[ts] != current:
if current:
summary.append( [current, start, stop, True] )
current, start, stop = where[ts], ts, ts
else:
stop = ts
summary.append( [current, start, stop, True] )</pre>
<p>(If there's a more "Pythonic" way of writing that kind of code, I'd be interested in knowing it.)</p>
<p>"Spurious" segments are then removed. These show up because when the logger is inside buildings the location jumps around and often out of the 50m radius meaning that, for example, there will be a sequence of Home-Other-Home-Other-Home logs. The "Other" segments that are between two known points of interest and less than 5 minutes long are deleted, as are "Other" segments that sit between a known place of interest and an unlogged segment.</p>
<p>Based on the above graphic, the summary might look something like the following:</p>
<table cellspacing=0 border=1><thead><tr><th>start</th><th>end</th><th>location</th></tr></thead>
<tbody>
<tr><td>10.00am</td><td>10.05am</td><td>A</td></tr>
<tr><td>10.05am</td><td>10.30am</td><td>Other</td></tr>
<tr><td>10.30am</td><td>10.35am</td><td>B</td></tr>
<tr><td>10.35am</td><td>11.00am</td><td>Other</td></tr>
<tr><td colspan=3>...</td></tr>
</tbody>
</table>
<p>The "Other" segments are then labelled if possible to indicate they were "commutes" between known locations:</p>
<table cellspacing=0 border=1><thead><tr><th>start</th><th>end</th><th>location</th></tr></thead>
<tbody>
<tr><td>10.00am</td><td>10.05am</td><td>A</td></tr>
<tr><td>10.05am</td><td>10.30am</td><td>A-B</td></tr>
<tr><td>10.30am</td><td>10.35am</td><td>B</td></tr>
<tr><td>10.35am</td><td>11.00am</td><td>B-C</td></tr>
<tr><td colspan=3>...</td></tr>
</tbody>
</table>
<p>Some segments cannot be labeled automatically and are left as "Other". This may be a trip out to a "one-off" location and back again, which can be left as "Other". However, sometimes it is because the logger didn't lock onto the satellites within the 50m radius on the way out of a place of interest and these can be manually fixed up later.</p>
<p>Once a list of "activities" has been obtained, with start and end times, it is easy to use gpsbabel again to split logs based on start and end of time segments:</p>
<pre>for (place, start, stop, place_from, place_to, logged) in summary:
dest = month / "processed" / ("%s-%s"%(time.strftime("%Y%m%d%H%M%S", time.localtime(start)),
time.strftime("%Y%m%d%H%M%S", time.localtime(stop))))
for (ext, chopFn) in [(".log", chopToLog),
(".kml", chopToKml),
(".speed", chopToSpeedVsDistance),
(".alt", chopToAltitudeVsDistance),
(".hist", chopToSpeedHistogram),
(".head", chopToHeadingHistogram),
(".stops", chopToStopsVsDistance)]:
if not (dest+ext).exists():
chopFn(dest, locals())
# make the file in case it was empty and not created
(dest+ext).touch()</pre>
<p>This generates a bunch of files for each segment, named with the start and end timestamps of the segment and an extension depending on the content. The first "chop" function generates an NMEA format log file that is then processed further by the remaining "chop" functions. The other chop functions will probably be explained in a later post, the first two are:</p>
<pre>def chopToLog(dest, p):
# filter input file entries within times of interest to temp file
os.system("gpsbabel " + p["logs"]
+ (" -x track,merge,start=%s,stop=%s"
% (time.strftime("%Y%m%d%H%M%S", time.gmtime(p["start"])),
time.strftime("%Y%m%d%H%M%S", time.gmtime(p["stop"]))))
+ " -o nmea -F "+sh_escape(dest)+".log")
def chopToKml(dest, p):
# create kml file with reduced resolution
os.system("gpsbabel -i nmea -f "+sh_escape(dest)+".log"
+ " -x simplify,error=0.01k"
+ " -o kml -F "+sh_escape(dest)+".kml")
def sh_escape(p):
return p.replace("(","\\(").replace(")","\\)").replace(" ","\\ ")</pre>
<p>(Again, if there's a better way to handle escaping special characters in shell commands, I would like to know it.)</p>
<p>Using this, I can simply plug in the logger, which launches an autorun script, and the end result are nicely segmented log files that I can map and graph. More about that in another post.</p>Tomhttp://www.blogger.com/profile/06694547908603725128noreply@blogger.com0