Saturday, July 21, 2012

Well-designed, Easily Assembled, Reasonably Priced, Adjustable-height Desk

There are lots of companies selling adjustable-height desks, but they tend to cost nearly a thousand dollars, or more.

I finally found something that works and doesn't cost so much: http://heightadjustableworktable.com. If you already have a desk, you have a desktop, and can probably reuse it, so all you need is the frame. (If you don't have a top, they sell complete desks too.)

The frame breaks down into a fairly small, dense package. It only took about an hour to assemble (not counting idiotic rework because a certain hamfisted idiot flipped the top the wrong way the first time).

The mechanism is metal on nylon bushings, and operates smoothly.

The engineering and manufacturing are first-rate.

A very good value.

My only complaint is that the lowest height of the desk is still too high for a normal-height woman, and there's no way to shorten the legs because the mechanism is integral.

Saturday, July 14, 2012

Building A Completely Silent PC

Under load, a PC fan becomes a distraction while listening to music with quiet passages, coding, etc.

So I built a couple of completely silent PCs. (The optical drive makes noise, but I only used it to load the OS.)

If you want to do the same, you'll need to follow steps similar to these:
  1. Get a fanless-PC chassis. These are also called "media PCs", and they're silent so they can be used in home theaters without distraction. There are several manufacturers. The best price/performance ratio seems to be Streacom. You can get them from Perfect Home Theater, and from Quiet PC. Both vendors are a pleasure to work with. The prices were better at Perfect Home Theater, but he was out of stock in silver, so I wound up getting them from Quiet PC, and then got the accessories from him. The FC8 chassis I used has the smallest footprint, but requires an external power supply. For our offices, there wasn't space on the rack for a flatter, wider chassis like the FC5, FC9, or FC10. Also, I wanted front-panel USB sockets. Be careful to get fanless, because Streacom makes other models that look like the fanless versions, but aren't. You can get them with remote controls, which is useful for media PCs, and useless for a regular PC.
  2. Get the necessary parts. You will need a motherboard, CPU, RAM, SSD, and, if you want an internal optical drive, the special slimline optical drives from Perfect Home Theater. I wanted a fast system, so I used an Intel DH67CF motherboard, Intel Core i7 3770S 3.1 GHz 4 Core LGA 1155 CPU, Crucial 8GB RAM, and Intel 520 180 GB SSD. Not being a gamer, the CPU's audio and video is perfectly adequate, so I didn't need any other cards. Make sure you select a motherboard that is compatible with the chassis (Streacom lists compatible boards on their site--make sure you get one with SATA 6). The CPU I used is the fastest 65W available for a motherboard compatible with the FC8.
  3. Get some thermal paste. Selecting a paste feels like it takes longer than building the PC (http://www.maximumpc.com/article/features/geek_tested_17_thermal_pastes_facehttp://benchmarkreviews.com/index.php?option=com_content&task=view&id=150&Itemid=62). I wound up using Prolimatech PRO-PK1-5G, which is available from Newegg. The paste makes a mess no matter how hard you try to be careful, so be sure to put down some plastic or layers of paper towels, and wear some throwaway plastic or latex gloves if you have them.
  4. Follow the detailed and very helpful instructions on the Perfect Home Theater site. The two most-important pieces of information are the FC8 manual, and the connection map. Make sure you connect the SSD to a SATA 6 socket.
The only tools needed are two screwdrivers (small and really small), small wire cutters (if you want to cut off the floppy power pigtail), and a small cresent wrench (if you want to tighten the power socket more than finger tight, although finger tight seems pretty tight already). Magnetic screwdrivers are very helpful

There wasn't a lot of room between the micro-PSU and the right heatpipe, so I (gently!) bent the heatpipe up a bit, and cut off the Molex socket that faces into the case (because an identical socket on the other side of the micro-PSU faces away from the heatpipe).

I also cut off (carefully!) the power pigtail for a floppy drive, to remove a bit of clutter from the interior.

There are a number of small screws--get a bowl to put them in so they don't disappear.

It took about three hours to build the first one, due to fumbling around and learning how everything connects. The second took under an hour. (Those times do not include how long it took to load and configure the software.)

Sunday, June 17, 2012

Flossing, Seatbelts, And Dynamic Type Checking


"It's all fun and games until someone loses an eye."

Researchers, many of whom were really really smart, deduced that you should floss, and wear seatbelts, and eat a balanced diet.

In software, similarly intelligent researchers determined that strong typing was like flossing and the wearing of seatbelts: a very minor inconvenience that saved you a lot of trouble later on.

Now the trend is increasingly towards dynamic languages that discover type mismatches at runtime.

You know who does that discovering?

Your users.

The only reason your users aren't already at your gates with torches and pitchforks is that browsers turn off JavaScript errors by default.

Is this really the best we can do? The argument against static type checking boils down to "I'm a very careful driver", which is what every driver thinks right up until they get in an accident.

Languages with static type checking allow programmers to opt out (even Ada has unchecked conversion). That's analogous to allowing passengers to not put on their seatbelts if they're crazy enough not to want to wear them. JavaScript doesn't allow programmers to opt into static type checking. That's analogous to a driver taking all of the seatbelts out of the car, even those for the passengers, including children.

Sunday, June 10, 2012

Javascript Slideshow With Fade

I wanted a simple slideshow for our Analog Perfection™ website (don't bother going there--we won't have it up until just before launching the Kickstarter project).

The requirements were:
  • Free
  • Source code provided
  • Fade transitions
  • Small file size
  • Fast initial load (implying lazy loading of thumbnails)
  • Plain old Javascript (no dependencies on frameworks such as JQuery, MooTools, Dojo, Flash, etc.)
Nothing quite matched the requirements, so I wrote one, cobbling pieces together from answers on Stack Overflow, some help from Tyler Riding, Priyajeet Hora, Fro Rosqueta, and Doug Crossley (colleagues at Intuit), and a couple of key insights from the Tigra Fader sample code.

The result is under 4k, non-minified.

The code puts all of the slide thumbnails in one <a> element, as imgs, on top of each other, but with different z-ordering, and then adjusts the opacity of the current and next thumbnails.

You can see the pairwise opacity changes by looking at the elements in your browser's developer-tools console.

The HTML for all but the first thumbnail is generated dynamically, when the thumbnail lazy loads.

When the next thumbnail has crossed a threshold, the href is updated to point to that thumbnail's corresponding full-size picture.

Mousing into the thumbnail region accelerates the fading to 100%, then pauses the program. Mousing out of the thumbnail region resumes the program (as does clicking into the thumbnail).

I've only tested in on IE9 and Chrome so far, but have tried not to use anything quirksmode says won't work on older browsers. Eventually this will be deployed on a live site, and then I can start using something like browsershots to test, and then the cursing begins.

I embed the Javascript in my HTML like this:

<div id="slides">
<a id="slideLarge" href="./images/FrontView.jpg" target="_blank">
<img id="slideSmall0" src="./images/FrontViewSmall.jpg" alt="[unable to display picture]"/>
<!--INCLUDE("slides.js")INCLUDE-->
</a>
</div>

and use the Includer to expand the contents before deployment. If you don't want to do that, just lexically replace the INCLUDE line with the contents at the bottom of this post, or use a regular script src include.

Note that for browsers that have Javascript disabled, the first thumbnail and its associated full-size picture will still display correctly.

There are a number of arguments to the main function:
  • imgdir: Directory containing the thumbnails and full-size pictures--must end with '/'. If current directory, use "./".
  • fullid: HTML ID of the full-size picture.
  • idprefix: Shared prefix for the HTML IDs of all of the thumbnails.
  • width: Width in px (but without "px") of the thumbnails.
  • holdtime: How long in milliseconds to hold one thumbnail image constant before starting to fade.
  • fadetime: How long in milliseconds to take to perform a fade.
  • steps: How many steps to use to perform a fade. More is smoother but uses more CPU.
  • fastfadetime: How long in milliseconds to take to perform a fade when the mouse is over the thumbnail.
  • faststeps: How many steps to use to perform a fade when the mouse is over the thumbnail. More is smoother but uses more CPU.
  • flip: Percent opacity (as a number from 0.0 to 1.0) next thumbnail must have before clicking on it brings up its full-size picture instead of the current thumbnail's full-size picture.
  • names: Array of arrays where the inner arrays each have two elements: [the simple name of a thumbnail, the simple name of the full-size picture corresponding to the thumbnail]. The names of the first thumbnail and first full-size picture must not be included (they are extracted from the non-generated HTML during initialization).
The arguments are not passed in using name/value pairs, but are instead just positionally associated.

The thumbnails all have to be the same size (width and height) for the overlaying to work.

Using a naming convention for the shared ID prefix was done on purpose. If you don't like it, you can modify the code to add the prefix to the names as another element in each array, for all but the first thumbnail, and get rid of idprefix entirely. (But to do that, you need to obtain the ID of the 0th thumbnail by walking the DOM starting at the fullid. It's not difficult, but seemed like overkill.)

