UNPKG

s2-tools

Version:

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

309 lines 11.4 kB
import { Info } from './info'; import { fromMultiLineString, fromMultiPolygon } from '../../geometry'; /** * @param relation - the intermediate relation * @param reader - the OSM reader * @returns - the feature in vector format */ export async function intermediateRelationToVectorFeature(relation, reader) { const { addBBox, nodeGeometry, wayGeometry } = reader; const { id, members, properties, metadata } = relation; const iNodes = members.filter((m) => 'node' in m); const nodes = []; for (const { role, node } of iNodes) { const n = await nodeGeometry.get(node); if (n === undefined) return; nodes.push({ id: node, role, node: n }); } const iWays = members.filter((m) => 'way' in m); const ways = []; for (const { role, way } of iWays) { const w = await wayGeometry.get(way); if (w === undefined) return; const mappedW = []; for (const nodeID of w) { const n = await nodeGeometry.get(nodeID); if (n === undefined) return; mappedW.push(n); } ways.push({ id: way, role, way: mappedW }); } const geo = buildGeometry(ways); if (geo === undefined) return; const { type, coordinates } = geo; let bbox; if (addBBox) { if (type === 0) bbox = fromMultiLineString(coordinates); else bbox = fromMultiPolygon(coordinates); } const is3D = false; const geometry = type === 0 ? coordinates.length === 1 ? { type: 'LineString', is3D, coordinates: coordinates[0], bbox } : { type: 'MultiLineString', is3D, coordinates, bbox } : coordinates.length === 1 ? { type: 'Polygon', is3D, coordinates: coordinates[0], bbox } : { type: 'MultiPolygon', is3D, coordinates, bbox }; return { id, type: 'VectorFeature', properties, geometry, metadata: { info: metadata, nodes: iNodes, }, }; } /** Member Type can be Node (0), Way (1) or Relation (2). */ export var MemberType; (function (MemberType) { /** Node Member */ MemberType[MemberType["Node"] = 0] = "Node"; /** Way Member */ MemberType[MemberType["Way"] = 1] = "Way"; /** Relation Member */ MemberType[MemberType["Relation"] = 2] = "Relation"; })(MemberType || (MemberType = {})); /** * Relation class contains a collection of nodes, ways and relations as members. */ export class Relation { primitiveBlock; reader; id = -1; info; // Parallel arrays #keys = []; #vals = []; #rolesSid = []; // This should have been defined as uint32 for consistency, but it is now too late to change it #memids = []; // DELTA encoded #types = []; /** * @param primitiveBlock - the primitive block * @param reader - the OSM reader * @param pbf - the Protobuf if provided */ constructor(primitiveBlock, reader, pbf) { this.primitiveBlock = primitiveBlock; this.reader = reader; this.primitiveBlock = primitiveBlock; if (pbf !== undefined) pbf.readMessage(this.#readLayer, this); } /** @returns - true if the relation is filterable */ isFilterable() { const { primitiveBlock: pb, reader } = this; const { tagFilter, skipRelations } = reader; if (skipRelations) return true; if (tagFilter !== undefined) { for (let i = 0; i < this.#keys.length; i++) { const keyStr = pb.getString(this.#keys[i]); const valStr = pb.getString(this.#vals[i]); if (tagFilter.matchFound('Relation', keyStr, valStr)) return false; } // if we make it here, we didn't find any matching tags return true; } return false; } /** @returns - the properties of the relation */ properties() { return this.primitiveBlock.tags(this.#keys, this.#vals); } /** * Each member can be node, way or relation. * @returns an array of members associated with this relation */ members() { const { primitiveBlock: pb } = this; const res = []; let memid = 0; for (let i = 0; i < this.#memids.length; i++) { memid += this.#memids[i]; const role = pb.getString(this.#rolesSid[i]); const curType = this.#types[i]; if (curType === MemberType.Node) { res.push({ role, node: memid, relationID: this.id }); } else if (curType === MemberType.Way) { res.push({ role, way: memid }); } else { // Relation -> no-op } } return res; } /** @returns - the feature in intermediate format to build later */ toIntermediateFeature() { const members = this.members(); if (members.length === 0) return; return { id: this.id, properties: this.properties(), members, metadata: this.info?.toBlock() ?? {}, }; } /** * @param tag - the tag * @param relation - the relation to update * @param pbf - the protobuf to parse from */ #readLayer(tag, relation, pbf) { if (tag === 1) relation.id = pbf.readVarint(); else if (tag === 2) relation.#keys = pbf.readPackedVarint(); else if (tag === 3) relation.#vals = pbf.readPackedVarint(); else if (tag === 4) relation.info = new Info(relation.primitiveBlock, pbf); else if (tag === 8) relation.#rolesSid = pbf.readPackedVarint(); else if (tag === 9) relation.#memids = pbf.readPackedSVarint(); else if (tag === 10) relation.#types = pbf.readPackedVarint(); else throw new Error(`unexpected tag ${tag}`); } } /** * @param members - an array of members * @returns - an array of node members that have a 'label' or 'admin_centre' role */ export function getNodeRelationPairs(members) { const res = []; for (const member of members) { if ('node' in member && (member.role === 'label' || member.role === 'admin_centre')) res.push(member); } return res; } /** * Given a group of Members whose type is "way", build a multilinestring or multipolygon Feature. * If the ways include an 'outer' or 'inner', then we know its an area, otherwise its a line. * @param ways - an array of way members * @returns - a multipolygon */ function buildGeometry(ways) { // prep variables const polygons = []; const currentPolygon = []; const currentRing = []; const isArea = ways.some((m) => m.role === 'outer') || ways.some((m) => m.role === 'inner'); // prepare step: members are stored out of order sortMembers(ways); for (const member of ways) { // Using "isClockwise", depending on whether the ring is outer or inner, // we may need to reverse the order of the points. Every time we find the // first and last point are the same, close out the ring, add it to the current // polygon, and start a new ring. if the current polygon is NOT empty, we store // it in the polygons list and start a new one before adding the completed ring. // NOTE: Due to the nature of OSM data, it is possible that resulting ring is reversed. // Check against the current ring to see if the way needs to be edited. // // grab the geometry from the member const geometry = member.way; if (geometry === undefined) return; // store in current ring, checking current rings order if (currentRing.length === 0) { currentRing.push(...geometry); } else { currentRing.push(...geometry.slice(1)); } // if current rings first and last point are the same, close out the ring if (equalPoints(currentRing[0], currentRing[currentRing.length - 1])) { // add the ring to the current polygon. If member role is outer and // currentPolygon already has data, we need to store the current poly and // start a new polygon. // If the member role is inner, we can add the ring to the // current polygon. if (member.role === 'outer' && currentPolygon.length > 0) { polygons.push(currentPolygon); } currentPolygon.push(currentRing); } } // Last step is to build: // flush ring if it exists if (currentRing.length > 0) currentPolygon.push(currentRing); if (!isArea) return { type: 0, coordinates: currentPolygon }; // flush the current polygon if it exists if (currentPolygon.length > 0) polygons.push(currentPolygon); // grab the polys and return a feature return { type: 1, coordinates: polygons }; } /** * @param a - the first point * @param b - the second point * @returns true if the points are equal */ function equalPoints(a, b) { return a.x === b.x && a.y === b.y; } /** * osm throws relation members out of order, so we need to not only sort them * but also check if the first and last points of each way follow the same direction. * @param members - the ways to be sorted */ function sortMembers(members) { if (members.length < 3) return; for (let i = 0; i < members.length - 1; i++) { const curWay = members[i].way; const curFirstPoint = curWay[0]; const curLastPoint = curWay[curWay.length - 1]; // if current way is already self closing break if (curFirstPoint === curLastPoint) break; for (let j = i + 1; j < members.length; j++) { const nextWay = members[j].way; const nextFirstPoint = nextWay[0]; const nextLastPoint = nextWay[nextWay.length - 1]; // if we find a match between any of the points, swap the member positions // if curFirstPoint == nextFirstPoint or curLastPoint == nextLastPoint // swap the order const equalFirst = equalPoints(curFirstPoint, nextFirstPoint); const equalLast = equalPoints(curLastPoint, nextLastPoint); const equalFirstLast = equalPoints(curFirstPoint, nextLastPoint); const equalLastFirst = equalPoints(curLastPoint, nextFirstPoint); if (equalFirst || equalLast || equalFirstLast || equalLastFirst) { if (equalFirst) { curWay.reverse(); } else if (equalLast) { nextWay.reverse(); } else if (equalFirstLast) { curWay.reverse(); nextWay.reverse(); } // we want to move the found member to be next to the current member if (i + 1 !== j) { const temp = members[i + 1]; members[i + 1] = members[j]; members[j] = temp; } break; } } } } //# sourceMappingURL=relation.js.map