diff --git a/benchmark.js b/benchmark.js index 0033cb8..46bd4bc 100644 --- a/benchmark.js +++ b/benchmark.js @@ -26,21 +26,21 @@ return buffer; } - const awsManager = new LocalDemManager( - "https://elevation-tiles-prod.s3.amazonaws.com/terrarium/{z}/{x}/{y}.png", - 100, - "terrarium", - 12, - 10_000, - ); + const awsManager = new LocalDemManager({ + demUrlPattern: "https://elevation-tiles-prod.s3.amazonaws.com/terrarium/{z}/{x}/{y}.png", + cacheSize: 100, + encoding: "terrarium", + maxzoom: 12, + timeoutMs: 10_000, + }); - const demoManager = new LocalDemManager( - "https://demotiles.maplibre.org/terrain-tiles/{z}/{x}/{y}.png", - 100, - "mapbox", - 11, - 10_000, - ); + const demoManager = new LocalDemManager({ + demUrlPattern: "https://demotiles.maplibre.org/terrain-tiles/{z}/{x}/{y}.png", + cacheSize: 100, + encoding: "mapbox", + maxzoom: 11, + timeoutMs: 10_000, + }); let noMoreFetch = false; if (typeof document === "undefined") { demoManager.decodeImage = awsManager.decodeImage = (blob, encoding) => diff --git a/src/dem-source.ts b/src/dem-source.ts index dfe9d73..9eba957 100644 --- a/src/dem-source.ts +++ b/src/dem-source.ts @@ -1,8 +1,12 @@ -import type { DemManager } from "./dem-manager"; -import { LocalDemManager } from "./dem-manager"; +import { LocalDemManager } from "./local-dem-manager"; import { decodeOptions, encodeOptions, getOptionsForZoom } from "./utils"; import RemoteDemManager from "./remote-dem-manager"; -import type { DemTile, GlobalContourTileOptions, Timing } from "./types"; +import type { + DemManager, + DemTile, + GlobalContourTileOptions, + Timing, +} from "./types"; import type WorkerDispatch from "./worker-dispatch"; import Actor from "./actor"; import { Timer } from "./performance"; @@ -129,14 +133,14 @@ export class DemSource { this.sharedDemProtocolUrl = `${this.sharedDemProtocolId}://{z}/{x}/{y}`; this.contourProtocolUrlBase = `${this.contourProtocolId}://{z}/{x}/{y}`; const ManagerClass = worker ? RemoteDemManager : LocalDemManager; - this.manager = new ManagerClass( - url, + this.manager = new ManagerClass({ + demUrlPattern: url, cacheSize, encoding, maxzoom, timeoutMs, actor, - ); + }); } /** Registers a callback to be invoked with a performance report after each tile is requested. */ diff --git a/src/e2e.test.ts b/src/e2e.test.ts index 5ad3fa5..8829a0d 100644 --- a/src/e2e.test.ts +++ b/src/e2e.test.ts @@ -6,6 +6,7 @@ import { MainThreadDispatch } from "./remote-dem-manager"; import type { DemTile, Timing } from "./types"; import { VectorTile } from "@mapbox/vector-tile"; import Pbf from "pbf"; +import { LocalDemManager } from "./local-dem-manager"; beforeEach(() => { jest.useFakeTimers({ now: 0, doNotFake: ["performance"] }); @@ -282,3 +283,28 @@ test("decode image from worker", async () => { data: expectedElevations, }); }); + +test("fake decode image and fetch tile", async () => { + const getTileSpy = jest.fn().mockReturnValue(Promise.resolve({})); + const demManager = new LocalDemManager({ + demUrlPattern: "https://example/{z}/{x}/{y}.png", + cacheSize: 100, + encoding: "terrarium", + maxzoom: 11, + timeoutMs: 10000, + decodeImage: async () => ({ + width: 4, + height: 4, + data: expectedElevations, + }), + getTile: getTileSpy, + }); + const demTile = await demManager.fetchAndParseTile( + 1, + 2, + 3, + new AbortController(), + ); + expect(demTile.data).toEqual(expectedElevations); + expect(getTileSpy.mock.calls[0][0]).toBe("https://example/1/2/3.png"); +}); diff --git a/src/index.ts b/src/index.ts index 69263ac..f8160fa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import generateIsolines from "./isolines"; import { DemSource } from "./dem-source"; import { decodeParsedImage } from "./decode-image"; -import { LocalDemManager } from "./dem-manager"; +import { LocalDemManager } from "./local-dem-manager"; import CONFIG from "./config"; import { HeightTile } from "./height-tile"; diff --git a/src/dem-manager.ts b/src/local-dem-manager.ts similarity index 76% rename from src/dem-manager.ts rename to src/local-dem-manager.ts index 8db2177..4d2a606 100644 --- a/src/dem-manager.ts +++ b/src/local-dem-manager.ts @@ -1,47 +1,39 @@ import AsyncCache from "./cache"; -import decodeImage from "./decode-image"; +import defaultDecodeImage from "./decode-image"; import { HeightTile } from "./height-tile"; import generateIsolines from "./isolines"; import { encodeIndividualOptions, isAborted, withTimeout } from "./utils"; import type { ContourTile, + DecodeImageFunction, + DemManager, + DemManagerInitizlizationParameters, DemTile, Encoding, FetchResponse, + GetTileFunction, IndividualContourTileOptions, } from "./types"; import encodeVectorTile, { GeomType } from "./vtpbf"; import { Timer } from "./performance"; -/** - * Holds cached tile state, and exposes `fetchContourTile` which fetches the necessary - * tiles and returns an encoded contour vector tiles. - */ -export interface DemManager { - loaded: Promise; - fetchTile( - z: number, - x: number, - y: number, - abortController: AbortController, - timer?: Timer, - ): Promise; - fetchAndParseTile( - z: number, - x: number, - y: number, - abortController: AbortController, - timer?: Timer, - ): Promise; - fetchContourTile( - z: number, - x: number, - y: number, - options: IndividualContourTileOptions, - abortController: AbortController, - timer?: Timer, - ): Promise; -} +const defaultGetTile: GetTileFunction = async ( + url: string, + abortController: AbortController, +) => { + const options: RequestInit = { + signal: abortController.signal, + }; + const response = await fetch(url, options); + if (!response.ok) { + throw new Error(`Bad response: ${response.status} for ${url}`); + } + return { + data: await response.blob(), + expires: response.headers.get("expires") || undefined, + cacheControl: response.headers.get("cache-control") || undefined, + }; +}; /** * Caches, decodes, and processes raster tiles in the current thread. @@ -55,26 +47,19 @@ export class LocalDemManager implements DemManager { maxzoom: number; timeoutMs: number; loaded = Promise.resolve(); - decodeImage: ( - blob: Blob, - encoding: Encoding, - abortController: AbortController, - ) => Promise = decodeImage; + decodeImage: DecodeImageFunction; + getTile: GetTileFunction; - constructor( - demUrlPattern: string, - cacheSize: number, - encoding: Encoding, - maxzoom: number, - timeoutMs: number, - ) { - this.tileCache = new AsyncCache(cacheSize); - this.parsedCache = new AsyncCache(cacheSize); - this.contourCache = new AsyncCache(cacheSize); - this.timeoutMs = timeoutMs; - this.demUrlPattern = demUrlPattern; - this.encoding = encoding; - this.maxzoom = maxzoom; + constructor(options: DemManagerInitizlizationParameters) { + this.tileCache = new AsyncCache(options.cacheSize); + this.parsedCache = new AsyncCache(options.cacheSize); + this.contourCache = new AsyncCache(options.cacheSize); + this.timeoutMs = options.timeoutMs; + this.demUrlPattern = options.demUrlPattern; + this.encoding = options.encoding; + this.maxzoom = options.maxzoom; + this.decodeImage = options.decodeImage || defaultDecodeImage; + this.getTile = options.getTile || defaultGetTile; } fetchTile( @@ -92,24 +77,11 @@ export class LocalDemManager implements DemManager { return this.tileCache.get( url, (_, childAbortController) => { - const options: RequestInit = { - signal: childAbortController.signal, - }; timer?.fetchTile(url); const mark = timer?.marker("fetch"); return withTimeout( this.timeoutMs, - fetch(url, options).then(async (response) => { - mark?.(); - if (!response.ok) { - throw new Error(`Bad response: ${response.status} for ${url}`); - } - return { - data: await response.blob(), - expires: response.headers.get("expires") || undefined, - cacheControl: response.headers.get("cache-control") || undefined, - }; - }), + this.getTile(url, childAbortController).finally(() => mark?.()), childAbortController, ); }, diff --git a/src/remote-dem-manager.ts b/src/remote-dem-manager.ts index 4811758..40ed00b 100644 --- a/src/remote-dem-manager.ts +++ b/src/remote-dem-manager.ts @@ -2,10 +2,11 @@ import Actor from "./actor"; import CONFIG from "./config"; import type WorkerDispatch from "./worker-dispatch"; import decodeImage from "./decode-image"; -import type { DemManager } from "./dem-manager"; import { Timer } from "./performance"; import type { ContourTile, + DemManager, + DemManagerInitizlizationParameters, DemTile, Encoding, FetchResponse, @@ -41,28 +42,17 @@ export default class RemoteDemManager implements DemManager { actor: Actor; loaded: Promise; - constructor( - demUrlPattern: string, - cacheSize: number, - encoding: Encoding, - maxzoom: number, - timeoutMs: number, - actor?: Actor, - ) { + constructor(options: DemManagerInitizlizationParameters) { const managerId = (this.managerId = ++id); - this.actor = actor || defaultActor(); + this.actor = options.actor || defaultActor(); this.loaded = this.actor.send( "init", [], new AbortController(), undefined, { - cacheSize, - demUrlPattern, - encoding, - maxzoom, + ...options, managerId, - timeoutMs, }, ); } diff --git a/src/types.ts b/src/types.ts index a59cc7a..5db7582 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,7 @@ +import type Actor from "./actor"; +import type { Timer } from "./performance"; +import type WorkerDispatch from "./worker-dispatch"; + /** Scheme used to map pixel rgb values elevations. */ export type Encoding = "terrarium" | "mapbox"; export interface IsTransferrable { @@ -77,15 +81,6 @@ export interface Image { data: Uint8Array; } -export interface InitMessage { - managerId: number; - demUrlPattern: string; - cacheSize: number; - encoding: Encoding; - maxzoom: number; - timeoutMs: number; -} - export type TimingCategory = "main" | "worker" | "fetch" | "decode" | "isoline"; /** Performance profile for a tile request */ @@ -114,3 +109,63 @@ export interface Timing { /** If the tile failed with an error */ error?: boolean; } + +/** + * Holds cached tile state, and exposes `fetchContourTile` which fetches the necessary + * tiles and returns an encoded contour vector tiles. + */ +export interface DemManager { + loaded: Promise; + fetchTile( + z: number, + x: number, + y: number, + abortController: AbortController, + timer?: Timer, + ): Promise; + fetchAndParseTile( + z: number, + x: number, + y: number, + abortController: AbortController, + timer?: Timer, + ): Promise; + fetchContourTile( + z: number, + x: number, + y: number, + options: IndividualContourTileOptions, + abortController: AbortController, + timer?: Timer, + ): Promise; +} + +export type GetTileFunction = ( + url: string, + abortController: AbortController, +) => Promise; + +export type DecodeImageFunction = ( + blob: Blob, + encoding: Encoding, + abortController: AbortController, +) => Promise; + +export type DemManagerRequiredInitializationParameters = { + demUrlPattern: string; + cacheSize: number; + encoding: Encoding; + maxzoom: number; + timeoutMs: number; +}; + +export type DemManagerInitizlizationParameters = + DemManagerRequiredInitializationParameters & { + decodeImage?: DecodeImageFunction; + getTile?: GetTileFunction; + actor?: Actor; + }; + +export type InitMessage = DemManagerRequiredInitializationParameters & { + managerId: number; +}; diff --git a/src/worker-dispatch.ts b/src/worker-dispatch.ts index 4021f22..80b960a 100644 --- a/src/worker-dispatch.ts +++ b/src/worker-dispatch.ts @@ -1,4 +1,4 @@ -import { LocalDemManager } from "./dem-manager"; +import { LocalDemManager } from "./local-dem-manager"; import { Timer } from "./performance"; import type { ContourTile, @@ -20,13 +20,7 @@ export default class WorkerDispatch { managers: { [id: number]: LocalDemManager } = {}; init = (message: InitMessage, _: AbortController): Promise => { - this.managers[message.managerId] = new LocalDemManager( - message.demUrlPattern, - message.cacheSize, - message.encoding, - message.maxzoom, - message.timeoutMs, - ); + this.managers[message.managerId] = new LocalDemManager(message); return Promise.resolve(); };