I considered using a naming convention to eliminate the need to pass in an array of arrays, and just pass in an array of names. But then what if the extensions differ between the two sizes of files?

Things left to do:
  • Add a row of numbers (or circles, or some other kind of icon) that indicates which slide is currently active, and to allow direct navigation to that slide by clicking on the number/icon, similar to how National Geographic and menucool do it. We don't really need it for our site, but it's an interesting exercise. To keep the layout clean, it could perhaps only display the row when the mouse is over the thumbnail, and display it over the thumbnail, at the bottom (not outside the thumbnail region), similar to how Dynamic Drive does it.
  • Add drag support, so the user can slide the thumbnails left or right by mouse down followed by mouse move. Don't treat the eventual mouse up as a click that opens the large picture.
  • Figure out how to get the width from the 0th slide, instead of having to pass it in, which is annoying. I tried style.clientWidth, style.offsetWidth, and various other suggestions online. Nothing worked, perhaps because at the point where the width is needed, the page hasn't been rendered yet, so the viewport size isn't known. If someone knows how to solve this, please post the solution!
Misc notes:
  • I tried a different approach before this one, but couldn't get it to work: manipulate the opacity of the link's background image and one img, and once whichever one of those is set to full opacity, replace the other one with a new image, then repeat the seesawing.
