Source: formats/shapefile/DBaseFile.js

/*
 * Copyright 2015-2017 WorldWind Contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/**
 * @exports DBaseFile
 */
define(['../../error/ArgumentError',
        '../../util/ByteBuffer',
        '../../formats/shapefile/DBaseField',
        '../../formats/shapefile/DBaseRecord',
        '../../util/Logger'
    ],
    function (ArgumentError,
              ByteBuffer,
              DBaseField,
              DBaseRecord,
              Logger) {
        "use strict";

        /**
         * Constructs an object for dBase file at a specified URL. Applications typically do not call this constructor.
         * It is called by {@link {Shapefile} to read attributes for shapes.
         * @alias DBaseFile
         * @constructor
         * @classdesc Parses a dBase file.
         * @param {String} url The location of the dBase file.
         * @throws {ArgumentError} If the specified URL is null or undefined.
         */
        var DBaseFile = function(url) {
            if (!url) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "DBaseFile", "constructor", "missingUrl"));
            }

            this.url = url;

            // Internal use only. Intentionally not documented.
            // DBase file data.
            this.header = null;
            this.fields = null;

            // Internal use only. Intentionally not documented.
            // The buffer descriptor to read records from.
            this.buffer = null;

            // Internal use only. Intentionally not documented.
            // Source read parameters.
            this.boolean = true;
            this.numRecordsRead = 0;

            this._completionCallback = null;
        };

        /**
         * The modification date of the the dBase file.
         * @returns {String} The modification date.
         */
        DBaseFile.prototype.getLastModificationDate = function() {
            return this.header.lastModificationDate;
        };

        /**
         * The number of records in the dBase file.
         * @returns {Number} The number of records.
         */
        DBaseFile.prototype.getNumberOfRecords = function() {
            return this.header.numberOfRecords;
        };

        /**
         * The length of the header of the dBase file.
         * @returns {Number} The length of the header.
         */
        DBaseFile.prototype.getHeaderLength = function() {
            return this.header.headerLength;
        };

        /**
         * The length of a record in the dBase file.
         * @returns {Number} The lenght of a recrod.
         */
        DBaseFile.prototype.getRecordLength = function() {
            return this.header.recordLength;
        };

        /**
         * The number of fields in a dBase file.
         * @returns {Number} The number of fields.
         */
        DBaseFile.prototype.getNumberOfFields = function() {
            return (this.header.headerLength - 1 - DBaseFile.FIXED_HEADER_LENGTH) / DBaseFile.FIELD_DESCRIPTOR_LENGTH;
        };

        /**
         * The field descriptors of the dBase file.
         * @returns {DBaseField[]} The field descriptors.
         */
        DBaseFile.prototype.getFields = function() {
            return this.fields;
        };

        /**
         * Indicates whether the dBase file has additional records to read.
         * @returns {Boolean} True if more records can be read.
         */
        DBaseFile.prototype.hasNext = function() {
            return this.numRecordsRead < this.header.numberOfRecords;
        };

        /**
         * Read the next record in the dBase file.
         * @returns {DBaseRecord} The next record.
         */
        DBaseFile.prototype.nextRecord = function() {
            if (this.numRecordsRead >= this.getNumberOfRecords()) {
                return null;
            }

            return this.readNextRecord(this._buffer, ++this.numRecordsRead);
        };

        //**************************************************************//
        //********************  Initialization  ************************//
        //**************************************************************//

        /**
         * Initiate loading of the dBase file.
         * @param completionCallback
         */
        DBaseFile.prototype.load = function(completionCallback) {
            this._completionCallback = completionCallback;

            this.requestUrl(this.url);
        };

        /**
         * Internal use only.
         * Request data from the URL.
         * @param {String} url The URL for the requested data.
         */
        DBaseFile.prototype.requestUrl = function(url) {
            var xhr = new XMLHttpRequest();

            xhr.open("GET", url, true);
            xhr.responseType = 'arraybuffer';
            xhr.onreadystatechange = (function () {
                if (xhr.readyState === 4) {
                    if (xhr.status === 200) {
                        this._buffer = new ByteBuffer(xhr.response);

                        this.parse();

                        if (!!this._completionCallback) {
                            this._completionCallback(this);
                        }
                    }
                    else {
                        Logger.log(Logger.LEVEL_WARNING,
                            "DBaseFile retrieval failed (" + xhr.statusText + "): " + url);

                        if (!!this._completionCallback) {
                            this._completionCallback(this);
                        }
                    }
                }
            }).bind(this);

            xhr.onerror = function () {
                Logger.log(Logger.LEVEL_WARNING, "DBaseFile retrieval failed: " + url);

                if (!!this._completionCallback) {
                    this._completionCallback(this);
                }
            };

            xhr.ontimeout = function () {
                Logger.log(Logger.LEVEL_WARNING, "DBaseFile retrieval timed out: " + url);

                if (!!this._completionCallback) {
                    this._completionCallback(this);
                }
            };

            xhr.send(null);
        };

        /**
         * Parse the dBase file.
         */
        DBaseFile.prototype.parse = function() {
            this.header = this.readHeader(this._buffer);
            this.fields = this.readFieldDescriptors(this._buffer, this.getNumberOfFields());
        };

        //**************************************************************//
        //********************  Header  ********************************//
        //**************************************************************//

        /**
         * Read the header of the dBase file.
         * @param {ByteBuffer} buffer The buffer descriptor to read from.
         * @returns {{
         *      fileCode: Number,
         *      lastModificationDate: {year: number, month: number, day: Number},
         *      numberOfRecords: Number,
         *      headerLength: Number,
         *      recordLength: Number
         * }}
         */
        DBaseFile.prototype.readHeader = function(buffer) {
            var pos = buffer.position;

            buffer.order(ByteBuffer.LITTLE_ENDIAN);

            // Read file code - first byte
            var fileCode = buffer.getByte();
            if (fileCode > 5) {
                // Let the caller catch and log the message.
                // TODO: ??? determine correct type of error
                throw new Error("???");
                //throw new WWUnrecognizedException(Logging.getMessage("SHP.UnrecognizedDBaseFile", fileCode));
            }

            // Last update date
            var yy = buffer.getByte();
            var mm = buffer.getByte();
            var dd = buffer.getByte();

            // Number of records
            var numRecords = buffer.getInt32();

            // Header struct length
            var headerLength = buffer.getInt16();

            // Record length
            var recordLength = buffer.getInt16();

            var date = {
                year: 1900 + yy,
                month: mm - 1,
                day: dd
            };

            // Assemble the header.
            var header = {
                'fileCode': fileCode,
                'lastModificationDate': date,
                'numberOfRecords': numRecords,
                'headerLength': headerLength,
                'recordLength': recordLength
            };

            buffer.seek(pos + DBaseFile.FIXED_HEADER_LENGTH); // Move to end of header.

            return header;
        };

        //**************************************************************//
        //********************  Fields  ********************************//
        //**************************************************************//

        /**
         * Reads a sequence of {@link DBaseField} descriptors from the given buffer;
         * <p/>
         * The buffer current position is assumed to be set at the start of the sequence and will be set to the end of the
         * sequence after this method has completed.
         *
         * @param {ByteBuffer} buffer    A byte buffer descriptor to read from.
         * @param {Number} numFields The number of DBaseFields to read.
         *
         * @return {DBaseField[]} An array of {@link DBaseField} instances.
         */
        DBaseFile.prototype.readFieldDescriptors = function(buffer, numFields) {
            var pos = buffer.position;

            var fields = [];

            for (var i = 0; i < numFields; i += 1) {
                fields[i] = new DBaseField(this, buffer);
            }

            var fieldsLength = this.header.headerLength - DBaseFile.FIXED_HEADER_LENGTH;
            buffer.seek(pos + fieldsLength); // Move to end of fields.

            return fields;
        };

        //**************************************************************//
        //********************  Records  *******************************//
        //**************************************************************//

        /**
         * Reads a {@link DBaseRecord} instance from the given buffer;
         * <p/>
         * The buffer current position is assumed to be set at the start of the record and will be set to the start of the
         * next record after this method has completed.
         *
         * @param {ByteBuffer} buffer       The buffer descriptor to read from.
         * @param {Number} recordNumber The record's sequence number.
         *
         * @return {DBaseRecord} A {@link DBaseRecord} instance.
         */
        DBaseFile.prototype.readNextRecord = function(buffer, recordNumber) {
            return new DBaseRecord(this, buffer, recordNumber);
        };

        //**************************************************************//
        //********************  String Parsing  ************************//
        //**************************************************************//

        /**
         * Read a null-terminated string.
         * @param {ByteBuffer} buffer A buffer descriptor to read from.
         * @param {Number} maxLength The number of maximum characters.
         * @returns {String}
         */
        DBaseFile.prototype.readNullTerminatedString = function(buffer, maxLength) {
            if (maxLength <= 0) {
                return 0;
            }

            var string = "";

            for (var length = 0; length < maxLength; length += 1) {
                var byte = buffer.getByte();
                if (byte == 0) {
                    break;
                }
                string += String.fromCharCode(byte);
            }

            if (this.isStringEmpty(string))
                return "";

            return string;
        };

        /**
         * Indicate whether the string is "logically" empty in the dBase sense.
         * @param {String} string The string of characters.
         * @returns {Boolean} True if the string is logically empty.
         */
        DBaseFile.prototype.isStringEmpty = function(string) {
            return string.length <= 0 ||
                DBaseFile.isStringFilled(string, 0x20) || // Space character.
                DBaseFile.isStringFilled(string, 0x2A); // Asterisk character.
        };

        /**
         * Indicates if the string is filled with constant data of a particular kind.
         * @param {String} string The string of characters.
         * @param {Number} fillValue The character value to test.
         * @returns {Boolean} True if the character array is filled with the specified value.
         */
        DBaseFile.isStringFilled = function(string, fillValue) {
            if (string.length <= 0) {
                return false;
            }

            for (var i = 0; i < string.length; i++) {
                if (string.charAt(i) != fillValue)
                    return false;
            }

            return true;
        };

        /**
         * The length of a dBase file header.
         * @type {Number}
         */
        DBaseFile.FIXED_HEADER_LENGTH = 32;

        /**
         * The length of a dBase file field descriptor.
         * @type {Number}
         */
        DBaseFile.FIELD_DESCRIPTOR_LENGTH = 32;

        return DBaseFile;
    }
);