UNPKG

s2-tools

Version:

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

258 lines 8.91 kB
import { HeaderBlock } from './headerBlock'; import { KV } from '../../dataStore'; import { PrimitiveBlock } from './primitive'; import { Pbf as Protobuf } from '../../readers/protobuf'; import { intermediateRelationToVectorFeature } from './relation'; import { intermediateWayToVectorFeature } from './way'; import { mergeRelationIfExists } from './node'; import { tmpdir } from 'os'; import { toReader } from '..'; import { Blob, BlobHeader } from './blob'; /** * TagFilter Class * Builds a filter for the tags when parsing data. * Can parse tags from nodes, ways and relations. * Also allows the ability to add tags that apply to all object types. * Can filter by key, but also both key and value. */ export class TagFilter { #filters = new Map(); #nodeFilters = new Map(); #wayFilters = new Map(); #relationFilters = new Map(); /** * Internal method to get the correct filter map * @param filterType - The filter type * @returns - The correct filter map to check against */ #getFilter(filterType) { if (filterType === 'All') return this.#filters; else if (filterType === 'Node') return this.#nodeFilters; else if (filterType === 'Way') return this.#wayFilters; else return this.#relationFilters; } /** * Add a filter * @param filterType - The filter type to apply the filter * @param key - The key to apply the filter * @param value - The value to apply the filter (optional) */ addFilter(filterType, key, value) { const filter = this.#getFilter(filterType); filter.set(key, value); } /** * Check if a filter has been found * @param filterType - The filter type * @param key - The key * @param value - The value (optional) * @returns - True if the filter has been found */ matchFound(filterType, key, value) { const filter = this.#getFilter(filterType); // check all filters first if (this.#filters.has(key)) { const filterValue = this.#filters.get(key); if (filterValue === value) return true; } // check type-specific filters if (filterType !== 'All' && filter.has(key)) { const filterValue = filter.get(key); if (filterValue === value) return true; } return false; } } /** * # OSM Reader * * ## Description * Parses OSM PBF files * Implements the {@link FeatureIterator} interface * * ## Usage * ```ts * import { OSMReader } from 's2-tools'; * import { FileReader } from 's2-tools/file'; * * const reader = new OSMReader(new FileReader('./data.osm.pbf')); * // pull out the header * const header = reader.getHeader(); * // read the features * for (const feature of reader) { * console.log(feature); * } * // close the reader when done * reader.close(); * ``` * * ## Links * - https://wiki.openstreetmap.org/wiki/PBF_Format * - https://github.com/openstreetmap/pbf/blob/master/OSM-binary.md */ export class OSMReader { options; reader; /** if true, remove nodes that have no tags [Default = true] */ removeEmptyNodes; /** If provided, filters of the */ tagFilter; /** If set to true, nodes will be skipped */ skipNodes; /** If set to true, ways will be skipped */ skipWays; /** If set to true, relations will be skipped */ skipRelations; /** * If set to true, ways will be converted to areas if they are closed. * NOTE: They are upgraded anyways if the tag "area" is set to "yes". * [Default = false] */ upgradeWaysToAreas; /** If set to true, add a bbox property to each feature */ addBBox; nodeGeometry = new KV(); nodes = new KV(); wayGeometry = new KV(); ways = new KV(); relations = new KV(); nodeRelationPairs = new KV(); #offset = 0; /** * @param input - The input (may be a local memory filter or file reader) * @param options - User defined options to apply when reading the OSM file */ constructor(input, options) { this.options = options; this.reader = toReader(input); this.removeEmptyNodes = options?.removeEmptyNodes ?? true; this.tagFilter = options?.tagFilter; this.skipNodes = options?.skipNodes ?? false; this.skipWays = options?.skipWays ?? false; this.skipRelations = options?.skipRelations ?? false; this.upgradeWaysToAreas = options?.upgradeWaysToAreas ?? false; this.addBBox = options?.addBBox ?? false; const store = options?.store; if (store !== undefined) { this.nodeGeometry = new store(buildTmpFileName('nodeGeometry')); this.nodes = new store(buildTmpFileName('nodes')); this.wayGeometry = new store(buildTmpFileName('wayGeometry')); this.ways = new store(buildTmpFileName('ways')); this.relations = new store(buildTmpFileName('relations')); this.nodeRelationPairs = new store(buildTmpFileName('nodeRelationPairs')); } } /** * An async iterator to read in each feature * @yields {VectorFeature} */ async *[Symbol.asyncIterator]() { this.#offset = 0; // skip the header this.#next(); // PARSE while (true) { const blob = this.#next(); if (blob === undefined) break; await this.#readBlob(blob); } // NODES if (!this.skipNodes) { for await (const node of this.nodes) { await mergeRelationIfExists(node, this); yield node; } } // WAYS if (!this.skipWays) { for await (const interWay of this.ways) { const way = await intermediateWayToVectorFeature(interWay, this); if (way !== undefined) yield way; } } // RELATIONS if (!this.skipRelations) { for await (const interRelation of this.relations) { const relation = await intermediateRelationToVectorFeature(interRelation, this); if (relation !== undefined) yield relation; } } } /** * @returns - The header of the OSM file */ getHeader() { this.#offset = 0; const blobHeader = this.#next(); if (blobHeader === undefined) throw new Error('Header not found'); const headerBlock = new HeaderBlock(new Protobuf(new Uint8Array(blobHeader.buffer))); return headerBlock.toHeader(); } /** * Read the next blob * @returns - the next blob if it exists */ #next() { const { reader } = this; // if we've already read all the data, return null if (this.#offset >= reader.byteLength) return; // STEP 1: Get blob size // read length of current blob const length = reader.getInt32(this.#offset); this.#offset += 4; const blobHeaderData = reader.slice(this.#offset, this.#offset + length); this.#offset += length; // build a blob header const pbf = new Protobuf(new Uint8Array(blobHeaderData.buffer)); const blobHeader = new BlobHeader(pbf); // STEP 2: Get blob data const compressedBlobData = reader.slice(this.#offset, this.#offset + blobHeader.datasize); this.#offset += blobHeader.datasize; return compressedBlobData; } /** * Read the input blob and parse the block of data * @param data - the data to parse * @returns - the parsed primitive block */ async #readBlob(data) { // Blob data is PBF encoded and ?compressed, so we need to parse & decompress it first let pbf = new Protobuf(new Uint8Array(data.buffer)); const blob = new Blob(pbf); pbf = new Protobuf(await blob.data); // Parse the PrimitiveBlock and read its contents. // all nodes/ways/relations that can be filtered already are on invocation. return new PrimitiveBlock(pbf, this); } /** Close out the data which will cleanup any temporary files if they exist */ close() { this.nodeGeometry.close(); this.nodes.close(); this.wayGeometry.close(); this.ways.close(); this.relations.close(); this.nodeRelationPairs.close(); } } /** * Build a temporary file name * @param name - the name of the temporary file * @returns - a temporary file name based on a random number. */ function buildTmpFileName(name) { const tmpd = tmpdir(); const randomName = Math.random().toString(36).slice(2); return `${tmpd}/${name ?? ''}_${randomName}`; } //# sourceMappingURL=index.js.map