public static Location calculateLocation(final long byteOffset, final boolean isOnFistCharInHexPair) {

        if (byteOffset < 0) {
            throw new IllegalArgumentException(LogUtil.message("Invalid byteOffset {}. Must be >= 0", byteOffset));
        }

        final int lineNo = (int) (byteOffset / HexDump.MAX_BYTES_PER_LINE + 1); // one based
        final int byteOffsetOnLine = (int) (byteOffset % HexDump.MAX_BYTES_PER_LINE);
        final int blockOffsetOnLine = byteOffsetOnLine / BYTES_PER_BLOCK;
        final int charsPerByte = 3; // Two for the hex pair + one space
        final int colNo = PADDED_LINE_NO_WIDTH // left hand line no col
                + SPACER_AFTER_PADDED_LINE_NO.length()
                + (byteOffsetOnLine * charsPerByte) //
                + blockOffsetOnLine // One extra space after each 4 byte block
                + 1; // one based

        LOGGER.debug("lineNo: {}, colNo: {}, byteOffsetOnLine: {}, blockOffsetOnLine: {}",
                lineNo, colNo, byteOffsetOnLine, blockOffsetOnLine);

        return isOnFistCharInHexPair
                ? DefaultLocation.of(lineNo, colNo)
                : DefaultLocation.of(lineNo, colNo + 1);
    }
Calculates the line/col position (in the rendered hex dump) of a given byte offset.
The line/col position is of one of the 2 characters in the hex pair that corresponds to the
given byte offset. Use {@code isOnFistCharInHexPair} to set whether it returns the location
of the first or second char in this pair.
This method knows nothing about the hex dump in question so if byteOffset is greater
than the number of bytes in the dump then the returned {@link Location} will also
be invalid. It also assumes that whatever is displaying the hex dump is showing all of it
up to and including this byte or is only showing some but has had its line numbers adjusted
accordingly.

@param isOnFistCharInHexPair If true returns first char of the hex pair, else the second.
stroom-util/src/main/java/stroom/util/string/HexDumpUtil.java