Source: formats/shapefile/ShapefileRecord.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 ShapefileRecord
 */
define([
        '../../geom/Angle',
        '../../error/ArgumentError',
        '../../util/ByteBuffer',
        '../../geom/Location',
        '../../util/Logger',
        '../../formats/shapefile/Shapefile'
    ],
    function (Angle,
              ArgumentError,
              ByteBuffer,
              Location,
              Logger,
              Shapefile) {
        "use strict";

        /**
         * Constructs a shapefile record. Applications typically do not call this constructor. It is called by
         * {@link Shapefile} as shapefile records are read.
         * @alias ShapefileRecord
         * @constructor
         * @classdesc Contains the data associated with a shapefile record.
         * @param {Shapefile} shapefile The shapefile containing this record.
         * @param {ByteBuffer} buffer The buffer descriptor of the shapefile record's contents.
         * @throws {ArgumentError} If either the specified shapefile or buffer are null or undefined.
         */
        var ShapefileRecord = function (shapefile, buffer) {
            if (!shapefile) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "ShapefileRecord", "constructor",
                        "The specified shapefile is null or undefined"));
            }

            if (!buffer) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "ShapefileRecord", "constructor",
                        "The specified buffer is null or undefined"));
            }

            // All these are documented in their property definition below. All but the shapefile and point buffer
            // are determined when the record is read by this class.
            this._shapefile = shapefile;
            this._recordNumber = -1;
            this._attributes = {};
            this._numberOfParts = 0;
            this._firstPartNumber = 0;
            this._lastPartNumber = 0;
            this._numberOfPoints = 0;
            this._boundingRectangle = [];
            this._zRange = null;
            this._zValues = null;
            this._mRange = null;
            this._mValues = null;

            // Internal use only. Intentionally not documented.
            this._contentLengthInBytes = -1;

            // Internal use only. Intentionally not documented.
            this._buffer = buffer;

            // Internal use only. Intentionally not documented.
            this._isNormalized = false;

            // Internal use only. Intentionally not documented.
            this._parts = [];

            // Prime the input pump.
            this.readRecord();
        };

        Object.defineProperties(ShapefileRecord.prototype, {
            /**
             * The shapefile containing this record.
             * @memberof ShapefileRecord.prototype
             * @type {Shapefile}
             * @readonly
             */
            shapefile: {
                get: function () {
                    return this._shapefile;
                }
            },

            /**
             * This record's ordinal position in the shapefile. 0 indicates the first record in the shapefile.
             * @memberof ShapefileRecord.prototype
             * @type {Number}
             * @readonly
             */
            recordNumber: {
                get: function () {
                    return this._recordNumber;
                }
            },

            /**
             * The attributes associated with this record, as read from the attribute file associated with the
             * shapefile. Empty if there are no attributes associated with this record or with the shapefile.
             * @memberof ShapefileRecord.prototype
             * @type {Object}
             * @readonly
             */
            attributes: {
                get: function () {
                    return this._attributes;
                }
            },

            /**
             * The number of parts in the shapefile.
             * @memberof ShapefileRecord.prototype
             * @type {Number}
             * @readonly
             */
            numberOfParts: {
                get: function () {
                    return this._numberOfParts;
                }
            },

            /**
             * The first part number in the record.
             * @memberof ShapefileRecord.prototype
             * @type {Number}
             * @readonly
             */
            firstPartNumber: {
                get: function () {
                    return this._firstPartNumber;
                }
            },

            /**
             * The last part number in the record.
             * @memberof ShapefileRecord.prototype
             * @type {Number}
             * @readonly
             */
            lastPartNumber: {
                get: function () {
                    return this._lastPartNumber;
                }
            },

            /**
             * The number of points in the record.
             * @memberof ShapefileRecord.prototype
             * @type {Number}
             * @readonly
             */
            numberOfPoints: {
                get: function () {
                    return this._numberOfPoints;
                }
            },

            /**
             * A four-element array containing this record's bounding rectangle, or null if this record has no
             * bounding rectangle. The returned array is ordered as follows: minimum Y, maximum Y, minimum X,
             * maximum X. If the shapefile's coordinate system is geographic then the elements can be interpreted
             * as angular degrees in the order minimum latitude, maximum latitude, minimum longitude, maximum
             * longitude.
             * @memberof ShapefileRecord.prototype
             * @type {Number[]}
             * @readonly
             */
            boundingRectangle: {
                get: function () {
                    return this._boundingRectangle;
                }
            },

            /**
             * The record's Z range if the shapefile's shape type is a Z type, otherwise null.
             * @memberof ShapefileRecord.prototype
             * @type {Number[]}
             * @readonly
             */
            zRange: {
                get: function () {
                    return this._zRange;
                }
            },

            /**
             * The record's Z values if the shapefile's shape type is a Z type, otherwise null.
             * @memberof ShapefileRecord.prototype
             * @type {Number[]}
             * @readonly
             */
            zValues: {
                get: function () {
                    return this._zValues;
                }
            },

            /**
             * The record's M range if the shapefile's shape type is an M type, otherwise null.
             * @memberof ShapefileRecord.prototype
             * @type {Number[]}
             * @readonly
             */
            mRange: {
                get: function () {
                    return this._mRange;
                }
            },

            /**
             * The record's M values if the shapefile's shape type is an M type, otherwise null.
             * @memberof ShapefileRecord.prototype
             * @type {Number[]}
             * @readonly
             */
            mValues: {
                get: function () {
                    return this._mValues;
                }
            }
        });

        /**
         * Returns the points of a specified part of this record.
         * @param {Number} partNumber The part number of interest. The range of part numbers can be determined via
         * [firstPartNumber]{@link ShapefileRecord#firstPartNumber} and
         * [lastPartNumber]{@link ShapefileRecord#lastPartNumber}.
         * @returns {Float64Array} The part's points in the order X0, Y0, X1, Y1, ..., Xn, Yn, where n is the number
         * of points in the part minus one. Returns null if the specified part does not exist.
         */
        ShapefileRecord.prototype.pointBuffer = function (partNumber) {
            if (partNumber >= 0 && partNumber < this._parts.length) {
                return this._parts[partNumber];
            }
            else {
                return null;
            }
        };

        ShapefileRecord.prototype.readRecord = function() {
            this.readHeader();

            // Technically, the shape type in the record header is considered a part of the contents according to the
            // ESRI specification. However, every record has a shape type, so we will read before reading the record contents.
            // Read shape type as little endian.
            this._buffer.order(ByteBuffer.LITTLE_ENDIAN);
            var type = this._buffer.getInt32();
            var shapeType = this.shapefile.getShapeType(type);
            this.validateShapeType(shapeType);

            this.readContents();
        };

        /**
         * Reads and parses the contents of a shapefile record from a specified buffer. The buffer's current position must
         * be the start of the record and will be the start of the next record when the method returns.
         *
         */
        ShapefileRecord.prototype.readHeader = function() {
            // Read record number and record length - big endian.
            this._buffer.order(ByteBuffer.BIG_ENDIAN);
            this._recordNumber = this._buffer.getInt32();
            this._contentLengthInBytes = this._buffer.getInt32() * 2;
        };

        /**
         * Verifies that the record's shape type matches that of the shapefile. All non-null
         * records in a Shapefile must be of the same type. Throws an exception if the types do not match and the shape type
         * is not {@link Shapefile#NULL}. Records of type NULL are always valid, and
         * may appear in any Shapefile.
         * <p/>
         * For details, see the ESRI Shapefile specification at <a href="https://www.esri.com/library/whitepapers/pdfs/shapefile.pdf"/>,
         * pages 4 and 5.
         *
         * @throws Error If the shape types do not match.
         */
        ShapefileRecord.prototype.validateShapeType = function(shapeType) {
            if (shapeType !== this.shapefile.NULL &&
                shapeType !== this.shapefile.shapeType) {
                // TODO: throw the correct error
                throw new Error(
                    Logger.log(Logger.LEVEL_SEVERE, "Shapefile record is not supported.")
                );
            }
        };

        // Internal use only. Intentionally not documented.
        ShapefileRecord.prototype.readNullContents = function() {
            this._numberOfParts = 0;
            this._numberOfPoints = 0;
            this._parts = null;
            this._boundingRectangle = null;

            // Skip over the remaining contents of the record after the record's shape type.
            this._buffer.seek(this._contentLengthInBytes - ByteBuffer.INT32_SIZE);
        };

        // Internal use only. Intentionally not documented.
        ShapefileRecord.prototype.readPointContents = function() {
            this._numberOfParts = 1;
            this._firstPartNumber = 0;
            this._lastPartNumber = this._numberOfParts - 1;
            this._numberOfPoints = 1;

            this._parts = [this._buffer.getDoubleArray(2)];

            var latitude = this._parts[0][1];
            var longitude = this._parts[0][0];
            this._boundingRectangle = [latitude, latitude, longitude, longitude];

            // Read the optional Z value.
            if (this.isZType()) {
                this.readZ(true);
            }

            // Read the optional measure value.
            if (this.isMeasureType()) {
                this.readOptionalMeasures(true);
            }
        };

        // Internal use only. Intentionally not documented.
        ShapefileRecord.prototype.readPolylineContents = function() {
            // Read the bounding rectangle.
            var rect = this.shapefile.readBoundingRectangle(this._buffer);
            this._boundingRectangle = rect.coords;

            // Specify that the record's points should be normalized if the bounding rectangle is normalized. Ignore the
            // shapefile's normalizePoints property to avoid normalizing records that don't need it.
            this._isNormalized = rect.isNormalized;

            // Read the number of parts and the number of points.
            this._numberOfParts = this._buffer.getInt32();
            this._firstPartNumber = 0;
            this._lastPartNumber = this._numberOfParts - 1;
            this._numberOfPoints = this._buffer.getInt32();

            if (this._numberOfParts > 0 && this._numberOfPoints > 0) {
                // Read the part positions.
                var partPositions = this._buffer.getInt32Array(this.numberOfParts);

                for (var partNumber = 0; partNumber < this.numberOfParts; partNumber += 1) {
                    var numPointsInPart = (partNumber == this.numberOfParts - 1) ?
                        this._numberOfPoints - partPositions[partNumber] :
                        partPositions[partNumber + 1] - partPositions[partNumber];

                    // Add the record's points to the Shapefile's point buffer, and record this record's part offset in the
                    // Shapefile's point buffer.
                    this._parts[partNumber] = this._buffer.getDoubleArray(numPointsInPart * 2);
                    ShapefileRecord.normalizeLocations(this._parts[partNumber]);
                }
            }

            // Read the optional Z value.
            if (this.isZType()) {
                this.readZ(false);
            }

            // Read the optional measure value.
            if (this.isMeasureType()) {
                this.readOptionalMeasures(false);
            }
        };

        // Internal use only. Intentionally not documented.
        ShapefileRecord.prototype.readMultiPointContents = function() {
            // Read the bounding rectangle.
            var rect = this.shapefile.readBoundingRectangle(this._buffer);
            this._boundingRectangle = rect.coords;

            // Specify that the record's points should be normalized if the bounding rectangle is normalized. Ignore the
            // shapefile's normalizePoints property to avoid normalizing records that don't need it.
            this._isNormalized = rect.isNormalized;

            // Read the number of parts and the number of points.
            this._numberOfParts = 1;
            this._numberOfPoints = this._buffer.getInt32();

            this._parts = [this._buffer.getDoubleArray(this._numberOfPoints * 2)];
            ShapefileRecord.normalizeLocations(this._parts[0]);

            // Read the optional Z value.
            if (this.isZType()) {
                this.readZ(false);
            }

            // Read the optional measure value.
            if (this.isMeasureType()) {
                this.readOptionalMeasures(false);
            }
        };

        /**
         * Read's the shape's Z values from the record buffer.
         */
        ShapefileRecord.prototype.readZ = function(isPoint) {
            if (isPoint) {
                this._zValues = this._buffer.getDoubleArray(1);
                var z = this._zValues[0];
                this._zRange = [z, z];
            }
            else {
                this._zRange = this._buffer.getDoubleArray(2);
                this._zValues = this._buffer.getDoubleArray(this.numberOfPoints);
            }
        };

        /**
         * Reads any optional measure values from the record buffer.
         */
        ShapefileRecord.prototype.readOptionalMeasures = function(isPoint) {
            // Measure values are optional.
            if (this._buffer.hasRemaining() && (this._buffer.limit() - this._buffer.position) >= (this.numberOfPoints * 8)) {
                if (isPoint) {
                    this._mValues = this._buffer.getDoubleArray(1);
                    var m = this._mValues[0];
                    this._mRange = [m, m];
                }
                else {
                    this._mRange = this._buffer.getDoubleArray(2);
                    this._mValues = this._buffer.getDoubleArray(this.numberOfPoints);
                }
            }
        };

        /**
         * Normalize an array of doubles and treat them as lat/lon pairs,
         * where the longitude is the first value of the pair, and
         * the latitude is the second value of the pair.
         * <p>
         * This pair ordering is dictated by the format of shapefiles.
         * @param {Number} array
         */
        ShapefileRecord.normalizeLocations = function(array) {
            for (var idx = 0, len = array.length; idx < len; idx += 2) {
                var longitude = array[idx];
                var latitude = array[idx + 1];

                array[idx] = Angle.normalizedDegreesLongitude(longitude);
                array[idx + 1] = Angle.normalizedDegreesLatitude(latitude);
            }
        };

        /**
         * Indicate whether the record is of a point type.
         * @returns {Boolean} True if the record is of a point type.
         */
        ShapefileRecord.prototype.isPointType = function() {
            return this.shapefile.isPointType();
        };

        /**
         * Indicate whether the record is of a point type.
         * @returns {Boolean} True if the record is of a point type.
         */
        ShapefileRecord.prototype.isMultiPointType = function() {
            return this.shapefile.isMultiPointType();
        };

        /**
         * Indicate whether the record is of a polyline type.
         * @returns {Boolean} True if the record is of a polyline type.
         */
        ShapefileRecord.prototype.isPolylineType = function() {
            return this.shapefile.isPolylineType();
        };

        /**
         * Indicate whether the record is of a polygon type.
         * @returns {Boolean} True if the record is of a polygon type.
         */
        ShapefileRecord.prototype.isPolygonType = function() {
            return this.shapefile.isPolygonType();
        };

        /**
         * Indicate whether the record is of a depth type.
         * @returns {Boolean} True if the record is of a depth type.
         */
        ShapefileRecord.prototype.isZType = function() {
            return this.shapefile.isZType();
        };

        /**
         * Indicate whether the record is of a measure type.
         * @returns {Boolean} True if the record is of a measure type.
         */
        ShapefileRecord.prototype.isMeasureType = function() {
            return this.shapefile.isMeasureType();

        };

        /**
         * Internal use only.
         * Set the attributes of the record from a dBase file.
         * @param {Object} attributes Attributes contained in a dBase file.
         */
        ShapefileRecord.prototype.setAttributes = function(attributes) {
            this._attributes = attributes;
        };

        ShapefileRecord.RECORD_HEADER_LENGTH = 8;

        return ShapefileRecord;
    }
);