UNPKG

@loaders.gl/shapefile

Version:

Loader for the Shapefile Format

4 lines 76.6 kB
{ "version": 3, "sources": ["index.js", "lib/streaming/binary-chunk-reader.js", "lib/parsers/parse-shp-header.js", "lib/parsers/parse-shp-geometry.js", "lib/parsers/parse-shp.js", "shp-loader.js", "lib/parsers/parse-shapefile.js", "lib/parsers/parse-shx.js", "lib/streaming/zip-batch-iterators.js", "lib/parsers/parse-dbf.js", "dbf-loader.js", "shapefile-loader.js", "lib/streaming/binary-reader.js"], "sourcesContent": ["// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\nexport { ShapefileLoader } from \"./shapefile-loader.js\";\nexport { DBFLoader, DBFWorkerLoader } from \"./dbf-loader.js\";\nexport { SHPLoader, SHPWorkerLoader } from \"./shp-loader.js\";\n// EXPERIMENTAL\nexport { BinaryReader as _BinaryReader } from \"./lib/streaming/binary-reader.js\";\nexport { BinaryChunkReader as _BinaryChunkReader } from \"./lib/streaming/binary-chunk-reader.js\";\nexport { zipBatchIterators as _zipBatchIterators } from \"./lib/streaming/zip-batch-iterators.js\";\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\nexport class BinaryChunkReader {\n offset;\n arrayBuffers;\n ended;\n maxRewindBytes;\n constructor(options) {\n const { maxRewindBytes = 0 } = options || {};\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 /** bytes behind offset to hold on to */\n this.maxRewindBytes = maxRewindBytes;\n }\n /**\n * @param arrayBuffer\n */\n write(arrayBuffer) {\n this.arrayBuffers.push(arrayBuffer);\n }\n end() {\n this.arrayBuffers = [];\n this.ended = true;\n }\n /**\n * Has enough bytes available in array buffers\n *\n * @param bytes Number of bytes\n * @return boolean\n */\n hasAvailableBytes(bytes) {\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 * 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) {\n let offset = -this.offset;\n const selectedBuffers = [];\n for (let i = 0; i < this.arrayBuffers.length; i++) {\n const buf = this.arrayBuffers[i];\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 // 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;\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 // Will need to look into next buffer\n end = buf.byteLength;\n selectedBuffers.push([i, [start, end]]);\n // Need to read fewer bytes in next iter\n bytes -= buf.byteLength - start;\n offset += buf.byteLength;\n }\n // Should only finish loop if exhausted all arrays\n return null;\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) {\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 if (!bufferOffsets) {\n return null;\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 this.offset += bytes;\n this.disposeBuffers();\n return view;\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 * Dispose of old array buffers\n */\n disposeBuffers() {\n while (this.arrayBuffers.length > 0 &&\n this.offset - this.maxRewindBytes >= this.arrayBuffers[0].byteLength) {\n this.offset -= this.arrayBuffers[0].byteLength;\n this.arrayBuffers.shift();\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) {\n let byteLength = 0;\n for (const bufferOffset of bufferOffsets) {\n const [start, end] = bufferOffset[1];\n byteLength += end - start;\n }\n const result = new Uint8Array(byteLength);\n // Copy the subarrays\n let resultOffset = 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 return result.buffer;\n }\n /**\n * @param bytes\n */\n skip(bytes) {\n this.offset += bytes;\n }\n /**\n * @param bytes\n */\n rewind(bytes) {\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\nconst LITTLE_ENDIAN = true;\nconst BIG_ENDIAN = false;\nconst SHP_MAGIC_NUMBER = 0x0000270a;\n/**\n * Extract the binary header\n * Note: Also used by SHX\n * @param headerView\n * @returns SHPHeader\n */\nexport function parseSHPHeader(headerView) {\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\nconst LITTLE_ENDIAN = true;\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, options) {\n const { _maxDimensions = 4 } = options?.shp || {};\n let offset = 0;\n const type = view.getInt32(offset, LITTLE_ENDIAN);\n offset += Int32Array.BYTES_PER_ELEMENT;\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// TODO handle null\n/**\n * Parse Null geometry\n *\n * @return null\n */\nfunction parseNull() {\n return null;\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, offset, dim) {\n let positions;\n [positions, offset] = parsePositions(view, offset, 1, dim);\n return {\n positions: { value: positions, size: dim },\n type: 'Point'\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, offset, dim) {\n // skip parsing box\n offset += 4 * Float64Array.BYTES_PER_ELEMENT;\n const nPoints = view.getInt32(offset, LITTLE_ENDIAN);\n offset += Int32Array.BYTES_PER_ELEMENT;\n let xyPositions = null;\n let mPositions = null;\n let zPositions = null;\n [xyPositions, offset] = parsePositions(view, offset, nPoints, 2);\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 // 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 const positions = concatPositions(xyPositions, mPositions, zPositions);\n return {\n positions: { value: positions, size: dim },\n type: 'Point'\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(view, offset, dim, type) {\n // skip parsing bounding box\n offset += 4 * Float64Array.BYTES_PER_ELEMENT;\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 // 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 let xyPositions = null;\n let mPositions = null;\n let zPositions = null;\n [xyPositions, offset] = parsePositions(view, offset, nPoints, 2);\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 // 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 const positions = concatPositions(xyPositions, mPositions, zPositions);\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 // for every ring, determine sign of polygon\n // Use only 2D positions for ring calc\n const polygonIndices = [];\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 // A positive sign implies clockwise\n // A clockwise ring is a filled ring\n if (sign > 0) {\n polygonIndices.push(startRingIndex);\n }\n }\n polygonIndices.push(nPoints);\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 * 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(view, offset, nPoints, dim) {\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 * 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(xyPositions, mPositions, zPositions) {\n if (!(mPositions || zPositions)) {\n return xyPositions;\n }\n let arrayLength = xyPositions.length;\n let nDim = 2;\n if (zPositions && zPositions.length) {\n arrayLength += zPositions.length;\n nDim++;\n }\n if (mPositions && mPositions.length) {\n arrayLength += mPositions.length;\n nDim++;\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 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 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 return positions;\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) {\n return Math.sign(getSignedArea(positions));\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) {\n let area = 0;\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 return area / 2;\n}\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\nimport { BinaryChunkReader } from \"../streaming/binary-chunk-reader.js\";\nimport { parseSHPHeader } from \"./parse-shp-header.js\";\nimport { parseRecord } from \"./parse-shp-geometry.js\";\nconst LITTLE_ENDIAN = true;\nconst BIG_ENDIAN = false;\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;\nconst STATE = {\n EXPECTING_HEADER: 0,\n EXPECTING_RECORD: 1,\n END: 2,\n ERROR: 3\n};\nclass SHPParser {\n options = {};\n binaryReader = new BinaryChunkReader({ maxRewindBytes: SHP_RECORD_HEADER_SIZE });\n state = STATE.EXPECTING_HEADER;\n result = {\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 constructor(options) {\n this.options = options;\n }\n write(arrayBuffer) {\n this.binaryReader.write(arrayBuffer);\n this.state = parseState(this.state, this.result, this.binaryReader, this.options);\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}\nexport function parseSHP(arrayBuffer, options) {\n const shpParser = new SHPParser(options);\n shpParser.write(arrayBuffer);\n shpParser.end();\n // @ts-ignore\n return shpParser.result;\n}\n/**\n * @param asyncIterator\n * @param options\n * @returns\n */\nexport async function* parseSHPInBatches(asyncIterator, options) {\n const parser = new SHPParser(options);\n let headerReturned = false;\n for await (const arrayBuffer of asyncIterator) {\n parser.write(arrayBuffer);\n if (!headerReturned && parser.result.header) {\n headerReturned = true;\n yield parser.result.header;\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 return;\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(state, result, binaryReader, options) {\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 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 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 case STATE.EXPECTING_RECORD:\n while (binaryReader.hasAvailableBytes(SHP_RECORD_HEADER_SIZE)) {\n const recordHeaderView = binaryReader.getDataView(SHP_RECORD_HEADER_SIZE);\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 if (!binaryReader.hasAvailableBytes(recordHeader.byteLength - 4)) {\n binaryReader.rewind(SHP_RECORD_HEADER_SIZE);\n return state;\n }\n const invalidRecord = recordHeader.byteLength < 4 ||\n recordHeader.type !== result.header?.type ||\n recordHeader.recordNumber !== result.currentIndex;\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 }\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 const recordView = binaryReader.getDataView(recordHeader.byteLength);\n const geometry = parseRecord(recordView, options);\n result.geometries.push(geometry);\n result.currentIndex++;\n result.progress.rows = result.currentIndex - 1;\n }\n }\n if (binaryReader.ended) {\n state = STATE.END;\n }\n return state;\n default:\n state = STATE.ERROR;\n result.error = `illegal parser state ${state}`;\n return state;\n }\n }\n catch (error) {\n state = STATE.ERROR;\n result.error = `SHP parsing failed: ${error?.message}`;\n return state;\n }\n }\n}\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\nimport { parseSHP, parseSHPInBatches } from \"./lib/parsers/parse-shp.js\";\n// __VERSION__ is injected by babel-plugin-version-inline\n// @ts-ignore TS2304: Cannot find name '__VERSION__'.\nconst VERSION = typeof \"4.3.3\" !== 'undefined' ? \"4.3.3\" : 'latest';\nexport const SHP_MAGIC_NUMBER = [0x00, 0x00, 0x27, 0x0a];\n/**\n * SHP file loader\n */\nexport const SHPWorkerLoader = {\n dataType: null,\n batchType: null,\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};\n/** SHP file loader */\nexport const SHPLoader = {\n ...SHPWorkerLoader,\n parse: async (arrayBuffer, options) => parseSHP(arrayBuffer, options),\n parseSync: parseSHP,\n parseInBatches: (arrayBufferIterator, options) => parseSHPInBatches(arrayBufferIterator, options)\n};\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n// import type {Feature} from '@loaders.gl/gis';\nimport { parseInBatchesFromContext, parseFromContext } from '@loaders.gl/loader-utils';\nimport { binaryToGeometry, transformGeoJsonCoords } from '@loaders.gl/gis';\nimport { Proj4Projection } from '@math.gl/proj4';\nimport { parseShx } from \"./parse-shx.js\";\nimport { zipBatchIterators } from \"../streaming/zip-batch-iterators.js\";\nimport { SHPLoader } from \"../../shp-loader.js\";\nimport { DBFLoader } from \"../../dbf-loader.js\";\n/**\n * Parsing of file in batches\n */\n// eslint-disable-next-line max-statements, complexity\nexport async function* parseShapefileInBatches(asyncIterator, options, context) {\n const { reproject = false, _targetCrs = 'WGS84' } = options?.gis || {};\n const { shx, cpg, prj } = await loadShapefileSidecarFiles(options, context);\n // parse geometries\n const shapeIterable = await parseInBatchesFromContext(asyncIterator, SHPLoader, options, context);\n const shapeIterator = shapeIterable[Symbol.asyncIterator]?.() || shapeIterable[Symbol.iterator]?.();\n // parse properties\n let propertyIterator = null;\n const dbfResponse = await context?.fetch(replaceExtension(context?.url || '', 'dbf'));\n if (dbfResponse?.ok) {\n const propertyIterable = await parseInBatchesFromContext(dbfResponse, DBFLoader, {\n ...options,\n dbf: { encoding: cpg || 'latin1' }\n }, context);\n propertyIterator =\n propertyIterable[Symbol.asyncIterator]?.() || propertyIterable[Symbol.iterator]();\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 let dbfHeader = {};\n if (propertyIterator) {\n dbfHeader = (await propertyIterator.next()).value;\n if (dbfHeader && dbfHeader.batchType === 'metadata') {\n dbfHeader = (await propertyIterator.next()).value;\n }\n }\n const zippedIterator = propertyIterator\n ? zipBatchIterators(shapeIterator, propertyIterator, 'object-row-table')\n : shapeIterator;\n const zippedBatchIterable = {\n [Symbol.asyncIterator]() {\n return zippedIterator;\n }\n };\n for await (const batch of zippedBatchIterable) {\n let geometries;\n let properties;\n if (!propertyIterator) {\n geometries = batch;\n }\n else {\n [geometries, properties] = batch.data;\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 * Parse shapefile\n *\n * @param arrayBuffer\n * @param options\n * @param context\n * @returns output of shapefile\n */\nexport async function parseShapefile(arrayBuffer, options, context) {\n const { reproject = false, _targetCrs = 'WGS84' } = options?.gis || {};\n const { shx, cpg, prj } = await loadShapefileSidecarFiles(options, context);\n // parse geometries\n const { header, geometries } = await parseFromContext(arrayBuffer, SHPLoader, options, context); // {shp: shx}\n const geojsonGeometries = parseGeometries(geometries);\n // parse properties\n let propertyTable;\n const dbfResponse = await context?.fetch(replaceExtension(context?.url, 'dbf'));\n if (dbfResponse?.ok) {\n propertyTable = await parseFromContext(dbfResponse, DBFLoader, { dbf: { shape: 'object-row-table', encoding: cpg || 'latin1' } }, context);\n }\n let features = joinProperties(geojsonGeometries, propertyTable?.data || []);\n if (reproject) {\n features = reprojectFeatures(features, prj, _targetCrs);\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 * Parse geometries\n *\n * @param geometries\n * @returns geometries as an array\n */\nfunction parseGeometries(geometries) {\n const geojsonGeometries = [];\n for (const geom of geometries) {\n geojsonGeometries.push(binaryToGeometry(geom));\n }\n return geojsonGeometries;\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, properties) {\n const features = [];\n for (let i = 0; i < geometries.length; i++) {\n const geometry = geometries[i];\n const 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 return features;\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, sourceCrs, targetCrs) {\n if (!sourceCrs && !targetCrs) {\n return features;\n }\n const projection = new Proj4Projection({ from: sourceCrs || 'WGS84', to: targetCrs || 'WGS84' });\n return transformGeoJsonCoords(features, (coord) => projection.project(coord));\n}\n/**\n *\n * @param options\n * @param context\n * @returns Promise\n */\n// eslint-disable-next-line max-statements\nexport async function loadShapefileSidecarFiles(options, context) {\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 let shx;\n let cpg;\n let prj;\n const shxResponse = await shxPromise;\n if (shxResponse.ok) {\n const arrayBuffer = await shxResponse.arrayBuffer();\n shx = parseShx(arrayBuffer);\n }\n const cpgResponse = await cpgPromise;\n if (cpgResponse.ok) {\n cpg = await cpgResponse.text();\n }\n const prjResponse = await prjPromise;\n if (prjResponse.ok) {\n prj = await prjResponse.text();\n }\n return {\n shx,\n cpg,\n prj\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, newExtension) {\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// 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) {\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) {\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\nimport { parseSHPHeader } from \"./parse-shp-header.js\";\nconst SHX_HEADER_SIZE = 100;\nconst BIG_ENDIAN = false;\n/**\n * @param arrayBuffer\n * @returns SHXOutput\n */\nexport function parseShx(arrayBuffer) {\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 const contentView = new DataView(arrayBuffer, SHX_HEADER_SIZE, contentLength);\n const offsets = new Int32Array(contentLength);\n const lengths = new Int32Array(contentLength);\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 return {\n offsets,\n lengths\n };\n}\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n/**\n * Zip two iterators together\n *\n * @param iterator1\n * @param iterator2\n */\nexport async function* zipBatchIterators(iterator1, iterator2, shape) {\n const batch1Data = [];\n const batch2Data = [];\n let iterator1Done = false;\n let iterator2Done = false;\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 }\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 }\n else {\n batch2Data.push(...value);\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 * Extract batch of same length from two batches\n *\n * @param batch1\n * @param batch2\n * @return array | null\n */\nfunction extractBatchData(batch1, batch2) {\n const batchLength = Math.min(batch1.length, batch2.length);\n if (batchLength === 0) {\n return null;\n }\n // Non interleaved arrays\n const batch = [batch1.slice(0, batchLength), batch2.slice(0, batchLength)];\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\nimport { BinaryChunkReader } from \"../streaming/binary-chunk-reader.js\";\nconst LITTLE_ENDIAN = true;\nconst DBF_HEADER_SIZE = 32;\nvar STATE;\n(function (STATE) {\n STATE[STATE[\"START\"] = 0] = \"START\";\n STATE[STATE[\"FIELD_DESCRIPTORS\"] = 1] = \"FIELD_DESCRIPTORS\";\n STATE[STATE[\"FIELD_PROPERTIES\"] = 2] = \"FIELD_PROPERTIES\";\n STATE[STATE[\"END\"] = 3] = \"END\";\n STATE[STATE[\"ERROR\"] = 4] = \"ERROR\";\n})(STATE || (STATE = {}));\nclass DBFParser {\n binaryReader = new BinaryChunkReader();\n textDecoder;\n state = STATE.START;\n result = {\n data: []\n };\n constructor(options) {\n this.textDecoder = new TextDecoder(options.encoding);\n }\n /**\n * @param arrayBuffer\n */\n write(arrayBuffer) {\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 // important events:\n // - schema available\n // - first rows available\n // - all rows available\n }\n end() {\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 * @param arrayBuffer\n * @param options\n * @returns DBFTable or rows\n */\nexport function parseDBF(arrayBuffer, options = {}) {\n const { encoding = 'latin1' } = options.dbf || {};\n const dbfParser = new DBFParser({ encoding });\n dbfParser.write(arrayBuffer);\n dbfParser.end();\n const { data, schema } = dbfParser.result;\n const shape = options?.dbf?.shape;\n switch (shape) {\n case 'object-row-table': {\n const table = {\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(asyncIterator, options = {}) {\n const { encoding = 'latin1' } = options.dbf || {};\n const parser = new DBFParser({ encoding });\n let headerReturned = false;\n for await (const arrayBuffer of asyncIterator) {\n parser.write(arrayBuffer);\n if (!headerReturned && parser.result.dbfHeader) {\n headerReturned = true;\n yield parser.result.dbfHeader;\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(state, result, binaryReader, textDecoder) {\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 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 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 if (!fieldDescriptorView) {\n return state;\n }\n result.dbfFields = parseFieldDescriptors(fieldDescriptorView, textDecoder);\n result.schema = {\n fields: result.dbfFields.map((dbfField) => makeField(dbfField)),\n metadata: {}\n };\n state = STATE.FIELD_PROPERTIES;\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 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 // @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 default:\n state = STATE.ERROR;\n result.error = `illegal parser state ${state}`;\n return state;\n }\n }\n catch (error) {\n state = STATE.ERROR;\n result.error = `DBF parsing failed: ${error.message}`;\n return state;\n }\n }\n}\n/**\n * @param headerView\n */\nfunction parseDBFHeader(headerView) {\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 * @param view\n */\nfunction parseFieldDescriptors(view, textDecoder) {\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 = [];\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 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 * @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 * @param view\n * @param fields\n * @param textDecoder\n * @returns\n */\nfunction parseRow(view, fields, textDecoder) {\n const out = {};\n let offset = 0;\n for (const field of fields) {\n const text = textDecoder.decode(new Uint8Array(view.buffer, view.byteOffset + offset, field.fieldLength));\n out[field.name] = parseField(text, field.dataType);\n offset += field.fieldLength;\n }\n return out;\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, dataType) {\n switch (dataType) {\n case 'B':\n return parseNumber(text);\n case 'C':\n return parseCharacter(text);\n case 'F':\n return parseNumber(text);\n case 'N':\n return parseNumber(text);\n case 'O':\n return parseNumber(text);\n case 'D':\n return parseDate(text);\n case 'L':\n return parseBoolean(text);\n default:\n throw new Error('Unsupported data type');\n }\n}\n/**\n * Parse YYYYMMDD to date in milliseconds\n * @param str YYYYMMDD\n * @returns new Date as a number\n */\nfunction parseDate(str) {\n return Date.UTC(str.slice(0, 4), parseInt(str.slice(4, 6), 10) - 1, str.slice(6, 8));\n}\n/**\n * Read boolean value\n * any of Y, y, T, t coerce to true\n * any of N, n, F, f coerce to false\n * otherwise null\n * @param value\n * @returns boolean | null\n */\nfunction parseBoolean(value) {\n return /^[nf]$/i.test(value) ? false : /^[yt]$/i.test(value) ? true : null;\n}\n/**\n * Return null instead of NaN\n * @param text\n * @returns number | null\n */\nfunction parseNumber(text) {\n const number = parseFloat(text);\n return isNaN(number) ? null : number;\n}\n/**\n *\n * @param text\n * @returns string | null\n */\nfunction parseCharacter(text) {\n return text.trim() || null;\n}\n/**\n * Create a standard Arrow-style `Field` from field descriptor.\n * TODO - use `fieldLength` and `decimal` to generate smaller types?\n * @param param0\n * @returns Field\n */\n// eslint-disable\nfunction makeField({ name, dataType, fieldLength, decimal }) {\n switch (dataType) {\n case 'B':\n return { name, type: 'float64', nullable: true, metadata: {} };\n case 'C':\n return { name, type: 'utf8', nullable: true, metadata: {} };\n case 'F':\n return { name, type: 'float64', nullable: true, metadata: {} };\n case 'N':\n return { name, type: 'float64', nullable: true, metadata: {} };\n cas