UNPKG

s2-tools

Version:

A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.

192 lines 6.27 kB
import { toReader } from '..'; /** * # CSV Reader * * ## Description * Parse (Geo|S2)JSON from a file that is in the CSV format * Implements the {@link FeatureIterator} interface * * ## Usage * ```ts * import { CSVReader } from 's2-tools'; * import { FileReader } from 's2-tools/file'; * * const fileReader = new FileReader(`${__dirname}/fixtures/basic3D.csv`); * const csvReader = new CSVReader(fileReader, { * delimiter: ',', * lineDelimiter: '\n', * lonKey: 'Longitude', * latKey: 'Latitude', * heightKey: 'height', * }); * // read the features * for await (const feature of reader) { * console.log(feature); * } * ``` */ export class CSVReader { reader; #delimiter; #lineDelimiter; #lonKey = 'lon'; #latKey = 'lat'; #heightKey; #firstLine = true; #fields = []; /** * @param input - the input data to parse from * @param options - user defined options on how to parse the CSV file */ constructor(input, options) { this.reader = toReader(input); this.#delimiter = options?.delimiter ?? ','; this.#lineDelimiter = options?.lineDelimiter ?? '\n'; this.#lonKey = options?.lonKey ?? 'lon'; this.#latKey = options?.latKey ?? 'lat'; this.#heightKey = options?.heightKey; } /** * Generator to iterate over each (Geo|S2)JSON object in the file * @yields {VectorFeature} */ async *[Symbol.asyncIterator]() { const { reader } = this; let cursor = 0; let offset = 0; let partialLine = ''; while (offset < reader.byteLength) { const length = Math.min(65_536, reader.byteLength - cursor); // Prepend any partial line to the new chunk const chunk = partialLine + reader.parseString(offset, length); partialLine = ''; // Split the chunk by newlines and yield each complete line const lines = chunk.split(this.#lineDelimiter); for (let i = 0; i < lines.length - 1; i++) { if (this.#firstLine) { this.#parseFirstLine(lines[i]); this.#firstLine = false; } else { yield this.#parseLine(lines[i]); } } // Store the remaining partial line for the next iteration partialLine = lines[lines.length - 1]; // Update the cursor and offset offset += length; cursor += length; } // Yield any remaining partial line after the loop if (partialLine.length > 0) yield this.#parseLine(partialLine); } /** * @param line - the values mapped to the first lines fields * @returns - a GeoJSON Vector Feature */ #parseLine(line) { const values = line.split(this.#delimiter).map((v) => v.trim()); let is3D = false; const properties = {}; const coordinates = { x: 0, y: 0 }; for (let i = 0; i < this.#fields.length; i++) { const field = this.#fields[i]; const value = values[i]; if (field.length === 0 || value.length === 0) continue; if (field === this.#lonKey) coordinates.x = parseFloat(value); else if (field === this.#latKey) coordinates.y = parseFloat(value); else if (this.#heightKey !== undefined && field === this.#heightKey) { is3D = true; coordinates.z = parseFloat(value); } else properties[field] = value; } if (isNaN(coordinates.x) || isNaN(coordinates.y)) throw new Error('coordinates must be finite numbers'); return { type: 'VectorFeature', geometry: { type: 'Point', is3D, coordinates, }, properties: properties, }; } /** @param line - the fields in the first line split by the delimiter */ #parseFirstLine(line) { this.#fields = line.split(this.#delimiter).map((v) => v.trim()); } } /** * Parse CSV data into a record * @param source - the source of the CSV data * @returns - an object with key-value pairs whose keys and values are both strings */ export function parseCSVAsRecord(source) { const res = []; const lines = source.split('\n'); const header = parseCSVLine(lines[0]); for (const rawLine of lines.slice(1)) { const line = rawLine.trim(); if (line.length === 0) continue; const record = {}; const values = parseCSVLine(line); for (let i = 0; i < header.length; i++) { const value = values[i]; if (value === '' || value === ' ') continue; record[header[i]] = values[i] ?? ''; } res.push(record); } return res; } /** * @param line - a line of a CSV file * @returns - the values split by the delimiter */ function parseCSVLine(line) { const result = []; let current = ''; let inQuotes = false; let quoteChar = ''; for (let i = 0; i < line.length; i++) { const ch = line[i]; // If we encounter a quote and we aren't in quoted text yet if ((ch === '"' || ch === "'") && !inQuotes) { inQuotes = true; quoteChar = ch; } // If we see the same quote again, it might be the closing quote else if (ch === quoteChar && inQuotes) { // Check if it's an escaped quote by looking at the next character if (i < line.length - 1 && line[i + 1] === quoteChar) { current += quoteChar; i++; } else { inQuotes = false; } } // Split by commas only if not inside quotes else if (ch === ',' && !inQuotes) { result.push(current.trim()); current = ''; } else { current += ch; } } // Push the final field if (current !== undefined) result.push(current.trim()); return result; } //# sourceMappingURL=index.js.map