s2-tools
Version:
A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.
192 lines • 6.27 kB
JavaScript
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