Here's the code, plus the CSS I used. (To get the pictures to line up, they need to share a margin, and to do that the margins have to be set on the elements--the CSS will help you get it working.)

CODE:
<script type="text/javascript" id="slidesjs">
<!--
// Copyright © 2012 jimandlisa.com. All rights reserved.
function slides(imgdir, fullid, idprefix, width, holdtime, fadetime, steps, fastfadetime, faststeps, flip, names) {

    "use strict";

    var full, thumbs, pos, cur, next, initial, paused, stepsize, steptime, curstep, step, up, down, flipped, dofast;

    function getname(url) {
        return url.substring(url.lastIndexOf('/') + 1);
    }

    function setsteps(fast) {
        if (fast) {
            stepsize = 1.00 / faststeps;
            steptime = Math.round(fastfadetime / faststeps);
            dofast = true;
        } else {
            stepsize = 1.00 / steps;
            steptime = Math.round(fadetime / steps);
            dofast = false;
        }
    }

    function setopac(style, opac) {
        style.opacity = opac;
        style.filters = "alpha(opacity=" + Math.round(opac * 100) + ')';
    }

    function fade() {

        if (paused) {
            if (!dofast) {
                setsteps(true);
            }
        }

        step = curstep * stepsize;
        up = (step > 1.000) ? 1.000 : step;
        down = ((up === 1.000) || ((1.000 - step) < 0.000)) ? 0.000 : (1.000 - step);
        up = (down === 0.000) ? 1.000 : step;
        curstep += 1;

        setopac(cur.style, down);
        setopac(next.style, up);

        if (!flipped && (up > flip)) {
            full.href = imgdir + names[pos][1];
            full.target = names[pos][1];
            flipped = true;
        }

        if (up === 1.000) {
            return;
        }

        setTimeout(function () { fade(); }, steptime);
    }

    function run() {

        if (!initial && !paused) {
            cur = thumbs[pos];
            pos = (pos + 1) % names.length;
            if (pos === thumbs.length) {
                next = document.createElement("img");
                next.id = idprefix + pos;
                next.src = imgdir + names[pos][0];
                next.alt = "[unable to display picture]";
                full.appendChild(next);
                setopac(next.style, 0.000);
                next.style.marginLeft = "-" + width + "px";
                next.style.zIndex = names.length - pos - 1;
                thumbs.push(next);
            } else {
                next = thumbs[pos];
            }
            curstep = 1;
            flipped = false;
            setsteps(false);
            fade();
        }

        initial = false;

        setTimeout(function () { run(); }, holdtime);
    }

    function noprop(e) {
        if (!e) {
            e = window.event;
        }
        e.cancelBubble = true;
        if (e.stopPropagation) {
            e.stopPropagation();
        }
    }
    function doAddHandler(obj, name, newhandler) {
        var curhandler = obj[name];
        if (curhandler === null) {
            obj[name] = newhandler;
        } else {
            obj[name] = function () {
                curhandler();
                newhandler();
            };
        }
    }

    function pause(e) {
        noprop(e);
        if (paused) {
            return;
        }
        paused = true;
    }

    function resume(e) {
        noprop(e);
        if (!paused) {
            return;
        }
        paused = false;
    }

    function init() {

        pos = 0;

        full = document.getElementById(fullid);
        full.target = names[pos][1];

        cur = document.getElementById(idprefix + '0');
        names.splice(0, 0, [getname(cur.src), getname(full.href)]);
        //width = cur.clientWidth;
        thumbs = [];
        setopac(cur.style, 1.000);
        cur.style.zIndex = names.length - 1;
        thumbs.push(cur);

        next = null;
        initial = true;
        paused = false;
        setsteps(fadetime, steps);

        doAddHandler(full, "onmouseover", pause);
        doAddHandler(full, "onmouseout", resume);

        run();
    }

    doAddHandler(window, "onload", init);
}

