gis-tools-ts
Version:
A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.
233 lines • 7.96 kB
JavaScript
import { Vector } from '../dataStore/index.js';
import { chordAngFromS2Points } from '../geometry/s1/chordAngle.js';
import { pointFromST } from '../geometry/s2/point.js';
import { capFromS1ChordAngle, capGetIntersectingCells } from '../geometry/s2/cap.js';
import { compareIDs, convert, idFromS2Point, idRange } from '../geometry/index.js';
/**
* # Point Index
*
* ## Description
* An index of cells with radius queries
* Assumes the data is compatible with {@link https://open-s2.github.io/s2json/types/Properties.html}
*
* ## Usage
* ```ts
* import { PointIndex } from 'gis-tools-ts';
* import { FileVector } from 'gis-tools-ts/file';
*
* const pointIndex = new PointIndex();
* // or used a file based store
* const pointIndex = new PointIndex(FileVector);
*
* // insert a lon-lat
* pointIndex.insertLonLat(lon, lat, data);
* // insert an STPoint
* pointIndex.insertFaceST(face, s, t, data);
*
* // after adding data build the index. NOTE: You don't have to call this, it will be called
* // automatically when making a query
* await pointIndex.sort();
*
* // you can search a range
* const points = await pointIndex.searchRange(low, high);
* // or a radius
* const points = await pointIndex.searchRadius(center, radius);
* ```
*/
export class PointIndex {
projection;
#store;
#unsorted = false;
/**
* @param store - the store to index. May be an in memory or disk
* @param projection - the projection of the data, defaults to S2
*/
constructor(store = Vector, projection = 'S2') {
this.projection = projection;
this.#store = new store();
}
/**
* Set the index store to a defined one. Useful for file based stores where we want to reuse data
* @param store - the index store
*/
setStore(store) {
this.#store = store;
}
/**
* Insert a cell with the point and its corresponding data to the index
* @param cell - the cell id to be indexed
* @param point - the point to be indexed
*/
insertID(cell, point) {
this.#store.push({ cell, point });
this.#unsorted = true;
}
/**
* Insert a point3D and its corresponding data to the index
* @param point - the point to be indexed
*/
insert(point) {
this.insertID(idFromS2Point(point), point);
}
/**
* Add all points from a reader. It will try to use the M-value first, but if it doesn't exist
* it will use the feature properties data
* @param reader - a reader containing the input data
*/
async insertReader(reader) {
for await (const feature of reader)
this.insertFeature(feature);
}
/**
* Add a vector feature. It will try to use the M-value first, but if it doesn't exist
* it will use the feature properties data
* @param data - any source of data like a feature collection or features themselves
*/
insertFeature(data) {
const features = convert(this.projection, data, undefined, true);
for (const { face = 0, geometry, properties } of features) {
const { type, coordinates } = geometry;
if (type === 'Point') {
const { x: s, y: t, m } = coordinates;
this.#insertFaceST(face, s, t, m ?? properties);
}
else if (type === 'MultiPoint') {
for (const point of coordinates) {
const { x: s, y: t, m } = point;
this.#insertFaceST(face, s, t, m ?? properties);
}
}
}
}
/**
* Add a lon-lat pair to the cluster
* @param ll - lon-lat vector point in degrees
*/
insertLonLat(ll) {
this.insertFeature({
type: 'VectorFeature',
properties: ll.m ?? {},
geometry: { type: 'Point', coordinates: ll, is3D: false },
});
}
/**
* Insert an STPoint to the index
* @param face - the face of the cell
* @param s - the s coordinate
* @param t - the t coordinate
* @param data - the data associated with the point
*/
insertFaceST(face, s, t, data) {
this.insertFeature({
type: 'S2Feature',
face,
properties: data,
geometry: { type: 'Point', coordinates: { x: s, y: t, m: data }, is3D: false },
});
}
/**
* Insert an STPoint to the index
* @param face - the face of the cell
* @param s - the s coordinate
* @param t - the t coordinate
* @param data - the data associated with the point
*/
#insertFaceST(face, s, t, data) {
this.insert(pointFromST(face, s, t, data));
}
/**
* iterate through the points
* @yields {PointShape<M>} - a PointShape<T>
*/
async *[Symbol.asyncIterator]() {
await this.sort();
yield* this.#store;
}
/** Sort the index in place if unsorted */
async sort() {
if (!this.#unsorted)
return;
await this.#store.sort();
this.#unsorted = false;
}
/**
* Find the starting index of a search
* @param id - input id to seek the starting index of the search
* @returns the starting index
*/
async lowerBound(id) {
await this.sort();
// lower bound search
let lo = 0;
let hi = this.#store.length;
let mid;
while (lo < hi) {
mid = Math.floor(lo + (hi - lo) / 2);
const { cell: midCell } = await this.#store.get(mid);
if (compareIDs(midCell, id) < 0) {
lo = mid + 1;
}
else {
hi = mid;
}
}
return lo;
}
/**
* Search for points given a range of low and high ids
* @param low - the lower bound. If high is not provided, the low-high range will be created from the low
* @param high - the upper bound
* @param maxResults - the maximum number of results to return
* @returns the points in the range
*/
async searchRange(low, high, maxResults = Infinity) {
await this.sort();
const res = [];
if (high === undefined) {
const [lo, hi] = idRange(low);
low = lo;
high = hi;
}
let loIdx = await this.lowerBound(low);
while (true) {
if (loIdx >= this.#store.length)
break;
const currLo = await this.#store.get(loIdx);
if (compareIDs(currLo.cell, high) > 0)
break;
res.push(currLo);
if (res.length >= maxResults)
break;
loIdx++;
}
return res;
}
/**
* TODO: Adjust the radius for the WM projection. Really not a massive issue thogh just adjust your calcuation for now
* Search for points within a given radius of a target point
* @param target - the point to search
* @param radius - the search radius
* @param maxResults - the maximum number of results
* @returns the points within the radius
*/
async searchRadius(target, radius, maxResults = Infinity) {
await this.sort();
const res = [];
if (radius < 0)
return res;
const cap = capFromS1ChordAngle(target, radius, undefined);
for (const cell of capGetIntersectingCells(cap)) {
// iterate each covering s2cell min-max range on store. check distance from found
// store Cells to target and if within radius add to results
const [min, max] = idRange(cell);
for (const point of await this.searchRange(min, max)) {
if (chordAngFromS2Points(target, point.point) < radius)
res.push(point);
if (res.length >= maxResults)
break;
}
}
return res;
}
}
//# sourceMappingURL=pointIndex.js.map