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