slides("./images/", "slideLarge", "slideSmall", 650, 5200, 1750, 80, 700, 70, 0.6, [["SideViewSmall.jpg", "SideView.jpg"], ["RearViewSmall.jpg", "RearView.jpg"]]);
//-->
</script>

CSS:
#slides {
    float:left;
    width:650px;
    padding:0px;
    margin-left:100px;
    display:inline;
}

#slides img {
    float:left;
    border:0px;
    position:relative;
}

Saturday, June 2, 2012

A Utility To Inline Shared HTML

A website typically has sections that are shared among multiple pages. For example, a common header and footer. It makes sense to factor the shared sections into separate included files. But the include mechanism is not standard: some servers support SSI, other servers require using PHP includes or JSP, still others have no server-side support for includes and it has to be done with JS writes. None of these approaches seem satisfactory. When using a not-universally supported server-side mechanism, files may require rewriting when changing hosting providers. When using a client-side mechanism, there are more requests to the server. This utility eliminates this problem by inlining included HTML, CSS, and JavaScript before deployment:
/**
 * Copyright (c) 2012 jimandlisa LLC. All rights reserved.
 * This program is made available under the terms of the
 * Eclipse Public License v1.0, which is available at
 * http://www.opensource.org/licenses/eclipse-1.0.php.
 */

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Given a directory of source for a website, recursively navigates the directory structure,
 * inlining included files (which may in turn include files), and saving the result to a staging
 * directory. This eliminates the need for include-file solutions such as SSI, PHP, or Javascript.
 * It also speeds up page loads and eliminates requests. Included files must be located in a directory
 * called "includes" located in the root of the directory containing the source. Include directives
 * must have the prefix and suffix pairs defined by INCLUDES_DELIMITERS, and the name of the included
 * file must be between the left and right parentheses defined by the prefix and suffix. Included files
 * must have simple names (no directories in the names), and must contain text. Does not process individual
 * files, only the entire site; for large sites this would not be acceptable, but for our application
 * it only takes ~30 ms. Included files must form a directed acyclic graph (no cycles); the utility does
 * not perform cycle detection, but by going into an infinite loop it's pretty obvious when there is a cycle,
 * and the log output makes it easy to determine the cause.
 * @author Jim Showalter
 */
public class Includer {
    
    /**
     * Reserved name of includes directory. Must be contained directly within source directory.
     * Could of course be turned into an argument to the program, but was not necessary for
     * our application.
     */
    private static final String INCLUDES_DIR = "includes";
    
    /**
     * Set of types that support includes. Must all be text format.
     */
    private static final Set<String> INCLUDING_TYPES = new HashSet<String>();
    
    static {
        // Initialize including types.
        INCLUDING_TYPES.add("html");
        INCLUDING_TYPES.add("css");
        INCLUDING_TYPES.add("js");
    }
    
