diff --git a/README.md b/README.md
index 10d8f516..7d138172 100755
--- a/README.md
+++ b/README.md
@@ -3,6 +3,9 @@
+
+
+
@@ -122,6 +125,8 @@ To generate the coverage report, use the following command:
```bash
cargo tarpaulin
+# faster
+cargo tarpaulin --color always --skip-clean
# bacon
bacon coverage # or type `l` inside the tool
```
diff --git a/bacon.toml b/bacon.toml
index d89bb579..3df8c173 100644
--- a/bacon.toml
+++ b/bacon.toml
@@ -42,7 +42,7 @@ need_stdout = true
command = [
"sh",
"-c",
- "cargo tarpaulin --out xml > /dev/null 2>&1 && pycobertura show cobertura.xml",
+ "cargo tarpaulin --skip-clean --color always --out xml > /dev/null 2>&1 && pycobertura show cobertura.xml",
]
need_stdout = true
diff --git a/src/converters/toTiles/file.ts b/src/converters/toTiles/file.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/converters/toTiles/index.ts b/src/converters/toTiles/index.ts
index 35a29b11..d8f8cc22 100644
--- a/src/converters/toTiles/index.ts
+++ b/src/converters/toTiles/index.ts
@@ -1,11 +1,12 @@
import { MetadataBuilder } from 's2-tilejson';
import type { Reader } from '../../readers';
+import type { TileWriter } from '../../writers';
import type { VectorFeatures } from '../../geometry';
import type { Attribution, Encoding, LayerMetaData, Scheme } from 's2-tilejson';
-import type { TileWriter, Writer } from '../../writers';
import type { ClusterOptions } from '../../dataStructures/pointCluster';
+import type { TileStoreOptions } from '../../dataStructures/tile';
/** A layer defines the exact mechanics of what data to parse and how the data is stored */
export interface SourceLayer {
@@ -15,16 +16,69 @@ export interface SourceLayer {
metadata: LayerMetaData;
}
+/**
+ * Before tiling the data, you can mutate it here. It can also act as a filter if you return undefined
+ */
+export type OnFeature = (feature: VectorFeatures) => VectorFeatures | undefined;
+
/** Sources are the blueprints of what data to fetch and how to store it */
-export interface Source {
+export interface BaseSource {
+ /** The name of the source */
+ name: string;
/** The reader to parse the data from */
data: Reader;
+}
+
+/** Vector source */
+export interface VectorSource extends BaseSource {
+ /** The layers to construct and organize the data around for this source */
+ layers: SourceLayer[];
/** If options are provided, the assumption is the point data is clustered */
cluster?: ClusterOptions;
+ /** tile buffer on each side so lines and polygons don't get clipped */
+ buffer?: number;
+ /** whether to build the bounding box for each tile feature */
+ buildBBox?: boolean;
/** Before tiling the data, you can mutate it here. It can also act as a filter if you return undefined */
- onFeature?: (feature: VectorFeatures) => VectorFeatures | undefined;
- /** The layers to construct and organize the data around for this source */
- layers: SourceLayer[];
+ onFeature?: OnFeature;
+}
+
+/** Raster source */
+export interface RasterSource extends BaseSource {
+ description?: string;
+}
+
+/** A layerguide is a minified version of a layer used by workers to build tiles */
+export interface LayerGuide {
+ /** Components of how the layer is built and stored */
+ metadata: LayerMetaData;
+ /** Stringified version of the onFeature used by the source so it can be shipped to a worker. */
+ onFeature?: string;
+ /** Guide on how to splice the data into vector tiles */
+ tileGuide: TileStoreOptions;
+}
+
+/** A parsed LayerGuide where onFeature is parsed back into a function */
+export interface ParsedLayerGuide extends Omit {
+ onFeature?: OnFeature;
+}
+
+/** A Source Guide is a minified version of a source used by workers to build tiles */
+export interface SourceGuide {
+ /** The name of the source */
+ [sourceName: string]: {
+ /** Name of the associated layer */
+ [layerName: string]: LayerGuide;
+ };
+}
+
+/** A parsed SourceGuide where onFeature is parsed back into a function */
+export interface ParsedSourceGuide {
+ /** The name of the source */
+ [sourceName: string]: {
+ /** Name of the associated layer */
+ [layerName: string]: ParsedLayerGuide;
+ };
}
/** A user defined guide on building the vector tiles */
@@ -42,9 +96,11 @@ export interface BuildGuide {
scheme?: Scheme;
/** The encoding format. Can be either 'gz', 'br', 'zstd' or 'none' [Default: 'gz'] */
encoding?: Encoding;
- /** The sources that the tile is built from and how the layers are to be stored */
- sources: Source[];
- /** The attribution of the data. Store as { key: presentation name }. [value: href link] */
+ /** The vector sources that the tile is built from and how the layers are to be stored */
+ vectorSources?: VectorSource[];
+ /** The raster sources that will be conjoined into a single rgba pixel index for tile extraction */
+ rasterSources?: RasterSource[];
+ /** The attribution of the data. Store as { key: 'presentation name' }. [value: href link] */
attribution?: Attribution;
/**
* The vector format if applicable helps define how the vector data is stored.
@@ -52,35 +108,40 @@ export interface BuildGuide {
* and 3D geometries.
* - The legacy vector format is the 'open-v1' which only supports 2D geometries and works on
* older map engines like Mapbox-gl-js.
+ * [Default: 'open-v2']
*/
- vectorFormat?: 'open-v2' | 'open-v1';
+ vectorFormat?: 'open-v1' | 'open-v2';
/**
* The data created will be stored in either a folder structure or a pmtiles file
* Folder structure is either '{face}/{zoom}/{x}/{y}.pbf' or '{zoom}/{x}/{y}.pbf'.
* PMTiles store all data in a single data file.
*/
tileWriter: TileWriter;
- /** Explain to the module what kind of writer to use (A buffer or file writer) */
- writer: Writer;
+ /** Set the number of threads to use. [Default: 1] */
+ threads?: number;
}
/**
* Build vector tiles give a guide on what sources to parse data from and how to store it
* @param buildGuide - the user defined guide on building the vector tiles
*/
-export async function toTiles(buildGuide: BuildGuide): Promise {
+export async function toVectorTiles(buildGuide: BuildGuide): Promise {
const { tileWriter } = buildGuide;
- // first setup our metadata builder
- const metaBuilder = new MetadataBuilder();
- updateBuilder(metaBuilder, buildGuide);
+ // TODO: Prepare init messages for workers
// TODO: iterate features and have workers split/store them
+ // TODO: externalSort on features at this point.
+
// TODO: have workers build tiles
- // FINISH:
+ // build metadata based on the guide
+ const metaBuilder = new MetadataBuilder();
+ updateBuilder(metaBuilder, buildGuide);
const metadata = metaBuilder.commit();
+
+ // FINISH
await tileWriter.commit(metadata);
}
@@ -89,7 +150,7 @@ export async function toTiles(buildGuide: BuildGuide): Promise {
* @param buildGuide - the user defined guide on building the vector tiles
*/
function updateBuilder(metaBuilder: MetadataBuilder, buildGuide: BuildGuide): void {
- const { name, description, version, scheme, encoding, attribution, sources } = buildGuide;
+ const { name, description, version, scheme, encoding, attribution, vectorSources } = buildGuide;
metaBuilder.setName(name);
metaBuilder.setExtension('pbf');
@@ -103,14 +164,13 @@ function updateBuilder(metaBuilder: MetadataBuilder, buildGuide: BuildGuide): vo
metaBuilder.addAttribution(displayName, href);
}
}
- for (const { layers } of sources) {
- for (const layer of layers) {
- metaBuilder.addLayer(layer.name, layer.metadata);
- }
+ for (const { layers } of vectorSources ?? []) {
+ for (const layer of layers) metaBuilder.addLayer(layer.name, layer.metadata);
}
}
// TODO:
+// VECTOR:
// - step 1: ship individual features to workers
// - - step 1a: splite the features into tiles, requesting all tiles from range min->max of the layer
// - - step 1b: store all those features into a multimap where the key is the tile-id and the value is the features
@@ -119,3 +179,18 @@ function updateBuilder(metaBuilder: MetadataBuilder, buildGuide: BuildGuide): vo
// - - step 2b: build the tile from the features, gzip, etc. then ship the buffer and metadata to main thread
// - - step 2c: store metadata into metaBuilder and the buffer into the store
// finish
+//
+// CLUSTER:
+// - step 1: for each point, just add.
+// - step 2: cluster.
+// - step 3: request tiles as needed.
+//
+// RASTER:
+// - step 1: for every pixel, store to a point index where the m-value is the rgba value
+// - step 2: build tiles from the point index
+// - - step 2a: start at max zoom - for each pixel in the tile search the range and find average
+// of all the points in that range (remember color is logarithmic so multiply each pixel r-g-b-a by itself first)
+// If too far zoomed in, find the closest pixel within a resonable radius.
+// - - step 2b: after finishing max zoom, use https://github.com/rgba-image/lanczos as I move towards minzoom.
+// - - sidenote: I think the easiest way to build tiles is start at 0 zoom and dive down everytime we find at least one point, then move back up
+// finish
diff --git a/src/converters/toTiles/tileWorkers/buildTile.ts b/src/converters/toTiles/tileWorkers/buildTile.ts
new file mode 100644
index 00000000..c3ae9278
--- /dev/null
+++ b/src/converters/toTiles/tileWorkers/buildTile.ts
@@ -0,0 +1,16 @@
+declare let self: Worker;
+
+// TWO WAYS TO BUILD TILES:
+// * Vector Tiles
+// * Raster Tiles
+
+/**
+ * A worker that sorts a chunk of a file and sends it to an output directory
+ * @param event - the sort chunk message
+ * @param _event
+ */
+self.onmessage = (_event: Bun.MessageEvent): void => {
+ // void sortChunk(event.data as SortChunk).then((outFile): void => {
+ // postMessage(outFile);
+ // });
+};
diff --git a/src/converters/toTiles/vectorWorker/file.ts b/src/converters/toTiles/vectorWorker/file.ts
new file mode 100644
index 00000000..00328a4a
--- /dev/null
+++ b/src/converters/toTiles/vectorWorker/file.ts
@@ -0,0 +1,14 @@
+declare let self: Worker;
+
+import { FileMultiMap } from '../../../dataStore/multimap/file';
+import VectorTileWorker from './vectorTileWorker';
+
+import type { VectorFeature } from '../../../geometry';
+
+/** Convert a vector feature to a collection of tiles and store each tile feature */
+class FileVectorTileWorker extends VectorTileWorker {
+ writer = new FileMultiMap();
+}
+
+const vecWorker = new FileVectorTileWorker();
+self.onmessage = vecWorker.onmessage.bind(vecWorker);
diff --git a/src/converters/toTiles/vectorWorker/index.ts b/src/converters/toTiles/vectorWorker/index.ts
new file mode 100644
index 00000000..da1bdbad
--- /dev/null
+++ b/src/converters/toTiles/vectorWorker/index.ts
@@ -0,0 +1,6 @@
+declare let self: Worker;
+
+import VectorTileWorker from './vectorTileWorker';
+
+const vecWorker = new VectorTileWorker();
+self.onmessage = vecWorker.onmessage.bind(vecWorker);
diff --git a/src/converters/toTiles/vectorWorker/vectorTileWorker.ts b/src/converters/toTiles/vectorWorker/vectorTileWorker.ts
new file mode 100644
index 00000000..10de5828
--- /dev/null
+++ b/src/converters/toTiles/vectorWorker/vectorTileWorker.ts
@@ -0,0 +1,71 @@
+import { MultiMap } from '../../../dataStore';
+
+import type { MultiMapStore } from '../../../dataStore';
+import type { VectorFeature } from '../../../geometry';
+import type { OnFeature, ParsedSourceGuide, SourceGuide } from '../..';
+
+/** Take in options that will be used to create a tiled data correctly */
+export interface InitMessage {
+ /** Message type */
+ type: 'init';
+ /** The sources that will be used to create the tile */
+ sources: SourceGuide;
+}
+
+/** Take in a feature that will be added to the tile */
+export interface FeatureMessage {
+ /** Message type */
+ type: 'feature';
+ /** The feature to add to the tile */
+ feature: VectorFeature;
+}
+
+/** Convert a vector feature to a collection of tiles and store each tile feature */
+export default class VectorTileWorker {
+ sources: ParsedSourceGuide = {};
+ writer: MultiMapStore = new MultiMap();
+ /**
+ * Tile-ize input vector features and store them
+ * @param event - the init message or a feature message
+ */
+ onmessage(event: Bun.MessageEvent): void {
+ this.handleMessage(event.data);
+ self.postMessage({ type: 'ready' });
+ }
+
+ /**
+ * Tile-ize input vector features and store them
+ * @param message - the init message or a feature message
+ */
+ handleMessage(message: InitMessage | FeatureMessage): void {
+ const { type } = message;
+ if (type === 'init') {
+ this.sources = parseSourceGuide(message.sources);
+ } else {
+ // TODO:
+ }
+ }
+}
+
+/**
+ * Convert a source guide to a parsed source guide (where onFeature is parsed back into a function)
+ * @param sourceGuide - the source guide to parse
+ * @returns the parsed source guide
+ */
+function parseSourceGuide(sourceGuide: SourceGuide): ParsedSourceGuide {
+ const res: ParsedSourceGuide = {};
+
+ for (const [sourceName, source] of Object.entries(sourceGuide)) {
+ for (const [layerName, layer] of Object.entries(source)) {
+ res[sourceName][layerName] = {
+ ...layer,
+ onFeature:
+ layer.onFeature !== undefined
+ ? (new Function(layer.onFeature)() as OnFeature)
+ : undefined,
+ };
+ }
+ }
+
+ return res;
+}
diff --git a/src/dataStructures/tile.ts b/src/dataStructures/tile.ts
index f4f73b25..6738e6bc 100644
--- a/src/dataStructures/tile.ts
+++ b/src/dataStructures/tile.ts
@@ -95,7 +95,7 @@ export class Tile {
* @param ti - tile i
* @param tj - tile j
*/
-function _transform(geometry: VectorGeometry, zoom: number, ti: number, tj: number) {
+function _transform(geometry: VectorGeometry, zoom: number, ti: number, tj: number): void {
const { type, coordinates } = geometry;
zoom = 1 << zoom;
@@ -141,7 +141,7 @@ export interface TileStoreOptions {
minzoom?: number;
/** max zoom level to cluster the points on */
maxzoom?: number;
- /** tile buffer on each side in pixels */
+ /** max zoom to index data on construction */
indexMaxzoom?: number;
/** simplification tolerance (higher means simpler) */
tolerance?: number;
diff --git a/src/file.ts b/src/file.ts
index 3502333e..00e969f2 100644
--- a/src/file.ts
+++ b/src/file.ts
@@ -6,3 +6,4 @@ export * from './dataStore/vector/file';
export * from './readers/shapefile/file';
export * from './readers/file';
export * from './writers/file';
+export * from './writers/tile';
diff --git a/src/tools/lanczos/index.ts b/src/tools/lanczos/index.ts
new file mode 100644
index 00000000..0eab19db
--- /dev/null
+++ b/src/tools/lanczos/index.ts
@@ -0,0 +1,62 @@
+import { copy, createImage, resize } from './util';
+
+export * from './util';
+
+/**
+ * Lanczos filter function for downscaling
+ * @param source - the source image
+ * @param dest - the destination image
+ * @param sx - source starting x point [Default: 0]
+ * @param sy - source starting y point [Default: 0]
+ * @param sw - source width to use [Default: source width - sx]
+ * @param sh - source height to use [Default: source height - sy]
+ * @param dx - destination starting x point [Default: 0]
+ * @param dy - destination starting y point [Default: 0]
+ * @param dw - destination width to use [Default: destination width - dx]
+ * @param dh - destination height to use [Default: destination height - dy]
+ * @param use2 - use 2nd lanczos filter instead of 3rd [Default: false]
+ */
+export function lanczos(
+ source: ImageData,
+ dest: ImageData,
+ sx = 0,
+ sy = 0,
+ sw = source.width - sx,
+ sh = source.height - sy,
+ dx = 0,
+ dy = 0,
+ dw = dest.width - dx,
+ dh = dest.height - dy,
+ use2 = false,
+): void {
+ sx |= 0;
+ sy |= 0;
+ sw |= 0;
+ sh |= 0;
+ dx |= 0;
+ dy |= 0;
+ dw |= 0;
+ dh |= 0;
+
+ if (sw <= 0 || sh <= 0 || dw <= 0 || dh <= 0) return;
+
+ if (
+ sx === 0 &&
+ sy === 0 &&
+ sw === source.width &&
+ sh === source.height &&
+ dx === 0 &&
+ dy === 0 &&
+ dw === dest.width &&
+ dh === dest.height
+ ) {
+ resize(source, dest, use2);
+ return;
+ }
+
+ const croppedSource = createImage(sw, sh);
+ const croppedDest = createImage(dw, dh);
+ copy(source, croppedSource, sx, sy);
+ resize(croppedSource, croppedDest, use2);
+ copy(croppedDest, dest, 0, 0, croppedDest.width, croppedDest.height, dx, dy);
+}
diff --git a/src/tools/lanczos/util.ts b/src/tools/lanczos/util.ts
new file mode 100644
index 00000000..bec80fca
--- /dev/null
+++ b/src/tools/lanczos/util.ts
@@ -0,0 +1,335 @@
+const FIXED_FRAC_BITS = 14;
+
+/**
+ * Filter input value given a filter window.
+ * @param x - input
+ * @param a - filter window
+ * @returns - filtered value
+ */
+function filterValue(x: number, a: 2 | 3): number {
+ if (x <= -a || x >= a) return 0;
+ if (x === 0) return 0;
+ // appears to do nothing?
+ // if ( x > -1.19209290e-07 && x < 1.19209290e-07 ) return 1
+ const xPi = x * Math.PI;
+
+ return ((Math.sin(xPi) / xPi) * Math.sin(xPi / a)) / (xPi / a);
+}
+
+/**
+ * Convert value to fixed point
+ * @param value - input
+ * @returns - fixed point
+ */
+function toFixedPoint(value: number): number {
+ return Math.round(value * ((1 << FIXED_FRAC_BITS) - 1));
+}
+
+/**
+ * Create a Lanczos filter
+ * @param srcSize - source image size
+ * @param destSize - destination image size
+ * @param scale - scale factor
+ * @param offset - offset to apply
+ * @param use2 - use 2nd lanczos filter instead of 3rd
+ * @returns - filter
+ */
+export function filters(
+ srcSize: number,
+ destSize: number,
+ scale: number,
+ offset: number,
+ use2: boolean,
+): Int16Array {
+ const a = use2 ? 2 : 3;
+ const scaleInverted = 1 / scale;
+ const scaleClamped = Math.min(1, scale); // For upscale
+
+ // Filter window (averaging interval), scaled to src image
+ const srcWindow = a / scaleClamped;
+
+ const maxFilterElementSize = Math.floor((srcWindow + 1) * 2);
+ const packedFilter = new Int16Array((maxFilterElementSize + 2) * destSize);
+
+ let packedFilterPtr = 0;
+
+ // For each destination pixel calculate source range and built filter values
+ for (let destPixel = 0; destPixel < destSize; destPixel++) {
+ // Scaling should be done relative to central pixel point
+ const sourcePixel = (destPixel + 0.5) * scaleInverted + offset;
+ const sourceFirst = Math.max(0, Math.floor(sourcePixel - srcWindow));
+ const sourceLast = Math.min(srcSize - 1, Math.ceil(sourcePixel + srcWindow));
+
+ const filterElementSize = sourceLast - sourceFirst + 1;
+ const floatFilter = new Float32Array(filterElementSize);
+ const fxpFilter = new Int16Array(filterElementSize);
+
+ let total = 0;
+
+ // Fill filter values for calculated range
+ let index = 0;
+ for (let pixel = sourceFirst; pixel <= sourceLast; pixel++) {
+ const floatValue = filterValue((pixel + 0.5 - sourcePixel) * scaleClamped, a);
+
+ total += floatValue;
+ floatFilter[index] = floatValue;
+
+ index++;
+ }
+
+ // Normalize filter, convert to fixed point and accumulate conversion error
+ let filterTotal = 0;
+
+ for (let index = 0; index < floatFilter.length; index++) {
+ const filterValue = floatFilter[index] / total;
+
+ filterTotal += filterValue;
+ fxpFilter[index] = toFixedPoint(filterValue);
+ }
+
+ // Compensate normalization error, to minimize brightness drift
+ fxpFilter[destSize >> 1] += toFixedPoint(1 - filterTotal);
+
+ //
+ // Now pack filter to useable form
+ //
+ // 1. Trim heading and tailing zero values, and compensate shitf/length
+ // 2. Put all to single array in this format:
+ //
+ // [ pos shift, data length, value1, value2, value3, ... ]
+ //
+ let leftNotEmpty = 0;
+ while (leftNotEmpty < fxpFilter.length && fxpFilter[leftNotEmpty] === 0) {
+ leftNotEmpty++;
+ }
+
+ let rightNotEmpty = fxpFilter.length - 1;
+ while (rightNotEmpty > 0 && fxpFilter[rightNotEmpty] === 0) {
+ rightNotEmpty--;
+ }
+
+ const filterShift = sourceFirst + leftNotEmpty;
+ const filterSize = rightNotEmpty - leftNotEmpty + 1;
+
+ packedFilter[packedFilterPtr++] = filterShift; // shift
+ packedFilter[packedFilterPtr++] = filterSize; // size
+
+ packedFilter.set(fxpFilter.subarray(leftNotEmpty, rightNotEmpty + 1), packedFilterPtr);
+ packedFilterPtr += filterSize;
+ }
+
+ return packedFilter;
+}
+
+/**
+ * Convolve an image with a filter
+ * @param source - the source image
+ * @param dest - the destination image
+ * @param sw - source width
+ * @param sh - source height
+ * @param dw - destination width
+ * @param filters - image filter
+ */
+export function convolve(
+ source: Uint8ClampedArray,
+ dest: Uint8ClampedArray,
+ sw: number,
+ sh: number,
+ dw: number,
+ filters: Int16Array,
+): void {
+ let srcOffset = 0;
+ let destOffset = 0;
+
+ // For each row
+ for (let sourceY = 0; sourceY < sh; sourceY++) {
+ let filterPtr = 0;
+
+ // Apply precomputed filters to each destination row point
+ for (let destX = 0; destX < dw; destX++) {
+ // Get the filter that determines the current output pixel.
+ const filterShift = filters[filterPtr++];
+
+ let srcPtr = (srcOffset + filterShift * 4) | 0;
+
+ let r = 0;
+ let g = 0;
+ let b = 0;
+ let a = 0;
+
+ // Apply the filter to the row to get the destination pixel r, g, b, a
+ for (let filterSize = filters[filterPtr++]; filterSize > 0; filterSize--) {
+ const filterValue = filters[filterPtr++];
+
+ r = (r + filterValue * source[srcPtr]) | 0;
+ g = (g + filterValue * source[srcPtr + 1]) | 0;
+ b = (b + filterValue * source[srcPtr + 2]) | 0;
+ a = (a + filterValue * source[srcPtr + 3]) | 0;
+
+ srcPtr = (srcPtr + 4) | 0;
+ }
+
+ // Bring this value back in range. All of the filter scaling factors
+ // are in fixed point with FIXED_FRAC_BITS bits of fractional part.
+ //
+ // (!) Add 1/2 of value before clamping to get proper rounding. In other
+ // case brightness loss will be noticeable if you resize image with white
+ // border and place it on white background.
+ //
+ dest[destOffset] = (r + (1 << 13)) >> FIXED_FRAC_BITS;
+ dest[destOffset + 1] = (g + (1 << 13)) >> FIXED_FRAC_BITS;
+ dest[destOffset + 2] = (b + (1 << 13)) >> FIXED_FRAC_BITS;
+ dest[destOffset + 3] = (a + (1 << 13)) >> FIXED_FRAC_BITS;
+
+ destOffset = (destOffset + sh * 4) | 0;
+ }
+
+ destOffset = ((sourceY + 1) * 4) | 0;
+ srcOffset = ((sourceY + 1) * sw * 4) | 0;
+ }
+}
+
+/**
+ * Copy the contents of the source image to the destination image
+ * @param source - the source image
+ * @param dest - the destination image
+ * @param sx - source starting x point [Default: 0]
+ * @param sy - source starting y point [Default: 0]
+ * @param sw - source width to use [Default: source width - sx]
+ * @param sh - source height to use [Default: source height - sy]
+ * @param dx - destination starting x point [Default: 0]
+ * @param dy - destination starting y point [Default: 0]
+ */
+export function copy(
+ source: ImageData,
+ dest: ImageData,
+ sx = 0,
+ sy = 0,
+ sw = source.width - sx,
+ sh = source.height - sy,
+ dx = 0,
+ dy = 0,
+): void {
+ sx = sx | 0;
+ sy = sy | 0;
+ sw = sw | 0;
+ sh = sh | 0;
+ dx = dx | 0;
+ dy = dy | 0;
+
+ if (sw <= 0 || sh <= 0) return;
+
+ const sourceData = new Uint32Array(source.data.buffer);
+ const destData = new Uint32Array(dest.data.buffer);
+
+ for (let y = 0; y < sh; y++) {
+ const sourceY = sy + y;
+ if (sourceY < 0 || sourceY >= source.height) continue;
+
+ const destY = dy + y;
+ if (destY < 0 || destY >= dest.height) continue;
+
+ for (let x = 0; x < sw; x++) {
+ const sourceX = sx + x;
+ if (sourceX < 0 || sourceX >= source.width) continue;
+
+ const destX = dx + x;
+ if (destX < 0 || destX >= dest.width) continue;
+
+ const sourceIndex = sourceY * source.width + sourceX;
+ const destIndex = destY * dest.width + destX;
+
+ destData[destIndex] = sourceData[sourceIndex];
+ }
+ }
+}
+
+/**
+ * Create an image given the size, fill color and number of channels
+ * @param width - the image width
+ * @param height - the image height
+ * @param data - the image data [Default: creates a new array]
+ * @param fill - the fill color [Default: [0, 0, 0, 0]]
+ * @param channels - the number of channels [Default: 4]
+ * @returns - the created image
+ */
+export function createImage(
+ width: number,
+ height: number,
+ data?: Uint8ClampedArray,
+ fill: number[] | Uint8ClampedArray = [0, 0, 0, 0],
+ channels = 4,
+): ImageData {
+ width = Math.floor(width);
+ height = Math.floor(height);
+
+ if (width < 1 || height < 1) {
+ throw TypeError('Index or size is negative or greater than the allowed amount');
+ }
+
+ const length = width * height * channels;
+
+ if (data === undefined) {
+ data = new Uint8ClampedArray(length);
+ }
+
+ if (data.length !== length) {
+ throw TypeError('Index or size is negative or greater than the allowed amount');
+ }
+
+ for (let y = 0; y < height; y++) {
+ for (let x = 0; x < width; x++) {
+ const index = (y * width + x) * channels;
+ for (let c = 0; c < channels; c++) {
+ data[index + c] = fill[c];
+ }
+ }
+ }
+
+ return { data, width, height };
+}
+
+/**
+ * Lanczos resize function
+ * @param source - the source image
+ * @param dest - the destination image
+ * @param use2 - use 2nd lanczos filter instead of 3rd
+ */
+export function resize(source: ImageData, dest: ImageData, use2 = false): void {
+ const xRatio = dest.width / source.width;
+ const yRatio = dest.height / source.height;
+
+ const filtersX = filters(source.width, dest.width, xRatio, 0, use2);
+ const filtersY = filters(source.height, dest.height, yRatio, 0, use2);
+
+ const tmp = new Uint8ClampedArray(dest.width * source.height * 4);
+
+ convolve(source.data, tmp, source.width, source.height, dest.width, filtersX);
+ convolve(tmp, dest.data, source.height, dest.width, dest.height, filtersY);
+}
+
+/*
+ Adapted to typescript from pica: https://github.com/nodeca/pica
+
+ (The MIT License)
+
+ Copyright (C) 2014-2017 by Vitaly Puzrin
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+*/
diff --git a/src/writers/tile.ts b/src/writers/tile.ts
index 1aa6b36a..c7b245b7 100644
--- a/src/writers/tile.ts
+++ b/src/writers/tile.ts
@@ -5,7 +5,7 @@ import type { Metadata } from 's2-tilejson';
import type { TileWriter } from '.';
/** This is a filesystem Tile writer that organizes data via folders. */
-export class LocalTileWriter implements TileWriter {
+export class FileTileWriter implements TileWriter {
/**
* @param path - the location to write the data
* @param fileType - the file ending to write
diff --git a/tests/tools/lanczos/fixtures/pattern-border.png b/tests/tools/lanczos/fixtures/pattern-border.png
new file mode 100644
index 00000000..88bdfcf3
Binary files /dev/null and b/tests/tools/lanczos/fixtures/pattern-border.png differ
diff --git a/tests/tools/lanczos/fixtures/pattern-double-region.png b/tests/tools/lanczos/fixtures/pattern-double-region.png
new file mode 100644
index 00000000..104f8e32
Binary files /dev/null and b/tests/tools/lanczos/fixtures/pattern-double-region.png differ
diff --git a/tests/tools/lanczos/fixtures/pattern-double.png b/tests/tools/lanczos/fixtures/pattern-double.png
new file mode 100644
index 00000000..e333eaeb
Binary files /dev/null and b/tests/tools/lanczos/fixtures/pattern-double.png differ
diff --git a/tests/tools/lanczos/fixtures/pattern-half-2.png b/tests/tools/lanczos/fixtures/pattern-half-2.png
new file mode 100644
index 00000000..85fa0899
Binary files /dev/null and b/tests/tools/lanczos/fixtures/pattern-half-2.png differ
diff --git a/tests/tools/lanczos/fixtures/pattern-half-region-2.png b/tests/tools/lanczos/fixtures/pattern-half-region-2.png
new file mode 100644
index 00000000..5497771f
Binary files /dev/null and b/tests/tools/lanczos/fixtures/pattern-half-region-2.png differ
diff --git a/tests/tools/lanczos/fixtures/pattern-half-region.png b/tests/tools/lanczos/fixtures/pattern-half-region.png
new file mode 100644
index 00000000..e4d6f5f7
Binary files /dev/null and b/tests/tools/lanczos/fixtures/pattern-half-region.png differ
diff --git a/tests/tools/lanczos/fixtures/pattern-half.png b/tests/tools/lanczos/fixtures/pattern-half.png
new file mode 100644
index 00000000..80e757a7
Binary files /dev/null and b/tests/tools/lanczos/fixtures/pattern-half.png differ
diff --git a/tests/tools/lanczos/fixtures/pattern-out-of-bounds.png b/tests/tools/lanczos/fixtures/pattern-out-of-bounds.png
new file mode 100644
index 00000000..68b1922f
Binary files /dev/null and b/tests/tools/lanczos/fixtures/pattern-out-of-bounds.png differ
diff --git a/tests/tools/lanczos/fixtures/pattern.png b/tests/tools/lanczos/fixtures/pattern.png
new file mode 100644
index 00000000..f843e263
Binary files /dev/null and b/tests/tools/lanczos/fixtures/pattern.png differ
diff --git a/tests/tools/lanczos/index.test.ts b/tests/tools/lanczos/index.test.ts
new file mode 100644
index 00000000..2b7d48f6
--- /dev/null
+++ b/tests/tools/lanczos/index.test.ts
@@ -0,0 +1,184 @@
+import { createImage, lanczos } from '../../../src/tools/lanczos';
+import { expect, test } from 'bun:test';
+
+import sharp from 'sharp';
+
+const patternPng = await sharp(`${__dirname}/fixtures/pattern.png`)
+ .raw()
+ .toBuffer({ resolveWithObject: true });
+const patternBorderPng = await sharp(`${__dirname}/fixtures/pattern-border.png`)
+ .raw()
+ .toBuffer({ resolveWithObject: true });
+const expectPatternHalfPng = await sharp(`${__dirname}/fixtures/pattern-half.png`)
+ .raw()
+ .toBuffer({ resolveWithObject: true });
+const expectPatternHalf2Png = await sharp(`${__dirname}/fixtures/pattern-half-2.png`)
+ .raw()
+ .toBuffer({ resolveWithObject: true });
+const expectPatternDoublePng = await sharp(`${__dirname}/fixtures/pattern-double.png`)
+ .raw()
+ .toBuffer({ resolveWithObject: true });
+const expectPatternHalfRegionPng = await sharp(`${__dirname}/fixtures/pattern-half-region.png`)
+ .raw()
+ .toBuffer({ resolveWithObject: true });
+const expectPatternHalfRegion2Png = await sharp(`${__dirname}/fixtures/pattern-half-region-2.png`)
+ .raw()
+ .toBuffer({ resolveWithObject: true });
+const expectPatternDoubleRegionPng = await sharp(`${__dirname}/fixtures/pattern-double-region.png`)
+ .raw()
+ .toBuffer({ resolveWithObject: true });
+const expectPatternOutOfBoundsPng = await sharp(`${__dirname}/fixtures/pattern-out-of-bounds.png`)
+ .raw()
+ .toBuffer({ resolveWithObject: true });
+
+const pattern = {
+ data: new Uint8ClampedArray(patternPng.data.buffer),
+ width: patternPng.info.width,
+ height: patternPng.info.height,
+} as ImageData;
+const patternBorder = {
+ data: new Uint8ClampedArray(patternBorderPng.data.buffer),
+ width: patternBorderPng.info.width,
+ height: patternBorderPng.info.height,
+} as ImageData;
+const expectPatternHalf = {
+ data: new Uint8ClampedArray(expectPatternHalfPng.data.buffer),
+ width: expectPatternHalfPng.info.width,
+ height: expectPatternHalfPng.info.height,
+} as ImageData;
+const expectPatternHalf2 = {
+ data: new Uint8ClampedArray(expectPatternHalf2Png.data.buffer),
+ width: expectPatternHalf2Png.info.width,
+ height: expectPatternHalf2Png.info.height,
+} as ImageData;
+const expectPatternDouble = {
+ data: new Uint8ClampedArray(expectPatternDoublePng.data.buffer),
+ width: expectPatternDoublePng.info.width,
+ height: expectPatternDoublePng.info.height,
+} as ImageData;
+const expectPatternHalfRegion = {
+ data: new Uint8ClampedArray(expectPatternHalfRegionPng.data.buffer),
+ width: expectPatternHalfRegionPng.info.width,
+ height: expectPatternHalfRegionPng.info.height,
+} as ImageData;
+const expectPatternHalfRegion2 = {
+ data: new Uint8ClampedArray(expectPatternHalfRegion2Png.data.buffer),
+ width: expectPatternHalfRegion2Png.info.width,
+ height: expectPatternHalfRegion2Png.info.height,
+} as ImageData;
+const expectPatternDoubleRegion = {
+ data: new Uint8ClampedArray(expectPatternDoubleRegionPng.data.buffer),
+ width: expectPatternDoubleRegionPng.info.width,
+ height: expectPatternDoubleRegionPng.info.height,
+} as ImageData;
+const expectPatternOutOfBounds = {
+ data: new Uint8ClampedArray(expectPatternOutOfBoundsPng.data.buffer),
+ width: expectPatternOutOfBoundsPng.info.width,
+ height: expectPatternOutOfBoundsPng.info.height,
+} as ImageData;
+
+test('lanczos - resizes down', () => {
+ const patternHalf = createImage(4, 4);
+
+ lanczos(pattern, patternHalf);
+
+ expect(patternHalf).toEqual(expectPatternHalf);
+});
+
+test('resizes down with lanczos2', () => {
+ const patternHalf = createImage(4, 4);
+
+ lanczos(
+ pattern,
+ patternHalf,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ true,
+ );
+
+ expect(patternHalf).toEqual(expectPatternHalf2);
+});
+
+test('resizes up', () => {
+ const patternDouble = createImage(16, 16);
+
+ lanczos(pattern, patternDouble);
+
+ expect(patternDouble).toEqual(expectPatternDouble);
+});
+
+test('resizes region down', () => {
+ const patternHalfRegion = createImage(6, 6);
+
+ lanczos(patternBorder, patternHalfRegion, 2, 2, 8, 8, 1, 1, 4, 4);
+
+ expect(patternHalfRegion).toEqual(expectPatternHalfRegion);
+});
+
+test('resizes region down with lanczos2', () => {
+ const patternHalfRegion = createImage(6, 6);
+
+ lanczos(patternBorder, patternHalfRegion, 2, 2, 8, 8, 1, 1, 4, 4, true);
+
+ expect(patternHalfRegion).toEqual(expectPatternHalfRegion2);
+});
+
+test('resizes region up', () => {
+ const patternDoubleRegion = createImage(18, 18);
+
+ lanczos(patternBorder, patternDoubleRegion, 2, 2, 8, 8, 1, 1, 16, 16);
+
+ expect(patternDoubleRegion).toEqual(expectPatternDoubleRegion);
+});
+
+test('early return when any dimension is 0', () => {
+ const empty = createImage(8, 8);
+ const destSw = createImage(8, 8);
+ const destSh = createImage(8, 8);
+ const destDw = createImage(8, 8);
+ const destDh = createImage(8, 8);
+
+ lanczos(destSw, pattern, 0, 0, 0, 8);
+ lanczos(destSh, pattern, 0, 0, 8, 0);
+ lanczos(destDw, pattern, 0, 0, 8, 8, 0, 0, 0, 8);
+ lanczos(destDh, pattern, 0, 0, 8, 8, 0, 0, 8, 0);
+
+ expect(destSw).toEqual(empty);
+ expect(destSh).toEqual(empty);
+ expect(destDw).toEqual(empty);
+ expect(destDh).toEqual(empty);
+
+ lanczos(destSw, pattern, 0, 0, 0, 8, undefined, undefined, undefined, undefined, true);
+ lanczos(destSh, pattern, 0, 0, 8, 0, undefined, undefined, undefined, undefined, true);
+ lanczos(destDw, pattern, 0, 0, 8, 8, 0, 0, 0, 8, true);
+ lanczos(destDh, pattern, 0, 0, 8, 8, 0, 0, 8, 0, true);
+
+ expect(destSw).toEqual(empty);
+ expect(destSh).toEqual(empty);
+ expect(destDw).toEqual(empty);
+ expect(destDh).toEqual(empty);
+});
+
+test('does not sample outside bounds', () => {
+ const patternOutOfBounds = createImage(8, 8);
+
+ lanczos(pattern, patternOutOfBounds, 0, 0, 16, 16, 0, 0, 32, 32);
+
+ expect(patternOutOfBounds).toEqual(expectPatternOutOfBounds);
+});
+
+test('createImage errors', () => {
+ expect(() => {
+ createImage(0, 0);
+ }).toThrow('Index or size is negative or greater than the allowed amount');
+
+ expect(() => {
+ createImage(10, 10, new Uint8ClampedArray(0));
+ }).toThrow('Index or size is negative or greater than the allowed amount');
+});
diff --git a/tests/util/polyfills/dataview.test.ts b/tests/util/polyfills/dataview.test.ts
index 317be269..ebefaf75 100644
--- a/tests/util/polyfills/dataview.test.ts
+++ b/tests/util/polyfills/dataview.test.ts
@@ -11,9 +11,9 @@ test('getFloat16', () => {
dv.setFloat16(4, 0.1);
dv.setFloat16(6, -0.01, true);
dv.setFloat16(8, Infinity);
- expect(dv.getFloat16(0)).toEqual(0x7c00);
- expect(dv.getFloat16(2, true)).toEqual(0);
- expect(dv.getFloat16(4)).toEqual(0.0999755859375);
- expect(dv.getFloat16(6, true)).toEqual(-0.0099945068359375);
- expect(dv.getFloat16(8)).toEqual(Infinity);
+ expect(dv.getFloat16(0)).toBeCloseTo(0x7c00);
+ expect(dv.getFloat16(2, true)).toBeCloseTo(0);
+ expect(dv.getFloat16(4)).toBeCloseTo(0.0999755859375);
+ expect(dv.getFloat16(6, true)).toBeCloseTo(-0.0099945068359375);
+ expect(dv.getFloat16(8)).toBeCloseTo(Infinity);
});