Skip to content

Commit

Permalink
add convenience functions; improve a ton of typesafety; all readers o…
Browse files Browse the repository at this point in the history
…utput vectorFeatures; by default bbox is added to all features
  • Loading branch information
Mr Martian committed Jan 10, 2025
1 parent 2c846d7 commit eaf48a9
Show file tree
Hide file tree
Showing 37 changed files with 1,071 additions and 417 deletions.
Binary file modified bun.lockb
Binary file not shown.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
"geotiff": "^2.1.3",
"kdbush": "^4.0.2",
"lmdb": "^3.2.2",
"long": "^5.2.3",
"long": "^5.2.4",
"nextafter": "^1.0.0",
"prettier": "^3.4.2",
"robust-orientation": "^1.2.1",
Expand All @@ -106,7 +106,7 @@
"open-vector-tile": "^1.7.0",
"pbf-ts": "^1.0.2",
"s2-tilejson": "^1.8.5",
"s2json-spec": "^1.6.2",
"s2json-spec": "^1.7.3",
"sharp": "^0.33.5"
}
}
2 changes: 1 addition & 1 deletion src/converters/toJSON/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export async function toJSONLD(
): Promise<void> {
const projection = opts?.projection ?? 'S2';
const onFeature = opts?.onFeature ?? ((feature) => feature);
const buildBBox = opts?.buildBBox ?? false;
const buildBBox = opts?.buildBBox ?? true;

for (const iterator of iterators) {
for await (const feature of iterator) {
Expand Down
85 changes: 59 additions & 26 deletions src/dataStructures/pointCluster.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Tile } from '../dataStructures';
import { convert } from '../geometry/tools/convert';
import { fromS2Points } from '../geometry/s1/chordAngle';
import { PointShape as Point, PointIndex } from './pointIndex';
import {
Expand All @@ -11,21 +10,32 @@ import {
normalize,
toST,
} from '../geometry/s2/point';
import { fromFacePosLevel, getVertices, level, range } from '../geometry';
import { convert, fromFacePosLevel, getVertices, level, range, toWM } from '../geometry';

import type { FeatureIterator } from '..';
import type { PointShape } from './pointIndex';
import type { S1ChordAngle } from '../geometry/s1/chordAngle';
import type { Face, JSONCollection, Point3D, Projection, Properties, S2CellId } from '../geometry';
import type {
Face,
JSONCollection,
MValue,
Point3D,
Projection,
Properties,
S2CellId,
} from '../geometry';

import type { VectorStore, VectorStoreConstructor } from '../dataStore/vector';

/** The kind of input required to store a point for proper indexing */
export type ClusterStore = VectorStoreConstructor<PointShape<Cluster>>;
export type ClusterStore<P extends Properties = Properties> = VectorStoreConstructor<
PointShape<Cluster<P>>
>;

/** Options for point clustering */
export interface ClusterOptions {
export interface ClusterOptions<T extends Properties = Properties> {
/** type of store to use. Defaults to an in memory store */
store?: ClusterStore;
store?: ClusterStore<T>;
/** projection to use */
projection?: Projection;
/** Name of the layer to build when requesting a tile */
Expand All @@ -39,8 +49,8 @@ export interface ClusterOptions {
}

/** A cluster is a storage device to maintain groups of information in a cluster */
export interface Cluster extends Properties {
properties: Properties;
export interface Cluster<P extends Properties = Properties> extends Properties {
properties: P;
visited: boolean;
sum: number;
}
Expand All @@ -49,7 +59,7 @@ export interface Cluster extends Properties {
* @param properties - the properties associated with the cluster
* @returns - a new cluster
*/
function newCluster(properties: Properties): Cluster {
function newCluster<P extends Properties = Properties>(properties: P): Cluster<P> {
return { properties, visited: false, sum: 1 };
}

Expand All @@ -59,12 +69,12 @@ function newCluster(properties: Properties): Cluster {
* @param sum - the sum of the cluster
* @returns - a new cluster with the correct sum and properties data
*/
function sumToCluster(properties: Properties, sum: number): Cluster {
function sumToCluster<P extends Properties = Properties>(properties: P, sum: number): Cluster<P> {
return { properties, visited: false, sum };
}

/** Compare two data items, return true to merge data */
export type Comparitor = (a: Properties, b: Properties) => boolean;
export type Comparitor<P extends Properties = Properties> = (a: P, b: P) => boolean;

/**
* # Point Cluster
Expand All @@ -91,32 +101,36 @@ export type Comparitor = (a: Properties, b: Properties) => boolean;
* const clusters = await pointCluster.getCellData(id);
* ```
*/
export class PointCluster {
export class PointCluster<
M = Record<string, unknown>,
D extends MValue = Properties,
P extends Properties = Properties,
> {
projection: Projection;
layerName: string;
minzoom: number;
maxzoom: number;
radius: number;
extent = 512; // a 512x512 pixel tile
indexes = new Map<number, PointIndex<Cluster>>();
indexes = new Map<number, PointIndex<M, Cluster<D | P>, Cluster<D | P>>>();

/**
* @param data - if provided, the data to index
* @param options - cluster options on how to build the cluster
* @param maxzoomStore - the store to use for the maxzoom index
*/
constructor(
data?: JSONCollection,
options?: ClusterOptions,
maxzoomStore?: VectorStore<PointShape<Cluster>>,
data?: JSONCollection<M, D, P>,
options?: ClusterOptions<D | P>,
maxzoomStore?: VectorStore<PointShape<Cluster<D | P>>>,
) {
this.projection = options?.projection ?? 'S2';
this.layerName = options?.layerName ?? 'default';
this.minzoom = Math.max(options?.minzoom ?? 0, 0);
this.maxzoom = Math.min(options?.maxzoom ?? 16, 29);
this.radius = options?.radius ?? 40;
for (let zoom = this.minzoom; zoom <= this.maxzoom; zoom++) {
this.indexes.set(zoom, new PointIndex<Cluster>(options?.store));
this.indexes.set(zoom, new PointIndex<M, Cluster<D | P>, Cluster<D | P>>(options?.store));
}
if (maxzoomStore !== undefined) {
const maxzoomIndex = this.indexes.get(this.maxzoom);
Expand All @@ -141,18 +155,37 @@ export class PointCluster {
* @param point - the point to add
* @param data - the data associated with the point
*/
insert(point: Point3D, data: Properties): void {
insert(point: Point3D, data: D | P): void {
const maxzoomIndex = this.indexes.get(this.maxzoom);
maxzoomIndex?.insert(point, newCluster(data));
}

/**
* 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: FeatureIterator<M, D, P>): Promise<void> {
for await (const feature of reader) {
if (feature.geometry.type !== 'Point' && feature.geometry.type !== 'MultiPoint') continue;
const {
geometry: { coordinates, type },
} = feature.type === 'S2Feature' ? toWM(feature) : feature;
if (type === 'Point') {
this.insertLonLat(coordinates.x, coordinates.y, coordinates.m ?? feature.properties);
} else if (type === 'MultiPoint') {
for (const { x, y, m } of coordinates) this.insertLonLat(x, y, m ?? feature.properties);
}
}
}

/**
* Add a lon-lat pair to the cluster
* @param lon - longitude in degrees
* @param lat - latitude in degrees
* @param data - the data associated with the point
*/
insertLonLat(lon: number, lat: number, data: Properties): void {
insertLonLat(lon: number, lat: number, data: D | P): void {
this.insert(fromLonLat(lon, lat), data);
}

Expand All @@ -163,17 +196,17 @@ export class PointCluster {
* @param t - the t coordinate
* @param data - the data associated with the point
*/
insertFaceST(face: Face, s: number, t: number, data: Properties): void {
insertFaceST(face: Face, s: number, t: number, data: D | P): void {
this.insert(fromST(face, s, t), data);
}

/**
* Build the clusters when done adding points
* @param cmp_ - custom compare function
*/
async buildClusters(cmp_?: Comparitor): Promise<void> {
async buildClusters(cmp_?: Comparitor<D | P>): Promise<void> {
const { minzoom, maxzoom } = this;
const cmp: Comparitor = cmp_ ?? ((_a: Properties, _b: Properties) => true);
const cmp: Comparitor<D | P> = cmp_ ?? ((_a: D | P, _b: D | P) => true);
for (let zoom = maxzoom - 1; zoom >= minzoom; zoom--) {
const curIndex = this.indexes.get(zoom);
const queryIndex = this.indexes.get(zoom + 1);
Expand All @@ -192,9 +225,9 @@ export class PointCluster {
*/
async #cluster(
zoom: number,
queryIndex: PointIndex<Cluster>,
currIndex: PointIndex<Cluster>,
cmp: Comparitor,
queryIndex: PointIndex<M, Cluster<D | P>, Cluster<D | P>>,
currIndex: PointIndex<M, Cluster<D | P>, Cluster<D | P>>,
cmp: Comparitor<D | P>,
): Promise<void> {
const radius = this.#getLevelRadius(zoom);
for await (const clusterPoint of queryIndex) {
Expand Down Expand Up @@ -226,7 +259,7 @@ export class PointCluster {
* @param id - the cell id
* @returns - the data within the range of the tile id
*/
async getCellData(id: S2CellId): Promise<undefined | Point<Cluster>[]> {
async getCellData(id: S2CellId): Promise<undefined | Point<Cluster<D | P>>[]> {
const { minzoom, maxzoom, indexes } = this;
const zoom = level(id);
if (zoom < minzoom) return;
Expand Down
55 changes: 41 additions & 14 deletions src/dataStructures/pointIndex.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Vector } from '../dataStore';
import { fromS2Points } from '../geometry/s1/chordAngle';
import { range } from '../geometry';
import { compare, fromS2Point, toCell } from '../dataStructures/uint64';
import { fromS1ChordAngle, getIntersectingCells } from '../geometry/s2/cap';
import { range, toWM } from '../geometry';

import { fromLonLat, fromST } from '../geometry/s2/point';

import type { S1ChordAngle } from '../geometry/s1/chordAngle';
import type { Face, Point3D, Properties } from '..';
import type { Face, FeatureIterator, MValue, Point3D, Properties } from '..';
import type { Uint64, Uint64Cell } from '../dataStructures/uint64';
import type { VectorStore, VectorStoreConstructor } from '../dataStore';

Expand Down Expand Up @@ -49,20 +49,24 @@ export interface PointShape<T extends Properties = Properties> {
* const points = await pointIndex.searchRadius(center, radius);
* ```
*/
export class PointIndex<T extends Properties = Properties> {
#store: VectorStore<PointShape<T>>;
export class PointIndex<
M = Record<string, unknown>,
D extends MValue = Properties,
P extends Properties = Properties,
> {
#store: VectorStore<PointShape<D | P>>;
#unsorted: boolean = false;

/** @param store - the store to index. May be an in memory or disk */
constructor(store: VectorStoreConstructor<PointShape<T>> = Vector) {
constructor(store: VectorStoreConstructor<PointShape<D | P>> = Vector) {
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: VectorStore<PointShape<T>>): void {
setStore(store: VectorStore<PointShape<D | P>>): void {
this.#store = store;
}

Expand All @@ -71,18 +75,37 @@ export class PointIndex<T extends Properties = Properties> {
* @param point - the point to be indexed
* @param data - the data associated with the point
*/
insert(point: Point3D, data: T): void {
insert(point: Point3D, data: D | P): void {
this.#store.push({ cell: fromS2Point(point), point, data });
this.#unsorted = true;
}

/**
* 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: FeatureIterator<M, D, P>): Promise<void> {
for await (const feature of reader) {
if (feature.geometry.type !== 'Point' && feature.geometry.type !== 'MultiPoint') continue;
const {
geometry: { coordinates, type },
} = feature.type === 'S2Feature' ? toWM(feature) : feature;
if (type === 'Point') {
this.insertLonLat(coordinates.x, coordinates.y, coordinates.m ?? feature.properties);
} else if (type === 'MultiPoint') {
for (const { x, y, m } of coordinates) this.insertLonLat(x, y, m ?? feature.properties);
}
}
}

/**
* Add a lon-lat pair to the cluster
* @param lon - longitude in degrees
* @param lat - latitude in degrees
* @param data - the data associated with the point
*/
insertLonLat(lon: number, lat: number, data: T): void {
insertLonLat(lon: number, lat: number, data: D | P): void {
this.insert(fromLonLat(lon, lat), data);
}

Expand All @@ -93,15 +116,15 @@ export class PointIndex<T extends Properties = Properties> {
* @param t - the t coordinate
* @param data - the data associated with the point
*/
insertFaceST(face: Face, s: number, t: number, data: T): void {
insertFaceST(face: Face, s: number, t: number, data: D | P): void {
this.insert(fromST(face, s, t), data);
}

/**
* iterate through the points
* @yields a PointShape<T>
*/
async *[Symbol.asyncIterator](): AsyncGenerator<PointShape<T>> {
async *[Symbol.asyncIterator](): AsyncGenerator<PointShape<D | P>> {
await this.sort();
yield* this.#store;
}
Expand Down Expand Up @@ -146,9 +169,13 @@ export class PointIndex<T extends Properties = Properties> {
* @param maxResults - the maximum number of results to return
* @returns the points in the range
*/
async searchRange(low: Uint64, high: Uint64, maxResults = Infinity): Promise<PointShape<T>[]> {
async searchRange(
low: Uint64,
high: Uint64,
maxResults = Infinity,
): Promise<PointShape<D | P>[]> {
await this.sort();
const res: PointShape<T>[] = [];
const res: PointShape<D | P>[] = [];
let loIdx = await this.lowerBound(low);
const hiID = toCell(high);

Expand All @@ -174,9 +201,9 @@ export class PointIndex<T extends Properties = Properties> {
target: Point3D,
radius: S1ChordAngle,
maxResults = Infinity,
): Promise<PointShape<T>[]> {
): Promise<PointShape<D | P>[]> {
await this.sort();
const res: PointShape<T>[] = [];
const res: PointShape<D | P>[] = [];
if (radius < 0) return res;
const cap = fromS1ChordAngle<undefined>(target, radius, undefined);
for (const cell of getIntersectingCells(cap)) {
Expand Down
Loading

0 comments on commit eaf48a9

Please sign in to comment.