    /**
     * Allowable prefixes for includes. Must match pairwise with allowable suffixes.
     */
    private static final List<String[]> INCLUDES_DELIMITERS = new ArrayList<String[]>();
    
    static {
        // Initialize includes delimiters.
        INCLUDES_DELIMITERS.add(new String[] {"<!--INCLUDE(\"", "\")INCLUDE-->"}); // HTML
        INCLUDES_DELIMITERS.add(new String[] {"/*--INCLUDE(\"", "\")INCLUDE--*/"}); // CSS
        INCLUDES_DELIMITERS.add(new String[] {"//--INCLUDE(\"", "\")INCLUDE--//"}); // JavaScript
    }
    
    /**
     * Outputs usage error and exits program with error code.
     * @param explanation Explanation of error.
     */
    private static void usage(String explanation) {
        System.out.println(explanation);
        System.out.println(Includer.class.getSimpleName() + ": <source directory> <target directory>");
        System.exit(-1);
    }
    
    /**
     * Determines longest common prefix substring for a pair of paths. Used to shorten log messages.
     * @param sourcePath Source path.
     * @param targetPath Target path.
     * @return Longest common prefix.
     */
    private static String longestCommonPrefix(String sourcePath, String targetPath) {
        
        String longerString = null;
        String shorterString = null;
        
        if (sourcePath.length() > targetPath.length()) {
            longerString = sourcePath;
            shorterString = targetPath;
        } else {
            longerString = targetPath;
            shorterString = sourcePath;
        }
        
        String longestCommonPrefix = null;
        
        for (int i = 0; i < shorterString.length(); i++) {
            if (shorterString.charAt(i) != longerString.charAt(i)) {
                longestCommonPrefix = shorterString.substring(0, i - 1);
                break;
            }
        }
        
        return longestCommonPrefix;
    }
    
    /**
     * Shortens path by removing the longest prefix it has in common with another path.
     * @param path Path to shorten.
     * @param longestCommonPrefix Longest common prefix.
     * @return Shortened path.
     */
    private static String shorten(String path, String longestCommonPrefix) {
        return path.substring(longestCommonPrefix.length(), path.length());
    }
    
    /**
     * Logs a progress message.
     * @param prefix Beginning of logged message.
     * @param source Source file or directory.
     * @param longestCommonPrefix Longest common prefix.
     */
    private static void log(String prefix, File source, File target, String longestCommonPrefix) {
        String sourcePath = shorten(source.getAbsolutePath(), longestCommonPrefix);
        String targetPath = shorten(target.getAbsolutePath(), longestCommonPrefix);
        System.out.println(prefix + " copy: " + sourcePath + " => " + targetPath);
    }

    /**
     * Determines whether file name ends with a suffix that is one of the including types.
     * @param fileName File name to check.
     * @return True if file is an including type, false otherwise.
     */
    private static boolean isIncludingType(String fileName) {
        
        int dotIndex = fileName.lastIndexOf('.');
        
        if (dotIndex == -1) {
            return false;
        }
        
        String extension = fileName.substring(dotIndex + 1);
        
        return INCLUDING_TYPES.contains(extension);
    }
    
    /**
     * Extracts name of included file from current line if it is an include directive.
     * @param line Line.
     * @return Include file name, or null if current line is not an include directive.
     */
    private static String includeFileName(String line) {
        String trimmedLine = line.trim();
        for (String[] delimiters : INCLUDES_DELIMITERS) {
            String prefix = delimiters[0];
            String suffix = delimiters[1];
            if (trimmedLine.startsWith(prefix) && trimmedLine.endsWith(suffix)) {
                return trimmedLine.substring(prefix.length(), trimmedLine.length() - suffix.length());
            }
        }
        return null;
    }

    /**
     * Copies a file that does not have includes. Does not use NIO's transferTo or other new mechanisms
     * because, depending on file size, OS, etc., performance can actually wind up worse.
     * @param sourceFile File to copy from.
     * @param targetFile File to copy to.
     * @param longestCommonPrefix Longest common prefix.
     * @throws IOException Problem encountered opening, reading, creating, writing, or closing files.
     */
    private static void simpleCopy(File sourceFile, File targetFile, String longestCommonPrefix) throws IOException {
        
        log("simple", sourceFile, targetFile, longestCommonPrefix);
        
        InputStream in = new FileInputStream(sourceFile);
        OutputStream out = new FileOutputStream(targetFile);
        byte[] buf = new byte[1024*10];
        int len;
        while ((len = in.read(buf)) > 0) {
           out.write(buf, 0, len);
        }
        in.close();
        out.close();
    }
    
