@loaders.gl/shapefile
Version:
Loader for the Shapefile Format
4 lines • 96.1 kB
Source Map (JSON)
{
"version": 3,
"sources": ["../src/index.ts", "../src/lib/parsers/parse-shp.ts", "../src/lib/streaming/binary-chunk-reader.ts", "../src/lib/parsers/parse-shp-header.ts", "../src/lib/parsers/parse-shp-geometry.ts", "../src/shp-loader.ts", "../src/lib/parsers/parse-shapefile.ts", "../src/lib/parsers/parse-shx.ts", "../src/lib/streaming/zip-batch-iterators.ts", "../src/lib/parsers/parse-dbf.ts", "../src/dbf-loader.ts", "../src/shapefile-loader.ts", "../src/lib/parsers/parse-dbf-to-arrow.ts", "../src/dbf-format.ts", "../src/dbf-arrow-loader.ts", "../src/lib/streaming/binary-reader.ts"],
"sourcesContent": ["// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\nexport type {ShapefileLoaderOptions} from './shapefile-loader';\nexport {ShapefileLoader} from './shapefile-loader';\n\nexport type {DBFLoaderOptions} from './dbf-loader';\nexport {DBFLoader, DBFWorkerLoader} from './dbf-loader';\nexport {DBFArrowLoader, DBFArrowWorkerLoader} from './dbf-arrow-loader';\n\nexport type {SHPLoaderOptions} from './shp-loader';\nexport {SHPLoader, SHPWorkerLoader} from './shp-loader';\n\n// EXPERIMENTAL\nexport {BinaryReader as _BinaryReader} from './lib/streaming/binary-reader';\nexport {BinaryChunkReader as _BinaryChunkReader} from './lib/streaming/binary-chunk-reader';\nexport {zipBatchIterators as _zipBatchIterators} from './lib/streaming/zip-batch-iterators';\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\nimport type {BinaryGeometry} from '@loaders.gl/schema';\nimport {toArrayBufferIterator} from '@loaders.gl/loader-utils';\nimport {BinaryChunkReader} from '../streaming/binary-chunk-reader';\nimport {parseSHPHeader, SHPHeader} from './parse-shp-header';\nimport {parseRecord} from './parse-shp-geometry';\nimport {SHPLoaderOptions} from './types';\n\nconst LITTLE_ENDIAN = true;\nconst BIG_ENDIAN = false;\n\nconst SHP_HEADER_SIZE = 100;\n// According to the spec, the record header is just 8 bytes, but here we set it\n// to 12 so that we can also access the record's type\nconst SHP_RECORD_HEADER_SIZE = 12;\n\nconst STATE = {\n EXPECTING_HEADER: 0,\n EXPECTING_RECORD: 1,\n END: 2,\n ERROR: 3\n};\n\ntype SHPResult = {\n geometries: (BinaryGeometry | null)[];\n header?: SHPHeader;\n error?: string;\n progress: {\n bytesUsed: number;\n bytesTotal: number;\n rows: number;\n };\n currentIndex: number;\n};\n\nclass SHPParser {\n options?: SHPLoaderOptions = {};\n binaryReader = new BinaryChunkReader({maxRewindBytes: SHP_RECORD_HEADER_SIZE});\n state = STATE.EXPECTING_HEADER;\n result: SHPResult = {\n geometries: [],\n // Initialize with number values to make TS happy\n // These are initialized for real in STATE.EXPECTING_HEADER\n progress: {\n bytesTotal: NaN,\n bytesUsed: NaN,\n rows: NaN\n },\n currentIndex: NaN\n };\n\n constructor(options?: SHPLoaderOptions) {\n this.options = options;\n }\n\n write(arrayBuffer: ArrayBuffer) {\n this.binaryReader.write(arrayBuffer);\n this.state = parseState(this.state, this.result, this.binaryReader, this.options);\n }\n\n end() {\n this.binaryReader.end();\n this.state = parseState(this.state, this.result, this.binaryReader, this.options);\n // this.result.progress.bytesUsed = this.binaryReader.bytesUsed();\n if (this.state !== STATE.END) {\n this.state = STATE.ERROR;\n this.result.error = 'SHP incomplete file';\n }\n }\n}\n\nexport function parseSHP(arrayBuffer: ArrayBuffer, options?: SHPLoaderOptions): BinaryGeometry[] {\n const shpParser = new SHPParser(options);\n shpParser.write(arrayBuffer);\n shpParser.end();\n\n // @ts-ignore\n return shpParser.result;\n}\n\n/**\n * @param asyncIterator\n * @param options\n * @returns\n */\nexport async function* parseSHPInBatches(\n asyncIterator:\n | AsyncIterable<ArrayBufferLike | ArrayBufferView>\n | Iterable<ArrayBufferLike | ArrayBufferView>,\n options?: SHPLoaderOptions\n): AsyncGenerator<BinaryGeometry | object> {\n const parser = new SHPParser(options);\n let headerReturned = false;\n for await (const arrayBuffer of toArrayBufferIterator(asyncIterator)) {\n parser.write(arrayBuffer);\n if (!headerReturned && parser.result.header) {\n headerReturned = true;\n yield parser.result.header;\n }\n\n if (parser.result.geometries.length > 0) {\n yield parser.result.geometries;\n parser.result.geometries = [];\n }\n }\n parser.end();\n if (parser.result.geometries.length > 0) {\n yield parser.result.geometries;\n }\n\n return;\n}\n\n/**\n * State-machine parser for SHP data\n *\n * Note that whenever more data is needed, a `return`, not a `break`, is\n * necessary, as the `break` keeps the context within `parseState`, while\n * `return` releases context so that more data can be written into the\n * BinaryChunkReader.\n *\n * @param state Current state\n * @param result An object to hold result data\n * @param binaryReader\n * @return State at end of current parsing\n */\n/* eslint-disable complexity, max-depth */\nfunction parseState(\n state: number,\n result: SHPResult,\n binaryReader: BinaryChunkReader,\n options?: SHPLoaderOptions\n): number {\n // eslint-disable-next-line no-constant-condition\n while (true) {\n try {\n switch (state) {\n case STATE.ERROR:\n case STATE.END:\n return state;\n\n case STATE.EXPECTING_HEADER:\n // Parse initial file header\n const dataView = binaryReader.getDataView(SHP_HEADER_SIZE);\n if (!dataView) {\n return state;\n }\n\n result.header = parseSHPHeader(dataView);\n result.progress = {\n bytesUsed: 0,\n bytesTotal: result.header.length,\n rows: 0\n };\n // index numbering starts at 1\n result.currentIndex = 1;\n state = STATE.EXPECTING_RECORD;\n break;\n\n case STATE.EXPECTING_RECORD:\n while (binaryReader.hasAvailableBytes(SHP_RECORD_HEADER_SIZE)) {\n const recordHeaderView = binaryReader.getDataView(SHP_RECORD_HEADER_SIZE) as DataView;\n const recordHeader = {\n recordNumber: recordHeaderView.getInt32(0, BIG_ENDIAN),\n // 2 byte words; includes the four words of record header\n byteLength: recordHeaderView.getInt32(4, BIG_ENDIAN) * 2,\n // This is actually part of the record, not the header...\n type: recordHeaderView.getInt32(8, LITTLE_ENDIAN)\n };\n\n if (!binaryReader.hasAvailableBytes(recordHeader.byteLength - 4)) {\n binaryReader.rewind(SHP_RECORD_HEADER_SIZE);\n return state;\n }\n\n const invalidRecord =\n recordHeader.byteLength < 4 ||\n recordHeader.type !== result.header?.type ||\n recordHeader.recordNumber !== result.currentIndex;\n\n // All records must have at least four bytes (for the record shape type)\n if (invalidRecord) {\n // Malformed record, try again, advancing just 4 bytes\n // Note: this is a rewind because binaryReader.getDataView above\n // moved the pointer forward 12 bytes, so rewinding 8 bytes still\n // leaves us 4 bytes ahead\n binaryReader.rewind(SHP_RECORD_HEADER_SIZE - 4);\n } else {\n // Note: type is actually part of the record, not the header, so\n // rewind 4 bytes before reading record\n binaryReader.rewind(4);\n\n const recordView = binaryReader.getDataView(recordHeader.byteLength) as DataView;\n const geometry = parseRecord(recordView, options);\n result.geometries.push(geometry);\n\n result.currentIndex++;\n result.progress.rows = result.currentIndex - 1;\n }\n }\n\n if (binaryReader.ended) {\n state = STATE.END;\n }\n\n return state;\n\n default:\n state = STATE.ERROR;\n result.error = `illegal parser state ${state}`;\n return state;\n }\n } catch (error) {\n state = STATE.ERROR;\n result.error = `SHP parsing failed: ${(error as Error)?.message}`;\n return state;\n }\n }\n}\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\nexport type BinaryChunkReaderOptions = {\n maxRewindBytes: number;\n};\n\nexport class BinaryChunkReader {\n offset: number;\n arrayBuffers: ArrayBuffer[];\n ended: boolean;\n maxRewindBytes: number;\n\n constructor(options?: BinaryChunkReaderOptions) {\n const {maxRewindBytes = 0} = options || {};\n\n /** current global offset into current array buffer*/\n this.offset = 0;\n /** current buffer from iterator */\n this.arrayBuffers = [];\n this.ended = false;\n\n /** bytes behind offset to hold on to */\n this.maxRewindBytes = maxRewindBytes;\n }\n /**\n * @param arrayBuffer\n */\n write(arrayBuffer: ArrayBuffer): void {\n this.arrayBuffers.push(arrayBuffer);\n }\n\n end(): void {\n this.arrayBuffers = [];\n this.ended = true;\n }\n\n /**\n * Has enough bytes available in array buffers\n *\n * @param bytes Number of bytes\n * @return boolean\n */\n hasAvailableBytes(bytes: number): boolean {\n let bytesAvailable = -this.offset;\n for (const arrayBuffer of this.arrayBuffers) {\n bytesAvailable += arrayBuffer.byteLength;\n if (bytesAvailable >= bytes) {\n return true;\n }\n }\n return false;\n }\n\n /**\n * Find offsets of byte ranges within this.arrayBuffers\n *\n * @param bytes Byte length to read\n * @return Arrays with byte ranges pointing to this.arrayBuffers, Output type is nested array, e.g. [ [0, [1, 2]], ...]\n */\n findBufferOffsets(bytes: number): any[] | null {\n let offset = -this.offset;\n const selectedBuffers: any = [];\n\n for (let i = 0; i < this.arrayBuffers.length; i++) {\n const buf = this.arrayBuffers[i];\n\n // Current buffer isn't long enough to reach global offset\n if (offset + buf.byteLength <= 0) {\n offset += buf.byteLength;\n // eslint-disable-next-line no-continue\n continue;\n }\n\n // Find start/end offsets for this buffer\n // When offset < 0, need to skip over Math.abs(offset) bytes\n // When offset > 0, implies bytes in previous buffer, start at 0\n const start = offset <= 0 ? Math.abs(offset) : 0;\n let end: number;\n\n // Length of requested bytes is contained in current buffer\n if (start + bytes <= buf.byteLength) {\n end = start + bytes;\n selectedBuffers.push([i, [start, end]]);\n return selectedBuffers;\n }\n\n // Will need to look into next buffer\n end = buf.byteLength;\n selectedBuffers.push([i, [start, end]]);\n\n // Need to read fewer bytes in next iter\n bytes -= buf.byteLength - start;\n offset += buf.byteLength;\n }\n\n // Should only finish loop if exhausted all arrays\n return null;\n }\n\n /**\n * Get the required number of bytes from the iterator\n *\n * @param bytes Number of bytes\n * @return DataView with data\n */\n getDataView(bytes: number): DataView | null {\n const bufferOffsets = this.findBufferOffsets(bytes);\n // return `null` if not enough data, except if end() already called, in\n // which case throw an error.\n if (!bufferOffsets && this.ended) {\n throw new Error('binary data exhausted');\n }\n\n if (!bufferOffsets) {\n return null;\n }\n\n // If only one arrayBuffer needed, return DataView directly\n if (bufferOffsets.length === 1) {\n const [bufferIndex, [start, end]] = bufferOffsets[0];\n const arrayBuffer = this.arrayBuffers[bufferIndex];\n const view = new DataView(arrayBuffer, start, end - start);\n\n this.offset += bytes;\n this.disposeBuffers();\n return view;\n }\n\n // Concatenate portions of multiple ArrayBuffers\n const view = new DataView(this._combineArrayBuffers(bufferOffsets));\n this.offset += bytes;\n this.disposeBuffers();\n return view;\n }\n\n /**\n * Dispose of old array buffers\n */\n disposeBuffers(): void {\n while (\n this.arrayBuffers.length > 0 &&\n this.offset - this.maxRewindBytes >= this.arrayBuffers[0].byteLength\n ) {\n this.offset -= this.arrayBuffers[0].byteLength;\n this.arrayBuffers.shift();\n }\n }\n\n /**\n * Copy multiple ArrayBuffers into one contiguous ArrayBuffer\n *\n * In contrast to concatenateArrayBuffers, this only copies the necessary\n * portions of the source arrays, rather than first copying the entire arrays\n * then taking a part of them.\n *\n * @param bufferOffsets List of internal array offsets\n * @return New contiguous ArrayBuffer\n */\n _combineArrayBuffers(bufferOffsets: any[]): ArrayBufferLike {\n let byteLength: number = 0;\n for (const bufferOffset of bufferOffsets) {\n const [start, end] = bufferOffset[1];\n byteLength += end - start;\n }\n\n const result = new Uint8Array(byteLength);\n\n // Copy the subarrays\n let resultOffset: number = 0;\n for (const bufferOffset of bufferOffsets) {\n const [bufferIndex, [start, end]] = bufferOffset;\n const sourceArray = new Uint8Array(this.arrayBuffers[bufferIndex]);\n result.set(sourceArray.subarray(start, end), resultOffset);\n resultOffset += end - start;\n }\n\n return result.buffer;\n }\n /**\n * @param bytes\n */\n skip(bytes: number): void {\n this.offset += bytes;\n }\n /**\n * @param bytes\n */\n rewind(bytes: number): void {\n // TODO - only works if offset is already set\n this.offset -= bytes;\n }\n}\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\nexport interface SHPHeader {\n /** SHP Magic number */\n magic: number;\n\n /** Number of bytes in file */\n length: number;\n version: number;\n type: number;\n bbox: {\n minX: number;\n minY: number;\n minZ: number;\n minM: number;\n maxX: number;\n maxY: number;\n maxZ: number;\n maxM: number;\n };\n}\n\nconst LITTLE_ENDIAN = true;\nconst BIG_ENDIAN = false;\nconst SHP_MAGIC_NUMBER = 0x0000270a;\n\n/**\n * Extract the binary header\n * Note: Also used by SHX\n * @param headerView\n * @returns SHPHeader\n */\nexport function parseSHPHeader(headerView: DataView): SHPHeader {\n // Note: The SHP format switches endianness between fields!\n // https://www.esri.com/library/whitepapers/pdfs/shapefile.pdf\n const header = {\n magic: headerView.getInt32(0, BIG_ENDIAN),\n // Length is stored as # of 2-byte words; multiply by 2 to get # of bytes\n length: headerView.getInt32(24, BIG_ENDIAN) * 2,\n version: headerView.getInt32(28, LITTLE_ENDIAN),\n type: headerView.getInt32(32, LITTLE_ENDIAN),\n bbox: {\n minX: headerView.getFloat64(36, LITTLE_ENDIAN),\n minY: headerView.getFloat64(44, LITTLE_ENDIAN),\n minZ: headerView.getFloat64(68, LITTLE_ENDIAN),\n minM: headerView.getFloat64(84, LITTLE_ENDIAN),\n maxX: headerView.getFloat64(52, LITTLE_ENDIAN),\n maxY: headerView.getFloat64(60, LITTLE_ENDIAN),\n maxZ: headerView.getFloat64(76, LITTLE_ENDIAN),\n maxM: headerView.getFloat64(92, LITTLE_ENDIAN)\n }\n };\n if (header.magic !== SHP_MAGIC_NUMBER) {\n // eslint-disable-next-line\n console.error(`SHP file: bad magic number ${header.magic}`);\n }\n if (header.version !== 1000) {\n // eslint-disable-next-line\n console.error(`SHP file: bad version ${header.version}`);\n }\n return header;\n}\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\nimport type {BinaryGeometry, BinaryGeometryType} from '@loaders.gl/schema';\nimport {SHPLoaderOptions} from './types';\n\nconst LITTLE_ENDIAN = true;\n\n/**\n * Parse individual record\n *\n * @param view Record data\n * @return Binary Geometry Object\n */\n// eslint-disable-next-line complexity\nexport function parseRecord(view: DataView, options?: SHPLoaderOptions): BinaryGeometry | null {\n const {_maxDimensions = 4} = options?.shp || {};\n\n let offset = 0;\n const type: number = view.getInt32(offset, LITTLE_ENDIAN);\n offset += Int32Array.BYTES_PER_ELEMENT;\n\n switch (type) {\n case 0:\n // Null Shape\n return parseNull();\n case 1:\n // Point\n return parsePoint(view, offset, Math.min(2, _maxDimensions));\n case 3:\n // PolyLine\n return parsePoly(view, offset, Math.min(2, _maxDimensions), 'LineString');\n case 5:\n // Polygon\n return parsePoly(view, offset, Math.min(2, _maxDimensions), 'Polygon');\n case 8:\n // MultiPoint\n return parseMultiPoint(view, offset, Math.min(2, _maxDimensions));\n // GeometryZ can have 3 or 4 dimensions, since the M is not required to\n // exist\n case 11:\n // PointZ\n return parsePoint(view, offset, Math.min(4, _maxDimensions));\n case 13:\n // PolyLineZ\n return parsePoly(view, offset, Math.min(4, _maxDimensions), 'LineString');\n case 15:\n // PolygonZ\n return parsePoly(view, offset, Math.min(4, _maxDimensions), 'Polygon');\n case 18:\n // MultiPointZ\n return parseMultiPoint(view, offset, Math.min(4, _maxDimensions));\n case 21:\n // PointM\n return parsePoint(view, offset, Math.min(3, _maxDimensions));\n case 23:\n // PolyLineM\n return parsePoly(view, offset, Math.min(3, _maxDimensions), 'LineString');\n case 25:\n // PolygonM\n return parsePoly(view, offset, Math.min(3, _maxDimensions), 'Polygon');\n case 28:\n // MultiPointM\n return parseMultiPoint(view, offset, Math.min(3, _maxDimensions));\n default:\n throw new Error(`unsupported shape type: ${type}`);\n }\n}\n\n// TODO handle null\n/**\n * Parse Null geometry\n *\n * @return null\n */\nfunction parseNull(): null {\n return null;\n}\n\n/**\n * Parse point geometry\n *\n * @param view Geometry data\n * @param offset Offset in view\n * @param dim Dimension size\n */\nfunction parsePoint(view: DataView, offset: number, dim: number): BinaryGeometry {\n let positions: Float64Array;\n [positions, offset] = parsePositions(view, offset, 1, dim);\n\n return {\n positions: {value: positions, size: dim},\n type: 'Point'\n };\n}\n\n/**\n * Parse MultiPoint geometry\n *\n * @param view Geometry data\n * @param offset Offset in view\n * @param dim Input dimension\n * @return Binary geometry object\n */\nfunction parseMultiPoint(view: DataView, offset: number, dim: number): BinaryGeometry {\n // skip parsing box\n offset += 4 * Float64Array.BYTES_PER_ELEMENT;\n\n const nPoints = view.getInt32(offset, LITTLE_ENDIAN);\n offset += Int32Array.BYTES_PER_ELEMENT;\n\n let xyPositions: Float64Array | null = null;\n let mPositions: Float64Array | null = null;\n let zPositions: Float64Array | null = null;\n [xyPositions, offset] = parsePositions(view, offset, nPoints, 2);\n\n // Parse Z coordinates\n if (dim === 4) {\n // skip parsing range\n offset += 2 * Float64Array.BYTES_PER_ELEMENT;\n [zPositions, offset] = parsePositions(view, offset, nPoints, 1);\n }\n\n // Parse M coordinates\n if (dim >= 3) {\n // skip parsing range\n offset += 2 * Float64Array.BYTES_PER_ELEMENT;\n [mPositions, offset] = parsePositions(view, offset, nPoints, 1);\n }\n\n const positions = concatPositions(xyPositions, mPositions, zPositions);\n\n return {\n positions: {value: positions, size: dim},\n type: 'Point'\n };\n}\n\n/**\n * Polygon and PolyLine parsing\n *\n * @param view Geometry data\n * @param offset Offset in view\n * @param dim Input dimension\n * @param type Either 'Polygon' or 'Polyline'\n * @return Binary geometry object\n */\n// eslint-disable-next-line max-statements\nfunction parsePoly(\n view: DataView,\n offset: number,\n dim: number,\n type: BinaryGeometryType\n): BinaryGeometry {\n // skip parsing bounding box\n offset += 4 * Float64Array.BYTES_PER_ELEMENT;\n\n const nParts = view.getInt32(offset, LITTLE_ENDIAN);\n offset += Int32Array.BYTES_PER_ELEMENT;\n const nPoints = view.getInt32(offset, LITTLE_ENDIAN);\n offset += Int32Array.BYTES_PER_ELEMENT;\n\n // Create longer indices array by 1 because output format is expected to\n // include the last index as the total number of positions\n const bufferOffset = view.byteOffset + offset;\n const bufferLength = nParts * Int32Array.BYTES_PER_ELEMENT;\n const ringIndices = new Int32Array(nParts + 1);\n ringIndices.set(new Int32Array(view.buffer.slice(bufferOffset, bufferOffset + bufferLength)));\n ringIndices[nParts] = nPoints;\n offset += nParts * Int32Array.BYTES_PER_ELEMENT;\n\n let xyPositions: Float64Array | null = null;\n let mPositions: Float64Array | null = null;\n let zPositions: Float64Array | null = null;\n [xyPositions, offset] = parsePositions(view, offset, nPoints, 2);\n\n // Parse Z coordinates\n if (dim === 4) {\n // skip parsing range\n offset += 2 * Float64Array.BYTES_PER_ELEMENT;\n [zPositions, offset] = parsePositions(view, offset, nPoints, 1);\n }\n\n // Parse M coordinates\n if (dim >= 3) {\n // skip parsing range\n offset += 2 * Float64Array.BYTES_PER_ELEMENT;\n [mPositions, offset] = parsePositions(view, offset, nPoints, 1);\n }\n\n const positions = concatPositions(xyPositions, mPositions, zPositions);\n\n // parsePoly only accepts type = LineString or Polygon\n if (type === 'LineString') {\n return {\n type,\n positions: {value: positions, size: dim},\n pathIndices: {value: ringIndices, size: 1}\n };\n }\n\n // for every ring, determine sign of polygon\n // Use only 2D positions for ring calc\n const polygonIndices: number[] = [];\n for (let i = 1; i < ringIndices.length; i++) {\n const startRingIndex = ringIndices[i - 1];\n const endRingIndex = ringIndices[i];\n // @ts-ignore\n const ring = xyPositions.subarray(startRingIndex * 2, endRingIndex * 2);\n const sign = getWindingDirection(ring);\n\n // A positive sign implies clockwise\n // A clockwise ring is a filled ring\n if (sign > 0) {\n polygonIndices.push(startRingIndex);\n }\n }\n\n polygonIndices.push(nPoints);\n\n return {\n type,\n positions: {value: positions, size: dim},\n primitivePolygonIndices: {value: ringIndices, size: 1},\n // TODO: Dynamically choose Uint32Array over Uint16Array only when\n // necessary. I believe the implementation requires nPoints to be the\n // largest value in the array, so you should be able to use Uint32Array only\n // when nPoints > 65535.\n polygonIndices: {value: new Uint32Array(polygonIndices), size: 1}\n };\n}\n\n/**\n * Parse a contiguous block of positions into a Float64Array\n *\n * @param view Geometry data\n * @param offset Offset in view\n * @param nPoints Number of points\n * @param dim Input dimension\n * @return Data and offset\n */\nfunction parsePositions(\n view: DataView,\n offset: number,\n nPoints: number,\n dim: number\n): [Float64Array, number] {\n const bufferOffset = view.byteOffset + offset;\n const bufferLength = nPoints * dim * Float64Array.BYTES_PER_ELEMENT;\n return [\n new Float64Array(view.buffer.slice(bufferOffset, bufferOffset + bufferLength)),\n offset + bufferLength\n ];\n}\n\n/**\n * Concatenate and interleave positions arrays\n * xy positions are interleaved; mPositions, zPositions are their own arrays\n *\n * @param xyPositions 2d positions\n * @param mPositions M positions\n * @param zPositions Z positions\n * @return Combined interleaved positions\n */\n// eslint-disable-next-line complexity\nfunction concatPositions(\n xyPositions: Float64Array,\n mPositions: Float64Array | null,\n zPositions: Float64Array | null\n): Float64Array {\n if (!(mPositions || zPositions)) {\n return xyPositions;\n }\n\n let arrayLength = xyPositions.length;\n let nDim = 2;\n\n if (zPositions && zPositions.length) {\n arrayLength += zPositions.length;\n nDim++;\n }\n\n if (mPositions && mPositions.length) {\n arrayLength += mPositions.length;\n nDim++;\n }\n\n const positions = new Float64Array(arrayLength);\n for (let i = 0; i < xyPositions.length / 2; i++) {\n positions[nDim * i] = xyPositions[i * 2];\n positions[nDim * i + 1] = xyPositions[i * 2 + 1];\n }\n\n if (zPositions && zPositions.length) {\n for (let i = 0; i < zPositions.length; i++) {\n // If Z coordinates exist; used as third coord in positions array\n positions[nDim * i + 2] = zPositions[i];\n }\n }\n\n if (mPositions && mPositions.length) {\n for (let i = 0; i < mPositions.length; i++) {\n // M is always last, either 3rd or 4th depending on if Z exists\n positions[nDim * i + (nDim - 1)] = mPositions[i];\n }\n }\n\n return positions;\n}\n\n/**\n * Returns the direction of the polygon path\n * A positive number is clockwise.\n * A negative number is counter clockwise.\n *\n * @param positions\n * @return Sign of polygon ring\n */\nfunction getWindingDirection(positions: Float64Array): number {\n return Math.sign(getSignedArea(positions));\n}\n\n/**\n * Get signed area of flat typed array of 2d positions\n *\n * @param positions\n * @return Signed area of polygon ring\n */\nfunction getSignedArea(positions: Float64Array): number {\n let area = 0;\n\n // Rings are closed according to shapefile spec\n const nCoords = positions.length / 2 - 1;\n for (let i = 0; i < nCoords; i++) {\n area +=\n (positions[i * 2] + positions[(i + 1) * 2]) *\n (positions[i * 2 + 1] - positions[(i + 1) * 2 + 1]);\n }\n\n return area / 2;\n}\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\nimport type {Loader, LoaderWithParser, StrictLoaderOptions} from '@loaders.gl/loader-utils';\nimport {parseSHP, parseSHPInBatches} from './lib/parsers/parse-shp';\n\n// __VERSION__ is injected by babel-plugin-version-inline\n// @ts-ignore TS2304: Cannot find name '__VERSION__'.\nconst VERSION = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'latest';\n\nexport const SHP_MAGIC_NUMBER = [0x00, 0x00, 0x27, 0x0a];\n\n/** SHPLoader */\nexport type SHPLoaderOptions = StrictLoaderOptions & {\n shp?: {\n _maxDimensions?: number;\n /** Override the URL to the worker bundle (by default loads from unpkg.com) */\n workerUrl?: string;\n };\n};\n\n/**\n * SHP file loader\n */\nexport const SHPWorkerLoader = {\n dataType: null as unknown,\n batchType: null as never,\n\n name: 'SHP',\n id: 'shp',\n module: 'shapefile',\n version: VERSION,\n worker: true,\n category: 'geometry',\n extensions: ['shp'],\n mimeTypes: ['application/octet-stream'],\n // ISSUE: This also identifies SHX files, which are identical to SHP for the first 100 bytes...\n tests: [new Uint8Array(SHP_MAGIC_NUMBER).buffer],\n options: {\n shp: {\n _maxDimensions: 4\n }\n }\n} as const satisfies Loader<any, any, SHPLoaderOptions>;\n\n/** SHP file loader */\nexport const SHPLoader: LoaderWithParser = {\n ...SHPWorkerLoader,\n parse: async (arrayBuffer, options?) => parseSHP(arrayBuffer, options),\n parseSync: parseSHP,\n parseInBatches: (\n arrayBufferIterator:\n | AsyncIterable<ArrayBufferLike | ArrayBufferView>\n | Iterable<ArrayBufferLike | ArrayBufferView>,\n options\n ) => parseSHPInBatches(arrayBufferIterator, options)\n};\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\n// import type {Feature} from '@loaders.gl/gis';\nimport {\n LoaderContext,\n parseInBatchesFromContext,\n parseFromContext,\n toArrayBufferIterator\n} from '@loaders.gl/loader-utils';\nimport {convertBinaryGeometryToGeometry, transformGeoJsonCoords} from '@loaders.gl/gis';\nimport type {\n BinaryGeometry,\n Geometry,\n ObjectRowTable,\n ObjectRowTableBatch\n} from '@loaders.gl/schema';\nimport {Proj4Projection} from '@math.gl/proj4';\n\nimport type {SHXOutput} from './parse-shx';\nimport type {SHPHeader} from './parse-shp-header';\nimport type {ShapefileLoaderOptions} from './types';\nimport {parseShx} from './parse-shx';\nimport {zipBatchIterators} from '../streaming/zip-batch-iterators';\nimport {SHPLoader} from '../../shp-loader';\nimport {DBFLoader} from '../../dbf-loader';\n\ntype Feature = any;\ninterface ShapefileOutput {\n encoding?: string;\n prj?: string;\n shx?: SHXOutput;\n header: SHPHeader;\n data: object[];\n}\n/**\n * Parsing of file in batches\n */\n// eslint-disable-next-line max-statements, complexity\nexport async function* parseShapefileInBatches(\n asyncIterator:\n | AsyncIterable<ArrayBufferLike | ArrayBufferView>\n | Iterable<ArrayBufferLike | ArrayBufferView>,\n options?: ShapefileLoaderOptions,\n context?: LoaderContext\n): AsyncIterable<ShapefileOutput> {\n const {reproject = false, _targetCrs = 'WGS84'} = options?.gis || {};\n const {shx, cpg, prj} = await loadShapefileSidecarFiles(options, context);\n\n // parse geometries\n const shapeIterable = await parseInBatchesFromContext(\n toArrayBufferIterator(asyncIterator),\n SHPLoader,\n options,\n context!\n );\n\n const shapeIterator: AsyncIterator<any> =\n shapeIterable[Symbol.asyncIterator]?.() || shapeIterable[Symbol.iterator]?.();\n\n // parse properties\n let propertyIterator: AsyncIterator<any> | null = null;\n const dbfResponse = await context?.fetch(replaceExtension(context?.url || '', 'dbf'));\n if (dbfResponse?.ok) {\n const propertyIterable = await parseInBatchesFromContext(\n dbfResponse,\n DBFLoader,\n {\n ...options,\n dbf: {\n ...options?.dbf,\n encoding: cpg || 'latin1'\n }\n },\n context!\n );\n propertyIterator =\n propertyIterable[Symbol.asyncIterator]?.() || propertyIterable[Symbol.iterator]();\n }\n\n // When `options.metadata` is `true`, there's an extra initial `metadata`\n // object before the iterator starts. zipBatchIterators expects to receive\n // batches of Array objects, and will fail with non-iterable batches, so it's\n // important to skip over the first batch.\n let shapeHeader = (await shapeIterator.next()).value;\n if (shapeHeader && shapeHeader.batchType === 'metadata') {\n shapeHeader = (await shapeIterator.next()).value;\n }\n\n let dbfHeader: {batchType?: string} = {};\n if (propertyIterator) {\n dbfHeader = (await propertyIterator.next()).value;\n if (dbfHeader && dbfHeader.batchType === 'metadata') {\n dbfHeader = (await propertyIterator.next()).value;\n }\n }\n\n const zippedIterator: AsyncIterator<ObjectRowTableBatch> = propertyIterator\n ? zipBatchIterators(shapeIterator, propertyIterator, 'object-row-table')\n : shapeIterator;\n\n const zippedBatchIterable: AsyncIterable<ObjectRowTableBatch> = {\n [Symbol.asyncIterator]() {\n return zippedIterator;\n }\n };\n\n for await (const batch of zippedBatchIterable) {\n let geometries: any;\n let properties: any;\n if (!propertyIterator) {\n geometries = batch;\n } else {\n [geometries, properties] = batch.data;\n }\n\n const geojsonGeometries = parseGeometries(geometries);\n let features = joinProperties(geojsonGeometries, properties);\n if (reproject) {\n // @ts-ignore\n features = reprojectFeatures(features, prj, _targetCrs);\n }\n yield {\n encoding: cpg,\n prj,\n shx,\n header: shapeHeader,\n data: features\n };\n }\n}\n\n/**\n * Parse shapefile\n *\n * @param arrayBuffer\n * @param options\n * @param context\n * @returns output of shapefile\n */\nexport async function parseShapefile(\n arrayBuffer: ArrayBuffer,\n options?: ShapefileLoaderOptions,\n context?: LoaderContext\n): Promise<ShapefileOutput> {\n const {reproject = false, _targetCrs = 'WGS84'} = options?.gis || {};\n const {shx, cpg, prj} = await loadShapefileSidecarFiles(options, context);\n\n // parse geometries\n const {header, geometries} = await parseFromContext(arrayBuffer, SHPLoader, options, context!); // {shp: shx}\n\n const geojsonGeometries = parseGeometries(geometries);\n\n // parse properties\n let propertyTable: ObjectRowTable | undefined;\n\n const dbfResponse = await context?.fetch(replaceExtension(context?.url!, 'dbf'));\n if (dbfResponse?.ok) {\n const dbfOptions = {\n ...options,\n dbf: {\n ...options?.dbf,\n shape: 'object-row-table',\n encoding: cpg || 'latin1'\n }\n };\n propertyTable = await parseFromContext(dbfResponse as any, DBFLoader, dbfOptions, context!);\n }\n\n let features = joinProperties(geojsonGeometries, propertyTable?.data || []);\n if (reproject) {\n features = reprojectFeatures(features, prj, _targetCrs);\n }\n\n switch (options?.shapefile?.shape) {\n case 'geojson-table':\n return {\n // @ts-expect-error\n shape: 'geojson-table',\n type: 'FeatureCollection',\n encoding: cpg,\n schema: propertyTable?.schema || {metadata: {}, fields: []},\n prj,\n shx,\n header,\n features\n };\n default:\n return {\n encoding: cpg,\n prj,\n shx,\n header,\n data: features\n };\n }\n}\n\n/**\n * Parse geometries\n *\n * @param geometries\n * @returns geometries as an array\n */\nfunction parseGeometries(geometries: BinaryGeometry[]): Geometry[] {\n const geojsonGeometries: any[] = [];\n for (const geom of geometries) {\n geojsonGeometries.push(convertBinaryGeometryToGeometry(geom));\n }\n return geojsonGeometries;\n}\n\n/**\n * Join properties and geometries into features\n *\n * @param geometries [description]\n * @param properties [description]\n * @return [description]\n */\nfunction joinProperties(geometries: Geometry[], properties: object[]): Feature[] {\n const features: Feature[] = [];\n for (let i = 0; i < geometries.length; i++) {\n const geometry = geometries[i];\n const feature: Feature = {\n type: 'Feature',\n geometry,\n // properties can be undefined if dbfResponse above was empty\n properties: (properties && properties[i]) || {}\n };\n features.push(feature);\n }\n\n return features;\n}\n\n/**\n * Reproject GeoJSON features to output CRS\n *\n * @param features parsed GeoJSON features\n * @param sourceCrs source coordinate reference system\n * @param targetCrs \u2020arget coordinate reference system\n * @return Reprojected Features\n */\nfunction reprojectFeatures(features: Feature[], sourceCrs?: string, targetCrs?: string): Feature[] {\n if (!sourceCrs && !targetCrs) {\n return features;\n }\n\n const projection = new Proj4Projection({from: sourceCrs || 'WGS84', to: targetCrs || 'WGS84'});\n return transformGeoJsonCoords(features, (coord) => projection.project(coord));\n}\n\n/**\n *\n * @param options\n * @param context\n * @returns Promise\n */\n// eslint-disable-next-line max-statements\nexport async function loadShapefileSidecarFiles(\n options?: ShapefileLoaderOptions,\n context?: LoaderContext\n): Promise<{\n shx?: SHXOutput;\n cpg?: string;\n prj?: string;\n}> {\n // Attempt a parallel load of the small sidecar files\n // @ts-ignore context must be defined\n const {url, fetch} = context;\n const shxPromise = fetch(replaceExtension(url, 'shx'));\n const cpgPromise = fetch(replaceExtension(url, 'cpg'));\n const prjPromise = fetch(replaceExtension(url, 'prj'));\n await Promise.all([shxPromise, cpgPromise, prjPromise]);\n\n let shx: SHXOutput | undefined;\n let cpg: string | undefined;\n let prj: string | undefined;\n\n const shxResponse = await shxPromise;\n if (shxResponse.ok) {\n const arrayBuffer = await shxResponse.arrayBuffer();\n shx = parseShx(arrayBuffer);\n }\n\n const cpgResponse = await cpgPromise;\n if (cpgResponse.ok) {\n cpg = await cpgResponse.text();\n }\n\n const prjResponse = await prjPromise;\n if (prjResponse.ok) {\n prj = await prjResponse.text();\n }\n\n return {\n shx,\n cpg,\n prj\n };\n}\n\n/**\n * Replace the extension at the end of a path.\n *\n * Matches the case of new extension with the case of the original file extension,\n * to increase the chance of finding files without firing off a request storm looking for various case combinations\n *\n * NOTE: Extensions can be both lower and uppercase\n * per spec, extensions should be lower case, but that doesn't mean they always are. See:\n * calvinmetcalf/shapefile-js#64, mapserver/mapserver#4712\n * https://trac.osgeo.org/mapserver/ticket/166\n */\nexport function replaceExtension(url: string, newExtension: string): string {\n const baseName = basename(url);\n const extension = extname(url);\n const isUpperCase = extension === extension.toUpperCase();\n if (isUpperCase) {\n newExtension = newExtension.toUpperCase();\n }\n return `${baseName}.${newExtension}`;\n}\n\n// NOTE - this gives the entire path minus extension (i.e. NOT same as path.basename)\n/**\n * @param url\n * @returns string\n */\nfunction basename(url: string): string {\n const extIndex = url && url.lastIndexOf('.');\n if (typeof extIndex === 'number') {\n return extIndex >= 0 ? url.substr(0, extIndex) : '';\n }\n return extIndex;\n}\n/**\n * @param url\n * @returns string\n */\nfunction extname(url: string): string {\n const extIndex = url && url.lastIndexOf('.');\n if (typeof extIndex === 'number') {\n return extIndex >= 0 ? url.substr(extIndex + 1) : '';\n }\n return extIndex;\n}\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\nimport {parseSHPHeader} from './parse-shp-header';\n\nexport interface SHXOutput {\n offsets: Int32Array;\n lengths: Int32Array;\n}\n\nconst SHX_HEADER_SIZE = 100;\nconst BIG_ENDIAN = false;\n\n/**\n * @param arrayBuffer\n * @returns SHXOutput\n */\nexport function parseShx(arrayBuffer: ArrayBuffer): SHXOutput {\n // SHX header is identical to SHP Header\n const headerView = new DataView(arrayBuffer, 0, SHX_HEADER_SIZE);\n const header = parseSHPHeader(headerView);\n const contentLength = header.length - SHX_HEADER_SIZE;\n\n const contentView = new DataView(arrayBuffer, SHX_HEADER_SIZE, contentLength);\n\n const offsets = new Int32Array(contentLength);\n const lengths = new Int32Array(contentLength);\n\n for (let i = 0; i < contentLength / 8; i++) {\n offsets[i] = contentView.getInt32(i * 8, BIG_ENDIAN);\n lengths[i] = contentView.getInt32(i * 8 + 4, BIG_ENDIAN);\n }\n\n return {\n offsets,\n lengths\n };\n}\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\nimport type {ObjectRowTableBatch, ArrayRowTableBatch} from '@loaders.gl/schema';\n\ntype RowTableBatch = ObjectRowTableBatch | ArrayRowTableBatch;\n\n/**\n * Zip two iterators together\n *\n * @param iterator1\n * @param iterator2\n */\nexport async function* zipBatchIterators(\n iterator1: AsyncIterator<RowTableBatch> | Iterator<RowTableBatch>,\n iterator2: AsyncIterator<RowTableBatch> | Iterator<RowTableBatch>,\n shape: 'object-row-table' | 'array-row-table'\n): AsyncGenerator<RowTableBatch, void, unknown> {\n const batch1Data: unknown[] = [];\n const batch2Data: unknown[] = [];\n let iterator1Done: boolean = false;\n let iterator2Done: boolean = false;\n\n // TODO - one could let all iterators flow at full speed using `Promise.race`\n // however we might end up with a big temporary buffer\n while (!iterator1Done && !iterator2Done) {\n if (batch1Data.length === 0 && !iterator1Done) {\n const {value, done} = await iterator1.next();\n if (done) {\n iterator1Done = true;\n } else {\n // @ts-expect-error\n batch1Data.push(...value);\n }\n }\n if (batch2Data.length === 0 && !iterator2Done) {\n const {value, done} = await iterator2.next();\n if (done) {\n iterator2Done = true;\n } else {\n batch2Data.push(...value);\n }\n }\n\n const batchData = extractBatchData(batch1Data, batch2Data);\n if (batchData) {\n yield {\n batchType: 'data',\n shape,\n length: batchData.length,\n data: batchData\n };\n }\n }\n}\n\n/**\n * Extract batch of same length from two batches\n *\n * @param batch1\n * @param batch2\n * @return array | null\n */\nfunction extractBatchData(batch1: any[], batch2: any[]): any[] | null {\n const batchLength: number = Math.min(batch1.length, batch2.length);\n if (batchLength === 0) {\n return null;\n }\n\n // Non interleaved arrays\n const batch: any[] = [batch1.slice(0, batchLength), batch2.slice(0, batchLength)];\n\n // Modify the 2 batches\n batch1.splice(0, batchLength);\n batch2.splice(0, batchLength);\n return batch;\n}\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n\nimport type {Field, ObjectRowTable} from '@loaders.gl/schema';\nimport {toArrayBufferIterator} from '@loaders.gl/loader-utils';\nimport {BinaryChunkReader} from '../streaming/binary-chunk-reader';\nimport {\n DBFLoaderOptions,\n DBFResult,\n DBFTableOutput,\n DBFHeader,\n DBFRowsOutput,\n DBFField\n} from './types';\n\nconst LITTLE_ENDIAN = true;\nconst DBF_HEADER_SIZE = 32;\n\nenum STATE {\n START = 0, // Expecting header\n FIELD_DESCRIPTORS = 1,\n FIELD_PROPERTIES = 2,\n END = 3,\n ERROR = 4\n}\n\nclass DBFParser {\n binaryReader = new BinaryChunkReader();\n textDecoder: TextDecoder;\n state = STATE.START;\n result: DBFResult = {\n data: []\n };\n\n constructor(options: {encoding: string}) {\n this.textDecoder = new TextDecoder(options.encoding);\n }\n\n /**\n * @param arrayBuffer\n */\n write(arrayBuffer: ArrayBuffer): void {\n this.binaryReader.write(arrayBuffer);\n this.state = parseState(this.state, this.result, this.binaryReader, this.textDecoder);\n // this.result.progress.bytesUsed = this.binaryReader.bytesUsed();\n\n // important events:\n // - schema available\n // - first rows available\n // - all rows available\n }\n\n end(): void {\n this.binaryReader.end();\n this.state = parseState(this.state, this.result, this.binaryReader, this.textDecoder);\n // this.result.progress.bytesUsed = this.binaryReader.bytesUsed();\n if (this.state !== STATE.END) {\n this.state = STATE.ERROR;\n this.result.error = 'DBF incomplete file';\n }\n }\n}\n\n/**\n * @param arrayBuffer\n * @param options\n * @returns DBFTable or rows\n */\nexport function parseDBF(\n arrayBuffer: ArrayBuffer,\n options: DBFLoaderOptions = {}\n): DBFRowsOutput | DBFTableOutput | ObjectRowTable {\n const {encoding = 'latin1'} = options.dbf || {};\n\n const dbfParser = new DBFParser({encoding});\n dbfParser.write(arrayBuffer);\n dbfParser.end();\n\n const {data, schema} = dbfParser.result;\n const shape = options?.dbf?.shape;\n switch (shape) {\n case 'object-row-table': {\n const table: ObjectRowTable = {\n shape: 'object-row-table',\n schema,\n data\n };\n return table;\n }\n case 'table':\n return {schema, rows: data};\n case 'rows':\n default:\n return data;\n }\n}\n/**\n * @param asyncIterator\n * @param options\n */\nexport async function* parseDBFInBatches(\n asyncIterator:\n | AsyncIterable<ArrayBufferLike | ArrayBufferView>\n | Iterable<ArrayBufferLike | ArrayBufferView>,\n options: DBFLoaderOptions = {}\n): AsyncIterable<DBFHeader | DBFRowsOutput | DBFTableOutput> {\n const {encoding = 'latin1'} = options.dbf || {};\n\n const parser = new DBFParser({encoding});\n let headerReturned = false;\n for await (const arrayBuffer of toArrayBufferIterator(asyncIterator)) {\n parser.write(arrayBuffer);\n if (!headerReturned && parser.result.dbfHeader) {\n headerReturned = true;\n yield parser.result.dbfHeader;\n }\n\n if (parser.result.data.length > 0) {\n yield parser.result.data;\n parser.result.data = [];\n }\n }\n parser.end();\n if (parser.result.data.length > 0) {\n yield parser.result.data;\n }\n}\n/**\n * https://www.dbase.com/Knowledgebase/INT/db7_file_fmt.htm\n * @param state\n * @param result\n * @param binaryReader\n * @param textDecoder\n * @returns\n */\n/* eslint-disable complexity, max-depth */\nfunction parseState(\n state: STATE,\n result: DBFResult,\n binaryReader: BinaryChunkReader,\n textDecoder: TextDecoder\n): STATE {\n // eslint-disable-next-line no-constant-condition\n while (true) {\n try {\n switch (state) {\n case STATE.ERROR:\n case STATE.END:\n return state;\n\n case STATE.START:\n // Parse initial file header\n // DBF Header\n const dataView = binaryReader.getDataView(DBF_HEADER_SIZE);\n if (!dataView) {\n return state;\n }\n result.dbfHeader = parseDBFHeader(dataView);\n result.progress = {\n bytesUsed: 0,\n rowsTotal: result.dbfHeader.nRecords,\n rows: 0\n };\n state = STATE.FIELD_DESCRIPTORS;\n break;\n\n case STATE.FIELD_DESCRIPTORS:\n // Parse DBF field descriptors (schema)\n const fieldDescriptorView = binaryReader.getDataView(\n // @ts-ignore\n result.dbfHeader.headerLength - DBF_HEADER_SIZE\n );\n if (!fieldDescriptorView) {\n return state;\n }\n\n result.dbfFields = parseFieldDescriptors(fieldDescriptorView, textDecoder);\n result.schema = {\n fields: result.dbfFields.map((dbfField) => makeField(dbfField)),\n metadata: {}\n };\n\n state = STATE.FIELD_PROPERTIES;\n\n // TODO(kyle) Not exactly sure why start offset needs to be headerLength + 1?\n // parsedbf uses ((fields.length + 1) << 5) + 2;\n binaryReader.skip(1);\n break;\n\n case STATE.FIELD_PROPERTIES:\n const {recordLength = 0, nRecords = 0} = result?.dbfHeader || {};\n while (result.data.length < nRecords) {\n const recordView = binaryReader.getDataView(recordLength - 1);\n if (!recordView) {\n return state;\n }\n // Note: Avoid actually reading the last byte, which may not be present\n binaryReader.skip(1);\n\n // @ts-ignore\n const row = parseRow(recordView, result.dbfFields, textDecoder);\n result.data.push(row);\n // @ts-ignore\n result.progress.rows = result.data.length;\n }\n state = STATE.END;\n break;\n\n default:\n state = STATE.ERROR;\n result.error = `illegal parser state ${state}`;\n return state;\n }\n } catch (error) {\n state = STATE.ERROR;\n result.error = `DBF parsing failed: ${(error as Error).message}`;\n return state;\n }\n }\n}\n\n/**\n * @param headerView\n */\nfunction parseDBFHeader(headerView: DataView): DBFHeader {\n return {\n // Last updated date\n year: headerView.getUint8(1) + 1900,\n month: headerView.getUint8(2),\n day: headerView.getUint8(3),\n // Number of records in data file\n nRecords: headerView.getUint32(4, LITTLE_ENDIAN),\n // Length of header in bytes\n headerLength: headerView.getUint16(8, LITTLE_ENDIAN),\n // Length of each record\n recordLength: headerView.getUint16(10, LITTLE_ENDIAN),\n // Not sure if this is usually set\n languageDriver: headerView.getUint8(29)\n };\n}\n\n/**\n * @param view\n */\nfunction parseFieldDescriptors(view: DataView, textDecoder: TextDecoder): DBFField[] {\n // NOTE: this might overestimate the number of fields if the \"Database\n // Container\" container exists and is included in the headerLength\n const nFields = (view.byteLength - 1) / 32;\n const fields: DBFField[] = [];\n let offset = 0;\n for (let i = 0; i < nFields; i++) {\n const name = textDecoder\n .decode(new Uint8Array(view.buffer, view.byteOffset + offset, 11))\n // eslint-disable-next-line no-control-regex\n .replace(/\\u0000/g, '');\n\n fields.push({\n name,\n dataType: String.fromCharCode(view.getUint8(offset + 11)),\n fieldLength: view.getUint8(offset + 16),\n decimal: view.getUint8(offset + 17)\n });\n offset += 32;\n }\n return fields;\n}\n\n/*\n * @param {BinaryChunkReader} binaryReader\nfunction parseRows(binaryReader, fields, nRecords, recordLength, textDecoder) {\n const rows = [];\n for (let i = 0; i < nRecords; i++) {\n const recordView = binaryReader.getDataView(recordLength - 1);\n binaryReader.skip(1);\n // @ts-ignore\n rows.push(parseRow(recordView, fields, textDecoder));\n }\n return rows;\n}\n */\n\n/**\n *\n * @param view\n * @param fields\n * @param textDecoder\n * @returns\n */\nfunction parseRow(\n view: DataView,\n fields: DBFField[],\n textDecoder: TextDecoder\n): {[key: string]: any} {\n const out: {[key: string]: string | number | boolean | null} = {};\n let offset = 0;\n for (const field of fields) {\n const text = textDecoder.decode(\n new Uint8Array(view.buffer, view.byteOffset + offset, field.fieldLength)\n );\n out[field.name] = parseField(text, field.dataType);\n offset += field.fieldLength;\n }\n\n return out;\n}\n\n/**\n * Should NaN be coerced to null?\n * @param text\n * @param dataType\n * @returns Field depends on a type of the data\n */\nfunction parseField(text: string, dataType: string): string | number | boolean | null {\n switch (dataType) {\n case 'B':\n