Desert RATS 2020 Cancelled

Oh, rats!

Due to concerns about the spread of COVID-19, the 2020 edition of the Desert RATS Classic is cancelled.

Tweaks, Round 1

Problems solved. Well, some of them, at least.

above horsethief bench

Last Sunday, I took the bike out for a shakedown ride on some trails around Navajo Rocks. During the ride, I identified a list of things to address. I addressed some of the issues and went for another ride on it yesterday.

32T chainring

Here's where we stand:

  1. Rear hub: the hub uses sealed cartridge bearings which appear to spin smoothly. This is a very cheap hub and the freehub is not removable from the hub without special tools. I do have a spare Shimano hub that I could swap in—the sizes are similar enough that I should be able to use the spokes that I have. However, I didn't notice too much noise from the freehub during yesterday's ride, so I don't think this is anything urgent.
  2. Seatpost clamp: I added the bolt-on seatpost clamp and this seems to have helped. I still saw a little bit of slippage so I'll clean off the grease and keep an eye on it.
  3. Brake lever reach: I reduced the reach on the brake levers a little. I still need to play with the angle of the levers more to find the best place. Normally I have them pretty far down but when I'm descending they're a little awkward to reach (and that's when I need them most!).
  4. Viscoset: I haven't done anything with this yet.
  5. 180mm rear brake rotor: I installed a cheapo 180mm rotor that I had sitting around. It doesn't work well as the front. I have a Shimano 180mm rotor on the way.
  6. 32T chainring: Installed. Helps on the climbs. It's too low for most cases but perfect for those few times you need it.
  7. Tire pressures: I set the pressures to 20 psi front and 25 psi rear. This was a significant improvement and felt pretty good for the terrain on the trails I was riding yesterday. I added 1 psi to each tire and will try that for the next time out.

seatpost clamp

I'm hoping to get out on this bike again tomorrow or Friday.

Initial Shakedown

Well, I didn't die this time.

Yesterday, I finished reassembling the bicycle after powder coating; this afternoon I took it out for a shakedown ride around the 17-mile main loop at the Navajo Rocks area near Moab, Utah.

shakedown portrait

Overall things went pretty well, but a few things need attention:

  1. The rear hub sometimes makes an unpleasant sound when coasting. It sounds like the freehub needs some grease, so I'll have to pull it off and take a look.
  2. My seatpost slowly slips downwards. I'm currently using a quick-release clamp but I have a non-QR clamp on the way. This should be more secure as it does not have a plastic piece used as part of the clamp. If it continues to slip after that then I'll try some carbon paste or a soda can shim.
  3. My brake levers need a reach adjustment. The ESI Chunky grips are a little thicker than the old grips and the levers would be a little more comfortable to use if they were a little closer.
  4. The Viscoset is set up in a "mid-tune" configuration, meaning that there is room to increase the damping effect. During yesterday's ride it was essentially unnoticeable. After reading some other user comments about the Viscoset, I think I'll reconfigure it for maximum damping and try it that way.
  5. I'm thinking a 180mm rotor on the back might be a good idea.
  6. I could use a little lower gearing for the steep steep stuff, that 32T chainring sitting next to me is looking pretty attractive.
  7. Finally, I need to play with tire pressures a bit. Knowing that these are pretty cheap tires, I erred on the high side.

sunlit mesas

I'll try to address these issues in the next day or two and then give it another go.

Reassembly

Unlike Humpty Dumpty, it's put back together.

Yesterday, I got the frame back from the powder coating shop and I began reassembly by installing a new headset. Today, I reassembled the rest of the bike. Most of these components were already on the bike before powder-coating, so it all went back together without any trouble.

reassembled

Here's a summary of the components I have on the bike at this time:

Component Description
Frame 2013 Trek Marlin, aluminum, 19" size
Fork Manitou Markhor, 120mm travel
Headset Cane Creek Viscoset upper and 40-Series lower
Drivetrain SRAM NX Eagle, 34T chainring, 11-50 12-speed cassette
Pedals Crankbrothers Double Shot 1
Wheels Novatec D481SBT/D482TSBT hubs, WTB ST i25 rims
Tires Continental RaceKing ShieldWall 29x2.2, Muc-Off sealant
Handlebar FIFTY-FIFTY 35mm riser bar, cut to 740mm width
Stem Race Face Respond, 60mm length, 10° rise
Grips ESI Chunky silicone MTB grips
Brakes Shimano Deore M6000, 180mm/160mm
Seatpost Brand-X Ascend II 105mm dropper seatpost

crankset closeup

I've ridden the bike a lot with most of these components, but the handlebars and pedals are new—tomorrow afternoon I've got a ride in mind to see how everything feels.

Powder Coating

Happy Valentine's Day!

As I wrote in my previous post, I'm having my bicycle frame refinished. This is purely cosmetic; the paint on the frame was pretty scratched up and worn through to the metal in a few places. Powder coating will give the frame a nice durable finish.

I dropped off the frame at Pro Powder Coating earlier this week to have it sandblasted and powder coated in satin black, and this afternoon I got a call that it was finished.

powder-coated frame

It came out great; the folks at Pro Powder Coating know what they're doing and it shows. Everything was masked properly and I didn't have to sand off anything or chase any threads. Overall, it was well worth the relatively low cost compared to the time and tedium it would take me to properly sand and repaint the frame myself. And I hate painting.

powder-coated frame closeup

I also installed a new headset into the frame. The headset is basically the bearings that support the front fork securely in the frame. I chose to use a Cane Creek Viscoset headset.

viscoset headset top

The Viscoset provides a small amount of damping to steering inputs in order to help make the bike more stable, especially targeting uncontrolled oscillations, or "speed wobble", that can pop up at higher speeds. Although the Viscoset was originally designed for use with e-bikes, it turns out to be well-suited for all kinds of bikes. Bikepacking.com has a great in-depth review of the Viscoset.

Tomorrow I'll assemble the rest of the components onto the frame and get it ready for a nice Sunday afternoon ride.

Frame Modifications

Let's lose those cable stops.

For purely cosmetic reasons, I'm having the bike frame sandblasted and powder coated next week. But before it goes off to the shop, I'm reaming out the cable stops on the frame.

Many bikes used to have mostly exposed shifter cables. This does save a little weight (and probably cost), but leaves the cables exposed to the elements. The older 8-speed rear derailleur that was on the bike was quite tolerant of some extra cable friction, but not so the newer 12-speed rear derailleur that I will be using.

By reaming out the cable stops, I can use a continuous length of cable housing from the shifter to the derailleur. This will better keep the cable protected from grit and corrosion and the derailleur shifting precisely. I used a small power rotary tool, but some small files can also get the job done—the frame is made of aluminum.

Here you can see one of the enlarged cable stops and how the cable housing fits right through it now.

enlarged cable stop

Now the frame is ready for powder coating.

What am I riding?

I purchased this Trek Marlin—an entry-level hardtail mountain bike—from my local bike shop back in 2013. A few years back, I replaced the flat handlebars with a cheap riser bar, and the long original stem with a short stem in order to make the bike more comfortable for me to ride around town.

Ian's bike, summer 2019

Since starting to ride regularly again last summer, I've replaced just about all of the components on the bike, upgrading to modern mountain bike components—the only original part left is the frame. I've mostly been riding this bike on the roads and bike paths with a set of gravel tires so far this winter.

Ian's bike, January 2020

Now the frame has been refinished and the bike reassembled with a set of XC mountain bike tires, so I can prepare myself and the bike for the Desert RATS Classic mountain bike race in May 2020.

Ian's bike, February 2020

Bike Build Post Index

Here We Go Again

What have I gotten myself into?

I grew up riding a bicycle as a child. I vividly remember the first day I got my own 10-speed mountain bike for my birthday: it didn't go well. I ran into a big dog and went over the bars into the ditch on the side of the road. From that point, however, things improved.

I frequently rode my bike to school, and in the summer I would ride all over to meet up with my friends. We would take family day trips to some local rail trails to spend the day riding down by the ocean. But after high school, I stopped riding regularly.

Fast forward 17 years: last June my wife and I decided to take up mountain biking. Living in Grand Junction, Colorado, there are many trails nearby, so it was pretty easy to get out a few times a week and work up a sweat. Of course, summers are really hot, so I didn't start riding regularly until late September.

Looking ahead down the trail

By now, both my fitness level and bike handling have improved a lot—easy when you start from zero—and I figured it was time to set a goal. So on May 9th, 2020, I'll be competing in the Desert RATS Classic mountain bike race.

The Desert RATS Classic is a 50 kilometer (31-mile) course run on some local roads and trails (primarily the Edge Loop and Sarlacc) that involves a lot of climbing. There's a 5-hour time limit, so I'll need to average about 6.2 mph in order to be an official finisher.

I've never been a particularly athletic person, so I have a lot of work to do. Over the next few months, I'll be posting about my bike setup, my training and fitness, and fun rides.

Bike standing at trail intersection

Functional VGL List Class

The scripting language used inside of SampleManager, VGL, has basic multidimensional array datatypes for fixed-length and variable-length arrays. In addition to the usual array accessor, there are some routines in the core STD_ARRAY library to do some basic manipulation of arrays. Most of the names are self-explanatory:

  • array_copy
  • array_element_exists
  • array_get_dimensions
  • array_insert_slice
  • array_remove_slice
  • array_sort
  • array_complex_sort (for multidimensional arrays)

Over time, using modern languages like C# or JavaScript, I've become accustomed to having a more fully-featured Array datatype, so I set out to create something to make my life a little easier in VGL. I implemented a 1-dimensional LIST class, using the JavaScript Array object as a model for which actions I implemented.

API Summary

Here's an summary of the LIST API:

Create a new list

JOIN LIBRARY lib_list

DECLARE list

lib_list_define_list_class()
CREATE OBJECT LIST_CLASS, list

As with many VGL class-oriented libraries, a routine to define the class needs to be called before the class itself is available for instantiation.

Add elements to the list

                       { list contents                              }
list.append(1)         { [1]                                        }
list.append(2)         { [1  2]                                     }
list.push(3)           { [1  2  3]                                  }
list.unshift(4)        { [4  1  2  3]                               }

The append and push actions are identical, adding a new element to the end of the list. The unshift action adds a new element to the beginning of the list.

Access elements of the list by index

                       { list contains: [4  1  2  3]                }
list.get(3)            { action returns: 2                          }
list.set(3, 5)         { list contains: [4  1  5  3]                }

Removing elements of the list

                       { list contents                              }
                       { [4  1  5  3]                               }
list.remove(1)         { [1  5  3]                                  }
list.pop()             { [1  5]      pop() returns 3                }
list.shift()           { [5]         shift() returns 1              }

Corresponding to unshift and push, shift and pop remove and return the first and last elements of the list, respectively. The remove action returns the list object rather than the element that was removed.

Order elements of the list

                       { list contents                              }
                       { [1  2  3  4]                               }
list.reverse()         { [4  3  2  1]                               }

There is a reverse action to reverse the order of the elements in the list, but no sort action.

Slice and splice

                       { list contains: [a  b  c  d  e]             }
list.slice(2, 4)       { action returns: [b  c]                     }
list.slice(-3, EMPTY)  { action returns: [c  d  e]                  }
list.slice(EMPTY, -2)  { action returns: [a  b  c]                  }

The slice action is used to copy a range of elements into a new list without modfying the original list.

                       { list contains: [a  b  c  d  e  f  g]       }
list.splice(2, 4)      { action returns: [b  c  d  e]               }
                       { list now contains: [a  b  f  g]            }

The splice action is used to return a range of elements from the original list, removing them from the original list. This differs from the JavaScript Array.splice method in that it cannot be used to insert or replace existing elements.

Filter

                       { list contains: [a  b  b  c  d  a]          }
list.distinct()        { action returns: [a  b  c  d]               }

The distinct action returns a new list with all of the distinct elements in the original list. Elements appear in the returned list in the order in which they first appear in the original list. The original list is not modified.

Bounds

                       { list contains: [a  b  c  a  d]             }
list.inBounds(0)       { action returns: FALSE                      }
list.inBounds(3)       { action returns: TRUE                       }
list.inBounds(6)       { action returns: FALSE                      }

The inBounds action returns TRUE if the given index is contained within the list. Remember, list indexes are 1-based in order to be consistent with VGL array indexes.

                       { list contains: [a  b  c  a  d]             }
list.length            { statement value: 5                         }

There is also a length property containing the number of elements in the list. It should not be modified from outside the class actions, but it can be read.

Contents

                       { list contains: [a  b  c  a  d]             }
list.indexOf(a)        { action returns: 1                          }
list.indexOf(c)        { action returns: 3                          }
list.indexOf(e)        { action returns: 0                          }

list.lastIndexOf(a)    { action returns: 4                          }
list.lastIndexOf(c)    { action returns: 3                          }

list.includes(a)       { action returns: TRUE                       }
list.includes(e)       { action returns: FALSE                      }

The indexOf and lastIndexOf return the index of the first or last (respectively) occurrence of a given element in the list. If the element is not found in the list, the action returns 0. The includes action returns TRUE if the given element is included in the list—it is implemented by returning TRUE if indexOf is greater than 0.

Chaining

                       { list contains: [1  2  3  4]                }
list.push(5).unshift(6).remove(2).pop()
                       { action returns: 5                          }
                       { list now contains [6  2  3  4]             }

Many actions that result in the list being modified will return a reference to the modified list. This allows multiple actions to be chained together in the same statement.

Output

                       { list contains: [1  2  3]                   }
list.join(", ")        { action returns: "1, 2, 3"                  }
list.join("|")         { action returns: "1|2|3"                    }
list.join(" and ")     { action returns: "1 and 2 and 3"            }
list.toString()        { action returns: "[1,2,3]"                  }

The join action returns a string containing the list elements delimited by the specified string. The toString action returns a human-readable string containing the contents of the list suitable for logging or debugging purposes.

GitHub Repository

I have created a GitHub repository for this library and released it under the MIT License, which makes it easy to use in your own projects. If you have any problems with the library or suggestions on how it could be extended or improved, please create an issue or submit a pull request so that they can be tracked effectively.

If you find this useful and end up using it in a project, I'd love to know about it.

Sold: 2009 KTM 505 XC-F Rally

The rally bike is sold!

You can read a little bit about the history of the bike over here. Right now, it's set up like I had it at the 2019 Sandblast Rally.

side view There are a bunch more photos here.

Highlights

cockpit view There are a bunch more photos here.

Recent Maintenance

  • January 2019
    • Rally Moto Kit fairing replacement
    • new lithium battery installed
    • changed oil and filter with Motorex synthetic 10W-50 and K&N filter
    • switched to 15T front sprocket
  • February 2019
    • new tires (Shinko 546) and HD tubes
    • new air filter installed
    • new countershaft seal installed
    • new countershaft bolt kit installed
    • new seat bolt kit installed

action shot There are a bunch more photos here.

I've owned this bike since 2014. I raced it in the 2019 Sandblast Rally (bike prep details) at the beginning of March and it ran flawlessly. Before that, I mostly just used it to putt around on trails and do some light dual-sporting. Although it has served me reliably, it is really built to be a desert rally racer and isn't at home as a dual-sport or trail bike. I am more of a slow trail rider so I think it's time for the 505 to move on to a new home.

The bike has a license plate and is street-legal with a normal Kentucky street title in my name.

The rally bike has been sold; thanks to everyone who was interested.

left side front right side front right side rear left side rear cockpit There are a bunch more photos here.

Debriefing

sandblast 2019 start

Now that the rally is finished, I want to recap some of what worked and what I could do differently next time.

sandblast 2019 SS5 start

What Worked

  1. The rally racing clinic on the Thursday before the race was really beneficial. It was a good introduction to the terrain that we would be racing on, and a good general refresher on riding off-road. While I have a lot of off-road riding under my belt, I have not ridden a lot over the last year. Also, the clinic did a great job explaining the logistics of rally racing and how the timing system works. On race day I felt pretty confident that I knew what I was doing.

  2. The bike performed flawlessly. Although I did damage a fuel line during the rally racing clinic on Thursday, I was able to easily repair it at lunch and continue without any incidents on race day. The bike didn't miss a beat despite my complete lack of mechanical sympathy. Bonus: no flat tires.

  3. I rode with an endurance race mindset. With a total of nine special stages, I rode conservatively to make sure that I still had energy left for the later stages. I stayed hydrated and fed—no bonking here.

  4. Relating to the previous point, I also prioritized not crashing. Obviously crashing hurts, but even if it's a soft landing, a lot of energy is expended in picking the bike up and getting going again. I can't afford that.

  5. I had fun. I'm not a competitive rider. Sure, it was great fun to occasionally pass someone, but for me it was more of an opportunity to legally ride as fast as I want (or can) in a controlled environment. If I saw someone coming up behind me, they had already proven themselves to be much faster than me (by gaining at least thirty seconds on me) so I just moved over and waved them by—everyone's happier that way.

sandblast 2019 service

What I Could Do Differently

  1. The bike could take advantage of taller gearing. I topped out on the straights at around 80 mph. While that sort of speed on loose sand feels really fast, I know the bike has the power to easily go faster with taller gears. I could run a significantly smaller rear sprocket and gain some top speed without sacrificing bottom-end performance (since I was launching in 2nd gear anyways).

  2. I need to learn to shift around my weight more. I was too focused on holding on and not focused enough on my body position, so there is considerable opportunity to increase my level of control by being intentional about managing my body position.

  3. Commit to the line. Loose, rutted sand is a mind game. The bike will go where you look if you get on the gas and go for it. I was getting pretty comfortable with the straight sections but turns in the churned-up sand were still intimidating. This is an area where I need more practice to improve.

  4. Push harder. I know the point is to have fun, but the point is also to go fast. This is a controlled environment: the course is closed to traffic and the turns and hazards are marked. I rode to conserve energy, but I left a lot more in reserve than I needed to.

  5. Reduce the tire pressure. I ran with about 18 psi, but could have gone down to 15 or even 12 psi for a little more traction. This would help the bike feel a little more planted and hook up better while accelerating out of the corners.

  6. Get in shape. I'm happy with how I did but I know that my own physical fitness is one of the big things that holds me back. Working to lose 40-50 lbs and improving my cardiovascular capacity would be a huge boost to my performance.

sandblast 2019 SS8 start

Summary

Overall, my entry in the 2019 Sandblast Rally was a success! I finished the race, didn't hurt myself or the bike, and had a lot of fun. Plus, I made some new friends. If time and resources allow, I would like to race again here or elsewhere in the future.

sandblast 2019 transit selfie

Sandblast Rally 2019 Finished!

ktm at the finish

I'm officially a 2019 Sandblast Rally finisher! As racer #134, I placed 10th out of 17 in my class (MM), and 22nd out of 41 bikes overall. Here are the official results from NASA Rally Sport. I wasn't wicked fast, but I didn't hurt myself or the bike, and I had a lot of fun in the process.

Now it's time for a nap.

Departure

I'm heading down to South Carolina today to attend the rally racing clinic and then the Sandblast Rally itself. I am not bringing a computer with me, so I probably will not add any more posts until I get back.

In the meantime, follow my Facebook page and Instagram feed to stay up to date on how things are going.

Wish me luck!

T-minus 4 Days

Vineyards of Casa Valduga in Bento Gonçalves, Brazil

Horseshoe Falls in Niagara Falls, Ontario

I've been out of town for about a month, so let's pick up about where we left off in January. Now it's only four days until the 2019 Sandblast Rally! I have a long list of things to take care of before I leave tomorrow; I think it's going to be a late night.

Rally Racing Clinic

A few weeks ago, I signed up for Bill Conger's rally racing clinic held on Thursday before the race. This will give me some productive seat time before shakedown on Friday and the race on Saturday—my main goal is to get reacclimated to the sand and reduce the chance of getting hurt. Plus, it looks like fun!

Bike Maintenance

My last work on the bike was the oil change and valve check, but I still have a leaky countershaft seal to replace. I have the parts here, and it's on my to-do list for tonight.

A few other bits need some attention tonight as well:

  • Acerbis gas cap: my old cap was missing the rubber seal so gas leaks everywhere when it's full. Buying the whole cap assembly was the same price as just the rubber seal, so I guess I have an extra cap now.
  • Seat bolt kit: the hollow aluminum bolt that holds some of the rear plastics on and is where the seat bolt goes through was stripped out, so I need to install a new one.
  • Countershaft bolt and dome washer: the countershaft dome washer is intended to be replaced at every sprocket change. It comes with a new bolt (with preapplied threadlocker) so that will get changed out as well.

Finally, I need to mount the new tires. It's probably my least favorite maintenance item to deal with, but it needs to be taken care of tonight.

Packing

I'm not talking about packing my suitcase for the trip, but how I'm going to pack the required equipment onto myself and/or the bike. As I've discussed in a previous post, I need to carry a first aid kit and a few documents, and some water and/or sports drink would be good too.

I've been experimenting with carrying my Camelbak backpack while wearing my Leatt body protector, but it's not ideal since the neck brace interferes with where the backpack shoulder straps would normally go. I'll use the rollie bags and carry a water bladder in there instead of on my back. I think that there will be plenty of time during transit or waiting for my check-in times to snag a drink—I'm not going to be drinking on the stage regardless.

Reflectivity

The rules mandate a minimum of 10 square inches of reflective material on the front of my person or helmet, and another 10 square inches on the back (not obscured by any backpack). I have an inexpensive reflective safety vest that fits over the Leatt to satisfy this requirement.

Weather

The forecast for the rally racing clinic on Thursday is calling for some rain, but with temperatures expected in the 60s, it will still be good for riding. Hopefully the sand will drain well and keep the mud to a minimum.

The forecast for Friday and Saturday is looking great, with no rain and expected temperatures generally in the 60s. Good cloud cover is predicted for both days, minimizing those annoying sun-in-the-eyes sections.

Valve Clearance Check

Part of the periodic maintenance on the KTM is to make sure that there's the appropriate clearance between the cam and the followers when the valves are fully closed. Too much clearance means that the valves are not opening the whole way and causes extra noise and wear from the drive train. Too little clearance means that the valves are not seating fully and can make the bike hard to start or cause burned valves because much of the heat is transferred to the head through the seat when the valve is closed.

As an engine wears over time, the valve clearance generally decreases as the hardened steel valve seats are very slowly pushed further into the aluminum head by the hammering of the valve. In this bike, there are small steel shims that fit in the cam follower to allow some adjustment. If the clearance is less than the appropriate amount, the shim is replaced with a slightly thinner (like by 0.0015") shim so that the cam-to-follower clearance is corrected.

The specifications for this motor call for 0.003-0.005" clearance for the intake valves and 0.005-0.007" clearance for the exhaust valves—this is measured when the engine is cold. The clearance for the exhaust valves is usually specified to be more than the intake valves because they will get hotter and expand more than the intake valves.

inside the RF4 head

Although space is a bit tight, checking the valve clearance in an RF4 motor is really straightforward.

  1. Remove the fuel tank to get easy access to the top of the engine.
  2. Clean the top of the engine so you don't accidentally get any dirt inside the head.
  3. Pull out the spark plug lead.
  4. Remove the three bolts holding down the valve cover.
  5. Carefully remove the valve cover. The gasket is rubber and can be re-used if you do not damage it.
  6. Rotate the engine so that it is TDC in the compression stroke. There are many ways to do this.
  7. Measure the clearance between the cam and follower using feeler gauges.

If you have to swap shims to adjust the clearance, you need to remove the cams and it gets a little bit more finicky. Fortunately, all of the valves on the bike were in spec, so I just buttoned it back up. I'm a little surprised that everything was in spec—I think it means I just need to ride the bike harder!

It was nice to do some maintenance that didn't actually require any expense other than my time.

Graphics (part 2 of 2)

It's long overdue, but I was busy with other things recently. Last night I finally finished the vinyl on the fairing! I zipped home from work at lunch today to pull the bike out into the daylight for a few photos.

side view with completed vinyl

I love how it all came out. There were a few points in time where I was a little skeptical about how it was going, but in the end the effort was totally worth it. I'm neither an artist nor a professional wrapper, but I'm thrilled with the result.

front quarter view with completed vinyl

Of course, now there's a huge pile of scraps still on my kitchen table that I should probably clean up soon.

Changing Oil & Gearing

This evening I changed the oil on the KTM. Unlike some other KTM models (cough cough RFS cough cough), the RF4 motor in the 505 XC-F is pretty easy to service. I used 1.25L of Motorex 10W-50 synthetic oil and a K&N filter, and it was all finished in about 25 minutes. No fuss, no mess!

I also wanted to change the countershaft (front) sprocket from a 13-tooth sprocket to a 14-tooth sprocket. This will give me a little more speed for the same RPMs. I previously was using the bike for more trail riding. I have a 46-tooth sprocket on the rear wheel and I'll leave that as it is.

Changing the countershaft sprocket on a KTM dirt bike is pretty easy. I loosened the rear axle and slid it forward, giving me plenty of slack in the chain. I also loosened the countershaft sprocket guard and moved it out of the way. After removing the countershaft sprocket bolt and dome washer, it was easy to slip the 13T sprocket off.

At this point I noticed there was a bit more oil there than there should be—the countershaft seal was leaking a little. This is a normal wear item, so it's fairly easy to replace. Unfortunately, I had to order the parts (about $25 in total), so I finished the sprocket swap without addressing the leak. The parts should be here at the end of the week and then I can replace the seal.

On the plus side, the chain was already long enough to handle an additional tooth on the countershaft sprocket without having to add a another link.

Medical Evaluation

In order to get a NASA rally license, you have to see a doctor and fill out a medical evaluation form. The doctor will check your usual vitals, test your peripheral vision and neurological function, and make a determination as to whether or not you are fit to receive a rally license. Since this is directly related to getting the rally license, I included it on the expenses list in the Entry category.

Keep in mind that health insurance doesn't cover things like this. If you are due for an annual physical, your doctor may be able to complete this as part of the physical since basically everything in here should be something they're already doing.

The main goal of the evaluation is to make sure that you don't have any underlying condition that might preclude competitive motorsports. If you have a history of seizures or blackouts or a trick heart, then you probably shouldn't be racing and your doctor will tell you that. So don't stress out about it if you're out of shape (like me) but otherwise generally healthy—you're in good company.

medical evaluation form

I already had my annual physical recently, so I went down to the local walk-in clinic for this since I was going to be paying out of pocket. The evaluation itself took about 20 minutes (after about a 30 minute wait) since I had already filled out my medical history on the form before meeting the doctor.

Once you have the form completed, you can scan, fax, or mail it to NASA Rally Sport. Depending on your age, you may not have to deal with it again for a few years.

Navigation Equipment

Navigation at Sandblast is done by way of a roll chart (a.k.a. route sheet or road book), so I need to mount a roll chart holder to the KTM. Since the roll chart will be a narrow enduro-style roll chart, I chose to use the Enduro Engineering roll chart holder. It is inexpensive and mounts easily to just about anything thanks to a clever mounting solution involving some bits of rubber hose and a hose clamp.

I've mounted the roll chart holder high on the nav tower, on the left side. This way I can advance the roll chart without taking my hand off the throttle.

In addition to the roll chart holder, I do generally have a GPS mounted to the bike to give me basic information like time and speed as well as the ability to display and record tracks. While Sandblast does not provide GPS tracks for navigation, knowing the time is pretty important and it will be interesting to overlay the tracks onto a map for later perusal.

I use a Garmin 62 GPS, mounted to the right of the roll chart reader in a locking, vibration-damping Touratech cradle. This keeps it easily visible when I'm riding and relatively secure when I'm parked.

roadbook reader and GPS cradle

I still need to pick up a cheap, simple digital watch to strap up front for timekeeping purposes. While the GPS has a clock, presumably accurate, I need a clock that I can synchronize to the official race time. Seconds matter!

Graphics (part 1 of 2)

I've decided to make some custom graphics for the bike. The previous owner of the fairing did a cool Star Wars-themed paint job, but I want to go a different direction. The previous owner of the bike was kind enough to send me the outlines for the custom graphics that he previously had on the bike, so I have that to start with.

labeling the stencils

Yesterday, I printed the outlines; it came to a total of 42 pages that I need to cut up and tape together into fourteen different stencils. I started by labeling each of the stencils on a "map" and then went through each of the 42 pages and labeled each piece of each stencil so I didn't get anything mixed up. It took about a half hour to identify and label all of the pieces.

paper cutter, razor, and scissors

Each of part of each stencil needs to be carefully cut out of the page so that the stencil pieces can be taped together into a complete stencil. I pulled out my trusty paper cutter, razor blade, and scissors, and got to work. This took forever. I don't ever want to see a pair of scissors again.

assembling the stencils

It got pretty late so I called it a night with the stencils cut and sitting on my kitchen table.

This evening I got back to work. I cut a piece of vinyl sized to fit a stencil and taped it flat to my workbench. Then I took the appropriate stencil and taped it down onto the flattened piece of vinyl. Finally, I carefully cut out the vinyl in the shape of the stencil.

cutting out the vinyl

The vinyl is then carefully applied to the body of the KTM. I pulled the fairing and plastic off of the bike so it's a bit easier to work with.

It's getting late again, so I'll have to finish it later. Here's a sneak preview.

sneak preview of graphics

I'm excited about how it's all going to come out; I can't wait to show you all the finished product!

De-mudding the KTM

The bike is full of mud from the last time I took it out on the Redbird Crest Trail down in the Daniel Boone National Forest.

KTM at Redbird Crest

Step one to getting it cleaned up is to take it to a self-serve car wash to hit it with some soap and water. However, it's currently winter, and water tends to become rather solid at temperatures below freezing, making cleaning the bike a bit of a challenge. Fortunately, the temperatures today were forecast to rise up to a balmy 35°F, slightly above freezing.

parked dirty at work

I bundled up and rode the bike to work today with the hopes that it would be above freezing around lunchtime and I could ride over to the car wash to get it cleaned up without the risk of everything freezing solid before it dried. (Our Kentucky winters are relatively mild; the local self-serve car wash uses warmed water and appears to stay open year-round.)

By 12:30 p.m., it was up to 30°F and the sun was out—that was close enough for me.

bathtime for the bike

After a quick spraydown, I rode bike back home and parked it in the garage. The garage isn't directly heated but it's unusual for it to get below freezing in there.

clean-ish bike

Now I wouldn't quite call the bike "clean", but at least it's not muddy anymore. This weekend I'll pull off the bodywork and start to make sure that everything is race-ready under the skin.

New Shoes

The current tires on the KTM are fine for casual trail riding around here, but not really race-ready. There's a Bridgestone M403 on the front, a motocross tire which was discontinued a few years ago, and a Dunlop MX81 on the rear, which is a little newer but is showing some wear.

While they'd surely get me through the race, I like to stack the deck in my favor as much as is allowed and opted for some new shoes. Since most of the special sections of Sandblast are run on very sandy roads—as one should expect from the name—I went with a pair of Shinko 546 soft-intermediate terrain tires.

new tires

Being "soft-intermediate terrain" tires means that they're made from a harder rubber and with a wider-spaced tread pattern in order to provide good traction in softer terrain. The trade-off is reduced traction on pavement, which is acceptable given that the real racing primarily occurs on the soft stuff. I don't have any prior experience with this specific tire model, but they are inexpensive and seem to be pretty well-liked. Realistically, as long as there's air on the inside and some knobs on the outside, the tire isn't going to make or break the race for a novice competitor such as myself.

Like most real off-road tires, these tires are not DOT-approved for highway use. In some states, DOT-approved tires are technically required, though this seems to be rarely enforced. South Carolina, where Sandblast is held, does not require (as far as I can tell) that tires be DOT-approved for highway use in order to be legally used on public roads. The race regulations also do not specify that DOT-approved tires are required.

The rear tire is a 110/100-18 size, which is the widest variant that will fit my bike. Shinko do make a 120/100-19 but that's for the 19-inch rear wheels normally found on motocross bikes. Enduro bikes, like my bike, typically have an 18-inch rear wheel diameter. Normally I tend to run a wider tire, but I think the difference will be negligible, especially on the softer terrain.

The front tire is a 90/100-21 size, which is sometimes referred to as a "fatty" front tire. It's a size bigger than the usual 80/100-21 or 90/90-21 front tires, which raises the front end a hair, adds a little more front-end cushioning, and slightly changes the steering geometry. The steering geometry change tends to add a little bit of directional stability, which is always welcome in sand. (For what it's worth, I'm not running a hydraulic steering stabilizer on the bike.)

While normally I run with the front and rear tires each stuffed with a mousse instead of a tube so I don't have to worry about flats, the use of mousses is not allowed at Sandblast. Sad panda. I picked up a set of heavy-duty Kenda rubber tubes to keep the air where it's needed. I'll have spares at the truck so I can change if needed, but I'm not planning on any trailside tube patches during the race.

In the meantime, I'll keep running the old tires. I'll spoon the new ones on before I head down to Sandblast, but there's no reason to change them quite yet.

First Aid Kit

Obviously it's generally a good idea to carry a first aid kit when you're out riding. In this case, a first aid kit is specifically required by the rules. It states that at a minimum, the first aid kit should consist of:

  • Gauze pads or rolls
  • Adhesive tape
  • Elastic bandage (like an Ace bandage)
  • Safety pins or clips for the elastic bandage
  • Scissors or knife
  • Emergency blanket
  • First aid manual

It's basically impossible to find an off-the-shelf first aid kit with just those items, so I purchased an inexpensive but relatively comprehensive home first aid kit. I pulled the required items out of the kit, along with a few more things:

  • Nitrile gloves (2 pairs)
  • Alcohol-free sterile wipes
  • CPR face mask
  • Instant cold compress
  • Gauze trauma pad (5x9-inch)

first aid kit contents

I've taken everything and packed it into a gallon-sized ziplock bag. Note that the scissors are quite sharp however they have blunt ends so I don't think they'll inadvertently puncture the bag or anything in it. This bag will go, along with a folding FMVSS #125 warning triangle and the required OK/help placard, into a small bag mounted on the back of the bike or into my hydration pack. I haven't figured out storage yet, but according to the rules, it can be either mounted to the bike or mounted to me—either is acceptable as long as it is easily accessible.

first aid kit in ziplock bag

Generally I like to minimize what I'm carrying on my back, but I need to assess how things could mount to or fit on the bike—it might just make more sense to throw everything into my hydration pack. I'll write a post later on how I'm carrying the required equipment once I sort everything out.

It's important to be prepared, but here's hoping that I won't have to use anything in the first aid kit on myself or anyone else!

Update

After talking to one of my friends who is a medical professional, I upgraded the scissors to a proper pair of trauma shears. They're inexpensive and far more versatile than the super basic scissors that were included with the home first aid kit. I also added an Israeli bandadge and SAM Splint. The whole kit is a little more bulky now, but still not much heavier.

Fees, Fees, and More Fees

The 2019 Sandblast Rally has an entry fee that varies based on what type of vehicle you are competing in and when you register. Since I am a first-time competitor, I was eligible for early entry, which began in late December. For motorcycles, the early entry fee was $290. Notably, this is an increase of $30 from last year.

But that's not all. Since Sandblast is part of the NASA Rally Sport (NRS) Atlantic Rally Cup, I need to become a member of NRS—not to be confused with the spacegoing NASA---and also obtain a NASA rally license. In order to become a member of NRS, I just had to part with $45 for a 1-year membership; easy-peasy.

To get a NASA rally license, there's an extra step. In addition to the $65 rally license fee, I also had to get the medical form completed by my doctor and sent over to NRS. This isn't anything crazy, just a quick once-over and sign-off that you're shipshape and safe to race. No one wants you to have a seizure or heart attack while you're on the course.

So that's $400 in fees just to get in the door.

2019 Sandblast Expenses

Motorcycle racing isn't cheap.

In the interest of full transparency, here's a tally of my costs involved. I've grouped the expenses into several categories:

  • Entry: entry and administrative fees
  • Racer: things for me
  • Vehicle: things for the bike
  • Cosmetic: things related to making the bike look good
  • Travel: things related to getting to and from the event

Keep in mind, as I have been riding for years, I already have a lot of motorcycle gear. Don't consider this an exhaustive list of what's needed to participate in an event like this—notably, I also did not include any food costs. Most items in the list are linked to a relevant blog post.

Total cost: not cheap


Although the entry fee for Sandblast is relatively low, that's only a small part of the expense of participating in an event like this. Many thanks to my coworker and friend Andrew for his support in getting me to the 2019 Sandblast Rally.

Rider Protection

Racers participating in the Sandblast Rally are required to have chest and back protection—this is common gear for motocross and enduro racers. However, as just a casual rider, I typically wear an armored jacket like one normally does for street riding. So I needed to find a hard-shell chest and back protector to wear for the race.

Neck protection is also a thing. Leatt are the best-known name in neck protection in the motorcycling world. The Leatt neck braces are probably better described as neck restraints, with a similar function to a HANS device in the auto racing world.

In conjunction with a full-face helmet, the Leatt neck brace helps limit extreme movement of the neck by dispersing forces to the shoulders and chest. While this doesn't prevent all neck injuries, it reduces the risk of severe neck injury during a crash. Leatt have been making a variety of CE-approved neck brace models for the past decade.

Neck restraints are not required by NASA RallySport (the sanctioning body behind Sandblast) for competitors in either cars or bikes, however I think it's a wise choice (and there's data to back that up). I was able to find a lightly-used Leatt Fusion 3.0---a chest and back protector with an integral Leatt neck brace—on the secondhand market for essentially half price, so I snapped it up.

bathroom mirror Leatt selfie

It arrived today, and it fits comfortably. This one has shoulder pads, which is a nice bonus. And yes, that's a bathroom mirror selfie—I'm classy like that.

New Battery

After the incident bringing the bike home from the storage unit, it was clear that a new battery was needed. I ended up ordering a WPS Featherweight lithium battery while I was waiting around in the parking lot for roadside assistance to come and give me a jump. It arrived today! Hurray!

old Sycl lithium battery

The current battery in the KTM is an old lithium battery by Sycl (no longer in business) that has been in the bike for at least five years. It might be salvageable but I feel like it's worth the relatively low cost to replace it, especially since

  • the KTM 505 XC-F has no kickstarter and
  • the Rekluse auto-clutch means that push-starting it requires some tools.

I really want the magic button to work reliably.

new WPS Featherweight lithium battery

I chose the WPS battery because it was an affordable, lightweight, and of course won't spill when I dump the bike if the bike needs a nap. It has an integrated battery capacity indicator on top that gives you a low/medium/high indication by way of some LEDs at the press of a button. I know that it's more a gimmick than anything else, but it is cool.

battery capacity indicator lights

The new battery slipped right in and the cables attached to the terminals without any interference; there's good power to the lights and fan now. It's a bit late tonight to fire it up—the neighbors have little kids that I don't want to wake up—but I'll take it out for a spin (probably to the car wash) tomorrow.

new battery, installed

Rally Fairing Replacement

The KTM is equipped with a first generation Rally Moto Kit by my friends at Motominded. Unlike the second generation (released just last December), which uses a factory KTM 450RR windscreen, the first generation used a custom fiberglass fairing.

Here you can see what it looked like on that first day I brought it home: the KTM upon arriving home the first time

A few months later, I was riding the bike with a friend out in North Carolina when I had a slight mishap. While both the bike and I were largely unscathed, the fairing suffered a mortal blow and ripped into two pieces.

the original fairing torn in half

Only a handful of the first-generation Rally Moto Kits were made, and spare fiberglass was nigh impossible to find. Even the original molds were no longer serviceable, so no more could be made.

This is repairable if you are good with fiberglass. However, I'm not, so I've been riding the bike since 2014 with no fairing at all. The fairing did provide some useful wind protection, but the navigation tower still does a great job of holding my GPS and gives a place to mount the headlights.

the KTM at the Imogene Pass summit

Recently—with Sandblast in mind—I inquired with Motominded if they thought that the KTM 450RR windscreen used in the second-generation could be somehow mounted to the first-generation Rally Moto Kit. The short answer was, unfortunately, "not easily", but they were able to refer me to a mutual friend who had a second set of fairings from the first-generation model.

He was willing to sell me his second set of fairings at a very reasonable price, and they arrived just recently. A talented previous owner had painted them with a Star Wars theme, which I think is awesome, and the paint job is holding up pretty well.

the replacement fiberglass fairing

Since I brought the bike home to my garage last night, I couldn't resist a quick test-fit of the fairing. I was expecting to need some tweaks since these are older bikes and might be a little tweaked here and there themselves, but it fit easily without any need to "use the force" to make things line up.

first daylight look at the replacement fairing on the bike

I'll need to find a few pieces of hardware finish mounting it properly, but it's starting to look like a real rally bike again!

<script type="text/javascript">

Bringing It Home

Now that the KTM is street legal, it's time to get it from the storage unit where it lives over to my tiny garage. This means I need to ride the Crosstourer over to the storage unit, jump-start the KTM (since the battery is kaput), tuck the Crosstourer into the storage unit, and then ride the KTM back to my garage. No problem, right?

Well, I made it part of the way home before I turned off the choke and promptly killed the motor. Oops. The bike doesn't warm up very quickly in the near-freezing winter weather. There was not enough juice in the battery to restart the bike, so I called roadside assistance for a jump. Prepared for a long delay, I ordered a new battery while I sat there waiting for help to arrive.

waiting for roadside assistance

Fortunately, the tow truck showed up after only about twenty minutes. With a boost from a big battery, the bike started easily and I made it the rest of the way home without any issues.

The new battery should arrive next week.

The Racer

I bought my first real motorcycle back in 2005. It was a 1981 Suzuki GS550 that mostly ran and sometimes ran well enough to get me to class and back. I was hooked. The GS550 was quickly followed by a 1983 Honda Nighthawk 650 that was smooth and reliable and took me to and from work, school, and all over East Texas—rain or shine.

A long list of bikes have since graced a spot in my garage. In 2011, I was introduced to the art of off-road riding by a Suzuki DR650, and I was quickly sucked into the world of dual-sport and adventure riding. I've been riding a mix of on-road and off-road ever since. Currently, I own a 2009 KTM 505 XC-F and a 2016 Honda VFR1200X.

I attended the 2018 Sandblast Rally as a course volunteer; after seeing the action up close, I knew I had to try it for myself. I have accumulated thousands of miles of experience riding off-road on big adventure bikes (like my Moto Guzzi Stelvio and Africa Twin), dirt bikes, and even a Ural sidecar rig.

On March 2, 2019, I left the starting line at 8:46:00 a.m. and launched into the 2019 Sandblast Rally as a competitor. A little less than 10 hours later and a lot muddier, I rolled to the finish line to hand in my timecard as a finisher. What a blast!

2019 Sandblast Rally Racer Prep

About Ian’s Bike

the bike

I rode a 2009 KTM 505 XC-F for the 2019 Sandblast Rally. This bike was modified with a frame-mounted nav tower and fairing that was originally made by MotoMinded for Neduro's 2012 Dakar entry. Before I purchased the bike, it had also been raced by Ned in the 2013 Touareg Rally and the 2013 Baja Rally, so if I didn't make it to the finish, it wasn't because of the bike!

neduro to dakar 2012

2019 Sandblast Rally Bike Prep

Getting Legal

Race vehicles for the Sandblast Rally need to be registered and insured street-legal vehicles. I hadn't yet gone through that process for the KTM, and I had some concern that I might need to jump through some extra hoops since KTM originally only sold the 2009 505 XC-F as a off-road vehicle.

Inspection

The bike was previously converted from an off-road vehicle to a street vehicle in Colorado, which means that it went through an inspection process and received a title and license plates like any other street-legal bike or car. However, I'm not in Colorado anymore; any vehicle coming into Kentucky from outside the state requires an inspection by the county sheriff's office.

The sheriff's inspector verifies that the VIN number on the frame matches the VIN number on the documentation, and they check for some basic mechanical requirements--here's a non-exhaustive list based on my observation of the process:

  1. taillight
  2. brake light activated by at least one brake
  3. headlights
  4. horn
  5. tires, general appearance and condition
  6. generally operable condition

Note that neither DOT-approved tires nor turn signals are required on motorcycles to be registered in the state of Kentucky.

I first took the bike to the inspector at my nearest county clerk's office. This is where I've done the paperwork for my other vehicles and it's generally been a painless process. Unfortunately, the inspector there was not really well-informed on the law or motorcycles in general and refused to inspect the bike since it was not equipped with turn signals.

The inspector was using an inspection checklist that was clearly written for cars, so I pointed out to him that no motorcycles are equipped with doors, which were also on his checklist. It went predictably downhill from there and I left a bit frustrated and without the inspection that I needed in order to register the bike.

I called the sheriff's office to confirm with them that turn signals were not required. They suggested trying another inspector so I traveled across the county to the county clerk's other office location. There the inspector was much more informed and gave me a passing inspection without any issues despite copious amounts of mud caked everywhere and a mostly-dead battery.

Insurance

I was able to easily add the KTM to my existing motorcycle policy online without any roadblocks. My annual insurance premium actually decreased by about $50 with the addition of the bike.

Registration

Armed with a passing inspection and proof of insurance, getting the bike registered in Kentucky was as simple as handing the clerk the Colorado title, the inspector's report, and freshly-faxed proof of insurance. After handing over some hard-earned cash to cover the taxes and fees, I was able to walk out with some fresh plates.

photo of license plate after registration

Success!

Text Reports III: Filling the Page

Continuing to build on the first and second installments, let's try to reduce the wrapping used by expanding to fill the page width.

We'll continue to use the same data as before, but with the Moisture Content and Volatile Matter columns removed to give us room to expand. Thus we start with this:

                               Gross           
                               Caloric         
                    Fixed      Value at        
                    Carbon by  Constant        
Sample              Difference Volume   Ash    
Name    Date        wt. %      J/g      wt. %  
------- ----------- ---------- -------- -------
X24-03  01-Nov-2018 15.62      19985    0.25   
X24-02  31-Oct-2018 16.01      20004    0.23   
X24-01  30-Oct-2018 15.89      19996    0.24   

The output above only uses 48 columns of text, so you can see there's lots of room to grow.

How Wide?

We need to define how many columns of text can fit on a page. The code blocks that I use on this site comfortably fit about 80 columns—at least in my browser—so we'll use that. We'll set the page width next to where we set the minimum column width.

    // page width and minimum column width
    const minimumColumnWidth = 7;
    const pageWidth = 80;

Let's Get Organized

Our approach to filling the space will be by increasing the width of the most-wrapped header columns in an effort to reduce the wrapping. We define most-wrapped as the column header that has the greatest height.

To do this, let's first reorganize our code a bit to make it more modular. First, we'll make a word-wrap function based on the wrapping code we've used previously. Note that it returns an array of lines rather than a single string with newlines.

// wrap text to a specific length
function wrapText(str, len) {

    // split the string into words and use Array.reduce() to condense it into
    // lines that are all less than or equal to the target length
    return str.split(/\s+/).reduce((accumulator, current) => {

        // is this the first word?
        if (accumulator.length == 0) {

            // start a new line with the first word
            return [current];
        } else {

            // get the last line
            const lastLine = accumulator.pop();

            // add the next word to the last line
            const testLine = lastLine + ' ' + current;

            // is this line less than or equal to the target length?
            if (testLine.length <= len) {

                // add the line to the list
                return accumulator.concat(testLine);
            } else {

                // otherwise add the unmodified line to the list and start a
                // new line with the next word
                return accumulator.concat(lastLine, current);
            }
        }
    }, []);
}

Next, let's take our code to extract the units from the first data row of the grid and strip them out of all of the values and move it all into its own function which modifies the grid and returns the units. This is updated a little bit from the previous installment in order to modify the grid in place.

// extract the units from a string
function extractUnits(grid) {

    // check each cell in the first data row of the grid
    const units = grid[1].map(str => {

        // compare with the regular expression
        const matches = str.match(/\d+(\.\d+)?\s+(.+)/);

        // was there a match?
        if (matches !== null) {

            // return the units
            return matches[2];
        } else {

            // otherwise return false
            return false;
        }
    });

    // remove the units from the grid
    grid.forEach((row, i) => {

        // is this a data row?
        if (i > 0) {

            // if we have a unit, remove it from the cell
            grid[i] = row.map((x, j) => units[j] ? x.split(/\s/)[0] : x);
        }
    });

    // return the units; the grid is already modified
    return units;
}

Finally, let's take the code which renders the grid into text and put that into its own function. As an input, we'll give it the main grid data, the units, and the desired column widths.

// render the grid into text
function gridToLines(grid, units, columnWidths) {

    // wrap the column headers based on the calculated widths
    const headers = columnWidths.map((w, i) =>
        wrapText(grid[0][i], w).concat(units[i] ? units[i] : []));

    // how many header lines do we need?
    const headerHeight = Math.max(...headers.map(x => x.length));

    // pad our headers with blank lines so the content is bottom-aligned
    headers.forEach(h => {
        h.unshift(...new Array(headerHeight - h.length).fill(''));
    });

    // format as lines
    return new Array(headerHeight)
        .fill('').map((h, i) =>
            columnWidths.map((w, j) => headers[j][i].padEnd(w)).join(' '))
        .concat(columnWidths.map(w => ''.padEnd(w, '-')).join(' '))
        .concat(...grid.slice(1).map(row =>
            columnWidths.map((w, j) => row[j].padEnd(w)).join(' ')));
}

This Wide

Now that we have our existing code a little better organized, let's move into the new stuff. We want to incrementally make the tallest (defined by header height) column wider until it is shorter and we still fit on the page. Let's start by writing a function which, given a string and a target height, will tell us the smallest wrapping width to acheive the target height.

// find the minimum width to wrap text to a target height
function calcWidth(str, targetHeight = 0) {

    // start with the minimum width possible without breaking words
    let width = Math.max(...str.split(/\s+/).map(w => w.length));

    // calculation for the height
    const heightCalc = (str, width) => wrapText(str, width).length;

    // increase the width until we reach the target height
    while (targetHeight > 0 && heightCalc(str, width) > targetHeight) {
        width++;
    }

    // return the width used to hit the target height
    return width;
}

Note that in targetHeightWidth() we made targetHeight an optional parameter with a default value of zero. We'll reuse this function later to calculate the minimum possible width of a column.

Now we get to the meat—how do we expand things to fill the page width? The basic algorithm is this:

  1. Find the "tallest" column header.
  2. Expand it so that it's one row shorter.
  3. If we're still narrower than the page width, repeat from the top.

Here's the code integrated in a function to calculate the column widths.

// find the optimal column widths
function calcWidths(grid, units, minimumColumnWidth, pageWidth) {

    // calculate the minimum column widths
    let columnWidths = new Array(grid[0].length)
        .fill(minimumColumnWidth)
        .map((width, i) => Math.max(...grid.map((row, j) =>
            j === 0 ? calcWidth(row[i]) : row[i].length).concat(width)));

    // iterate until we fill the page
    let newWidths = columnWidths.slice(0);
    do {

        // use the new widths
        columnWidths = newWidths;

        // find the tallest column
        let tallest = columnWidths
            .map((w, i) => ({ i, h: wrapText(grid[0][i], w).length }))
            .sort((a, b) => a.i - b.i)
            .sort((a, b) => b.h - a.h)[0];

        // if our tallest column has a height of 1, bail on the loop
        if (tallest.h <= 1) break;

        // make a copy of the column widths
        newWidths = columnWidths.slice(0);

        // update the width of the tallest column
        newWidths[tallest.i] = calcWidth(grid[0][tallest.i], tallest.h - 1);

        // repeat if we're still under the target width
    } while (newWidths.reduce((a, c) => a + c + 1, -1) <= pageWidth);

    // return the column widths
    return columnWidths;
}

It's not an optimal algorithm, but it gets us close enough to be functional. Perhaps we'll improve on it in a later iteration.

Put It Together

We've refactored the code into a bunch of functions, so let's put it all together. Here's the main code that calls the functions above:

// page width and minimum column width
const minimumColumnWidth = 7;
const pageWidth = 80;

// populate the grid from our input data
const grid = [Object.keys(input.data[0])];
input.data.forEach(row => {
    grid.push(grid[0].map(key => row[key]));
});

// extract the units from the grid
const units = extractUnits(grid);

// calculate the column widths
const columnWidths = calcWidths(grid, units, minimumColumnWidth, pageWidth);

// print out the grid
console.log(gridToLines(grid, units, columnWidths).join('\n'));

Output

With a specified page width of 80 columns, this is the output that we get:

                                                   Gross Caloric Value        
                        Fixed Carbon by Difference at Constant Volume  Ash    
Sample Name Date        wt. %                      J/g                 wt. %  
----------- ----------- -------------------------- ------------------- -------
X24-03      01-Nov-2018 15.62                      19985               0.25   
X24-02      31-Oct-2018 16.01                      20004               0.23   
X24-01      30-Oct-2018 15.89                      19996               0.24   

You can download the complete code here.

Next Steps

In the next iteration, we'll handle wrapping of the entire table if the columns are too wide to fit in the width of the page.

Text Reports II: Extract Units

Building on the first installment, let's improve the formatting by pulling the units out of the rows and putting them in the header instead. We'll use the same data as before.

Define Pattern

Using a regular expression, we can easily determine if a cell contains a numeric value with a unit and then extract both parts. Let's create a function that takes the cell contents as an input and returns the unit (or false if no units are found).

// extract the units from a string
function extractUnits(str) {

    // compare with the regular expression
    const matches = str.match(/\d+(\.\d+)?\s+(.+)/);

    // was there a match?
    if (matches !== null) {

        // return the units
        return matches[2];
    } else {

        // otherwise return false
        return false;
    }
}

Extract Units

Now that we have a function that will parse the string and pull out the units if there are any, let's create an array that contains the units for each column (based on the first row). We are making the assumption that the units will be the same in every cell in a given column, and that the first data row exists with no blank values.

// make a list of units based on the first row of data
const units = grid[1].map(value => extractUnits(value));

Now that we've determined what the units are, we need to remove the units from the grid.

// remove the units from the grid
grid = grid.map((row, i) => {

    // is this the header row?
    if (i == 0) {

        // don't change anything
        return row;
    } else {

        // if we have a unit, remove it from the cell
        return row.map((x, j) => units[j] ? x.split(/\s/)[0] : x);
    }
});

Column Widths

With the units moving to the header, we need to make sure the column widths are all wide enough. Let's update the width calculation to make sure that the units are included when we're looking at the header row words. We consider the whole unit string as a "word" in this case as we do not want to break in the middle of the unit string.

// calculate the column widths
let columnWidths = new Array(grid[0].length)
    .fill(minimumColumnWidth)
    .map((width, i) => Math.max(...grid.map((row, j) => {
        
        // is this the first row?
        if (j === 0) {
            // find the width of the longest word (including the units)
            return Math.max(...row[i].split(/\b/)                      // words
                                     .concat(units[i] ? units[i] : []) // units
                                     .map(w => w.length));             // lengths
        } else {
            // find the width of the whole contents
            return row[i].length;
        }
    }).concat(width)));

Now that we've extracted the units and taken them into consideration when sizing the columns, let's update the code to print the units as part of the column headers. The units will come out on the bottom line of the header.

// wrap the column headers based on the calculated widths
let headers = columnWidths.map((w, i) => {
    return grid[0][i].split(/\s+/).reduce((accumulator, current) => {
        if (accumulator.length == 0) {
            return [current];
        } else {
            const lastLine = accumulator.pop();
            const testLine = lastLine + ' ' + current;
            if (testLine.length <= w) {
                return accumulator.concat(testLine);
            } else {
                return accumulator.concat(lastLine, current);
            }
        }
    }, []).concat(units[i] ? units[i] : []); // add units
});

Output

With our changes, the output now looks like this:

                                                 Gross           
                                                 Caloric         
                                      Fixed      Value at        
                    Moisture Volatile Carbon by  Constant        
Sample              Content  Matter   Difference Volume   Ash    
Name    Date        wt. %    wt. %    wt. %      J/g      wt. %  
------- ----------- -------- -------- ---------- -------- -------
X24-03  01-Nov-2018 4.85     79.29    15.62      19985    0.25   
X24-02  31-Oct-2018 4.52     80.91    16.01      20004    0.23   
X24-01  30-Oct-2018 4.68     80.03    15.89      19996    0.24   

You can download the complete code here.

Next Steps

In the next iteration, we'll improve the layout by expanding to fill the page width.

Column Formatting for Text Reports

Here's the scenario: we have a system that outputs plain-text reports with data formatted into a table. Our raw data comes in JSON format; we'll use this as the input to our program:

{
    "data": [
        {
            "Sample Name": "X24-03",
            "Date": "01-Nov-2018",
            "Moisture Content": "4.85 wt. %",
            "Volatile Matter": "79.29 wt. %",
            "Fixed Carbon by Difference": "15.62 wt. %",
            "Gross Caloric Value at Constant Volume": "19985 J/g",
            "Ash": "0.25 wt. %"
        },
        {
            "Sample Name": "X24-02",
            "Date": "31-Oct-2018",
            "Moisture Content": "4.52 wt. %",
            "Volatile Matter": "80.91 wt. %",
            "Fixed Carbon by Difference": "16.01 wt. %",
            "Gross Caloric Value at Constant Volume": "20004 J/g",
            "Ash": "0.23 wt. %"
        },
        {
            "Sample Name": "X24-01",
            "Date": "30-Oct-2018",
            "Moisture Content": "4.68 wt. %",
            "Volatile Matter": "80.03 wt. %",
            "Fixed Carbon by Difference": "15.89 wt. %",
            "Gross Caloric Value at Constant Volume": "19996 J/g",
            "Ash": "0.24 wt. %"
        }
    ]
}

Create Grid

Using the built-in JSON.parse() function, we take the above JSON and interpret it as a JavaScript object called input. Next, we need to transform it into a simple 2-dimensional array of strings that we'll refer to as our grid. The first row of the grid is made up of the column names, and each row after that is data.

// initialize our grid with the first row as the keys of the input data objects
let grid = [Object.keys(input.data[0])];

// load the rest of the rows into the grid
input.data.forEach(row => {
    grid.push(grid[0].map(key => row[key]));
});

Minimum Column Widths

Let's continue by calculating the minimum column width for each column in the grid. We'll say that we don't want any columns narrower than 7 characters wide. We also don't want to wrap any of the actual data values, so we'll break down the column headers (in grid[0]) into words but not the rest of the rows.

// minimum column width
const minimumColumnWidth = 7

// calculate the column widths
let columnWidths = new Array(grid[0].length)
    .fill(minimumColumnWidth)
    .map((width, i) => Math.max(...grid.map((row, j) => {
        
        // is this the first row?
        if (j === 0) {
            // find the width of the longest word
            return Math.max(...row[i].split(/\b/).map(w => w.length));
        } else {
            // find the width of the whole contents
            return row[i].length;
        }
    }).concat(width)));

Great, now that we have the width of each column, we can continue with outputting the column header lines. Because of the wrapping, some column headers will take more lines than others, so we'll take care to pad the header lines such that the column headers are aligned to the bottom.

// wrap the column headers based on the calculated widths
let headers = columnWidths.map((w, i) => {
    return grid[0][i].split(/\s+/).reduce((accumulator, current) => {
        if (accumulator.length == 0) {
            return [current];
        } else {
            const lastLine = accumulator.pop();
            const testLine = lastLine + ' ' + current;
            if (testLine.length <= w) {
                return accumulator.concat(testLine);
            } else {
                return accumulator.concat(lastLine, current);
            }
        }
    }, []);
});

// how many header lines do we need?
const headerHeight = Math.max(...headers.map(x => x.length));

// pad our headers with blank lines so the content is bottom-aligned
headers = headers.map(h =>
    new Array(headerHeight - h.length).fill('').concat(...h));

// create the headers
let lines = new Array(headerHeight).fill('').map((h, i) => 
    columnWidths.map((w, j) => headers[j][i].padEnd(w)).join(' '));

// create the separator lines
lines.push(columnWidths.map(w => ''.padEnd(w, '-')).join(' '));

Printing the data rows is a bit simpler as we do not do any wrapping, although we still pad the end of each cell with spaces using String.padEnd() to help everything line up correctly.

// compose each data line
grid.forEach((row, i) => {

    // skip the first line, we already have the headers
    if (i > 0) {
        lines.push(columnWidths.map((w, j) => row[j].padEnd(w)).join(' '));
    }

});

// print out the lines
console.log(lines.join('\n'));

Output

Here's what the program outputs to the console:

                                                       Gross               
                                                       Caloric             
                                           Fixed       Value at            
Sample              Moisture   Volatile    Carbon by   Constant            
Name    Date        Content    Matter      Difference  Volume    Ash       
------- ----------- ---------- ----------- ----------- --------- ----------
X24-03  01-Nov-2018 4.85 wt. % 79.29 wt. % 15.62 wt. % 19985 J/g 0.25 wt. %
X24-02  31-Oct-2018 4.52 wt. % 80.91 wt. % 16.01 wt. % 20004 J/g 0.23 wt. %
X24-01  30-Oct-2018 4.68 wt. % 80.03 wt. % 15.89 wt. % 19996 J/g 0.24 wt. %

You can download the complete code here.

Next Steps

In the next installment, we'll update the program to make the output more readable by extracting the units out of the cells and putting them in the column headers.

Removing Facebook Tracking Params

Facebook recently began adding a fbclid parameter to external links. Using the Neat URL Firefox Add-on and the Neat URL Chrome extension, you can easily remove these and other similar tracking parameters.

  1. Install the Neat URL Firefox add-on or the Neat URL Chrome extension, depending on which browser you are using. I have only tried the Firefox add-on as I am not a regular Chrome user.
  2. Go to the add-on preferences by right-clicking on the ?_ icon selecting Preferences. This will bring up the Firefox Add-ons Manager with the Neat URL preferences page open.
  3. Scroll down a bit to the Blocked parameters box. It should already be prepopulated with a lot of parameters. At the time of this writing, fbclid is not in there by default, but I won't be surprised when the author adds it.
  4. If it's not in there already, add fbclid to the list. I added it in the middle with the other fb_* parameters.
  5. Make sure you hit the Save preferences button at the bottom of the page.

Once you've completed the above steps, you can test by going to https://ianc.blog?fbclid=foo. The extension modifies the request before it is sent to the server, so you should see the address bar show https://ianc.blog right away. You're all set!

CC BY-SA