    /**
     * Platform-specific line separator.
     */
    private static final String LINE_SEPARATOR = System.getProperty("line.separator");
    
    /**
     * Reads contents of include file into memory, and caches it so it doesn't have to be read again.
     * For large sites this could be too much of a memory hog, in which case an eviction strategy
     * could be implemented (for example, keep the most-often referenced files).
     * @param includesDirName Name of includes directory.
     * @param includeFileName Name of include file.
     * @param includeFiles Cache of already included files.
     * @return Contents of include file.
     * @throws IOException Problem encountered opening, reading, or closing include file.
     */
    private static StringBuilder get(String includesDirName, String includeFileName,  Map<String, StringBuilder> includeFiles) throws IOException {
        
        System.out.println("get: " + includeFileName);

        StringBuilder includeFileContents = includeFiles.get(includeFileName);
        
        if (includeFileContents == null) {
            includeFileContents = new StringBuilder();
            File includeFile = new File(includesDirName + File.separator + INCLUDES_DIR + File.separator + includeFileName);
            BufferedReader reader = new BufferedReader(new InputStreamReader(new DataInputStream(new FileInputStream(includeFile))));
            
            String line;
            while ((line = reader.readLine()) != null) {
                String subIncludeFileName = includeFileName(line);
                if (subIncludeFileName != null) {
                    StringBuilder subIncludeFileContents = get(includesDirName, subIncludeFileName, includeFiles);
                    includeFileContents.append(subIncludeFileContents.toString());
                } else {
                    includeFileContents.append(line);
                    includeFileContents.append(LINE_SEPARATOR);
                }
            }
            
            reader.close();
            includeFiles.put(includeFileName, includeFileContents);
            System.out.println("put: " + includeFileName);
        }
        
        return includeFileContents;
    }

    /**
     * Copies a file that might have includes.
     * @param sourceFile File to copy from.
     * @param targetFile File to copy to.
     * @param longestCommonPrefix Longest common prefix.
     * @param includeFiles Cache of already included files.
     * @throws IOException Problem encountered opening, reading, creating, writing, or closing files.
     */
    private static void includeCopy(File sourceFile, File targetFile, String longestCommonPrefix, Map<String, StringBuilder> includeFiles) throws IOException {
        
        log("file", sourceFile, targetFile, longestCommonPrefix);

        BufferedReader reader = new BufferedReader(new InputStreamReader(new DataInputStream(new FileInputStream(sourceFile))));
        BufferedWriter writer = new BufferedWriter(new FileWriter(targetFile));

        String line;
        while ((line = reader.readLine()) != null) {
            String includeFileName = includeFileName(line);
            if (includeFileName != null) {
                StringBuilder includeFileContents = get(sourceFile.getParent(), includeFileName, includeFiles);
                writer.write(includeFileContents.toString());
            } else {
                writer.write(line);
                writer.newLine();
            }
        }

        reader.close();
        writer.close();
    }
    
    /**
     * Copies a source directory to a target directory, recursively, resolving any included files it encounters.
     * @param sourceDir Directory to copy from.
     * @param targetDir Directory to copy to.
     * @param longestCommonPrefix Longest common prefix. (For logging, to shorten messages.)
     * @param includeFiles Cache of already included files.
     * @throws IOException Problem encountered opening, reading, creating, writing, or closing files.
     */
    private static void copy(File sourceDir, File targetDir, String longestCommonPrefix, Map<String, StringBuilder> includeFiles) throws IOException {
        
        log("directory", sourceDir, targetDir, longestCommonPrefix);
        
        if (!targetDir.exists()) {
            targetDir.mkdirs();
        }
        
        File[] files = sourceDir.listFiles();
        for (File file : files) {
            if (file.isDirectory()) {
                File dir = file;
                String dirName = dir.getName();
                if (dirName.compareTo(INCLUDES_DIR) != 0) {
                    File subTargetDir = new File(targetDir.getAbsolutePath() + File.separator + dirName);
                    copy(dir, subTargetDir, longestCommonPrefix, includeFiles);
                }
            } else {
                String fileName = file.getName();
                String targetFileName = targetDir.getAbsolutePath() + File.separator + fileName;
                File targetFile = new File(targetFileName);
                if (!isIncludingType(fileName)) {
                    simpleCopy(file, targetFile, longestCommonPrefix);
                } else {
                    includeCopy(file, targetFile, longestCommonPrefix, includeFiles);
                }
            }
        }
    }
    
    /**
     * Main entry point for program.
     * @param args Command-line arguments.
     * @throws IOException Problem encountered opening, reading, creating, writing, or closing files.
     */
    public static void main(String[] args) throws IOException {
        
        // Validate the args.
        
        if (args == null) {
            usage("Null args");
        }
        
        if (args.length != 2) {
            usage("Wrong number of args");
        }
        
        String sourceDirName = args[0];
        
        if (sourceDirName == null) {
            usage("Null source directory");
        }
        
        if (sourceDirName.trim().length() == 0) {
            usage("Blank source directory");
        }
        
        String targetDirName = args[1];
        
        if (targetDirName == null) {
            usage("Null target directory");
        }
        
        if (targetDirName.trim().length() == 0) {
            usage("Blank target directory");
        }
        
        if (sourceDirName.startsWith(targetDirName)) {
            usage("Source directory child of target directory");
        }
        
        if (targetDirName.startsWith(sourceDirName)) {
            usage("Target directory child of source directory");
        }
        
        File source = new File(sourceDirName);
        
        if (!source.exists()) {
            usage("Non-existent source directory");
        }
        
        if (!source.isDirectory()) {
            usage("Non-directory source directory");
        }
        
        // Initialize and perform the copying.
        
        File target = new File(targetDirName);
        
        Map<String, StringBuilder> includeFiles = new HashMap<String, StringBuilder>();
        
        long start = System.currentTimeMillis();
        
        try {
            copy(source, target, longestCommonPrefix(sourceDirName, targetDirName), includeFiles);
        } catch (Exception e) {
            System.out.println("Encountered problem copying source to target: " + e.getMessage());
            e.printStackTrace();
            System.exit(-1);
        }
        
        long end = System.currentTimeMillis();
        
        System.out.println("done in " + (end - start) + " ms");
    }
}
An example file set up to be processed by the utility looks like this:
<!--INCLUDE("top.html")INCLUDE-->

<body id="manual">

<div id="header" class="header">
<h1 class="header">User Manual</h1>
</div>

<!--INCLUDE("nav.html")INCLUDE-->

<div>
<p>TODO: Put safety instructions and user manual here.</p>
</div>

<!--INCLUDE("bottom.html")INCLUDE-->

Sunday, February 19, 2012

A Comparison Of Various Software Revenue Models

One of our first tasks at yumpty.com is figuring out what revenue model to use.

To do that, I put together a comparison of various options.

What did I miss?

NameStrategyProsConsNotes
Old SchoolCharge a lot for a license, and a lot for upgrades and support.Lots of revenue (if not profit) from each sale. Captive user base if you can get away with proprietary data formats. Recurring revenue stream if you can keep convincing users to upgrade.Horribly expensive up-front development costs. Enterprise/government sales. Four-legged sales calls. Weird revenue-recognition issues limit ability to please users with new features outside of planned (and charged-for) upgrades. Users can wake up one day and realize they're paying way too much for your software and start looking for alternatives. License schemes are inevitably hacked.Works well for professional software that performs highly specialized tasks and that needs skilled support (CAD/CAM, circuit design, etc.). Licenses can be node-locked or floating.
Pay OnceCharge a lot for a license, but provide perpetual upgrades and support for free.Customer loyalty after getting them to cough up the one-time payment.No additional revenue from customers, despite their loyalty. Better hope you can continue to make more new sales.
Worry About It LaterGive the software away in order to get network effects, then figure out how to make money.Everyone says this is the way to start software companies these days. Easy to get VC buy-in. Speeds time to market because v1 of product doesn't have to be very good because it's free.Running out of money while waiting for network effects. Giving up bulk of control of company to VCs, because they're the only people with enough money to pay for the overhead (including ever-increasing support costs) while waiting for network effects. Few areas remain in which there is not already a clear winner of network-effects race.
Free With AdsGive the software away in order to get network effects, and charge for ads.Same Pros as "Worry About It Later". Worked great for Google.All of the Cons of "Worry About It Later" (because ads won't pay until network effects kick in), plus: Ads annoy users, and can look cheesy, reducing appeal of your site. If site provides a service to a company, ads for company's competitors annoy company.
"Freemium"Give the software away except for premium features, and charge for those.Same Pros as "Deferred Monitization". Free gets the network effects, while charging generates revenue. Worked for Evernote.Have to be extremely careful deciding where to draw the line between free and premium features; get that wrong, and either free won't be usable, or it will be so useful nobody needs premium (in which case site degenerates into entirely free).Fees are typically a monthly subscription, but other approaches could be used. Fun fact: Evernote was 15 minutes from bankruptcy before being saved by an investor.
TieredCharge for different levels of features and/or service, but always charge something.Always generates revenue (if not profits). Entry-level tier can be very inexpensive, but still cost enough to avoid the Cons of the various free approaches.Users may only opt for lowest tier, reducing profitability.Fees are typically a monthly subscription, but other approaches could be used.
Flat RateCharge a single fixed rate for unlimited access to all features.Simple for users to understand. Always generates revenue (if not profits).Users that only want a small subset of the features feel they are subsidizing the power users.Fees are typically a monthly subscription, but other approaches could be used.
CrippledGive away a version of the software that is missing key features and/or has limits on size, execution speed, number of calculations, support, etc. Hope users see enough potential in the software they pay to remove limits.If limits are lax enough for users to get some decent experience with the software, they may not need anything more than that. If limits are strict, users cannot test out the software to see if it will actually scale. License schemes are inevitably hacked.Freemium-on-the-desktop?
Time's UpProvide a fully featured version of the software that stops working when the trial period expires. Hope users see enough value in the software they pay for it.Users aren't hampered by limits, and are able to really test the software.Users may only need to use the software for a brief period, which they can do without paying anything. License schemes are inevitably hacked. Time-based schemes can be defeated by running in a VM with a skewed clock.
Nag, Nag, NagProvide a fully featured version of the software that pops up an annoying modal dialog on launch. Hope users see enough value in the software, and are sufficiently annoyed by the modal dialog, to pay to make the dialog go away.Users aren't hampered by limits, and are able to really test the software.Users resent being deliberately annoyed. Scheme for suppressing modal dialog can be hacked.A variant of deliberate annoyance: give away a fully featured version of the software that has ads, and offer an option to pay to turn off the ads.
Guilt TripGive the software away, but ask for donations.Works great for wikipedia.Appeals to users' better nature, which may or may not be effective. No guarantee of running without losses.Can be simple donate button, or concentrated donation drive (like wikipedia).
Per DrinkCharge a tiny amount for each operation performed by user.Nicely fits scalable cloud-computing hosting model, because revenue growth and hosting charges are both fine-grained. Tininess of charged amount can entice users into signing up.Users underutilize product to avoid charges, which diminishes acceptance. Users don't like not knowing how much they'll wind up owing. Some users feel like they're being nickel-and-dimed.Probably a better fit for service APIs than for end-user UIs.
PlatformBuild a platform and share revenue with companies that write applications for the platform.Worked for Salesforce.Nobody will write apps for a platform that isn't popular, and a platform won't be popular until it has apps. Which means many of the same Cons as "Worry About It Later"
ConsultingGive the software away, but charge for consulting.Consulting rates can be pretty good, particularly if the software becomes popular. Being free, the software stands a chance of becoming popular.Scale of company is limited by rate at which qualified consultants can be added.A good model for an independent software developer.
Support OnlyProvide support for a product sold by some other company.No software to write or maintain.Company selling the product may fight you, because they intended to make money on support. Same scalability problems as "Consulting".

Saturday, January 28, 2012

The Tyranny Of Extroverts

Here's a book that provides insight into why some people love open cubicles and a highly collaborative/noisy environment, and others don't:


It's unfortunate that the extroverts got the upper hand, because it's easy to work as an extrovert in an environment designed for introverts, but the converse is not true.

The author lists some contributions made by introverts. For example, the theory of relativity. Who knows what other contributions we're forfeiting, or at least slowing down, by creating environments in which introverts cannot be productive?