224 lines
6.4 KiB
TypeScript
224 lines
6.4 KiB
TypeScript
import { appDataDir, join } from "@tauri-apps/api/path";
|
|
import { BaseDirectory, exists, mkdir, open, remove, SeekMode } from "@tauri-apps/plugin-fs";
|
|
import { download } from "@tauri-apps/plugin-upload";
|
|
import { PMTiles, TileType, type Source } from "pmtiles";
|
|
|
|
export async function downloadPMTiles(url: string, name: string): Promise<void> {
|
|
// if(!window.__TAURI__) {
|
|
// throw new Error("Tauri environment is not available.");
|
|
// }
|
|
|
|
const filename = name + ".pmtiles";
|
|
const baseDir = BaseDirectory.AppData;
|
|
const appDataDirPath = await appDataDir();
|
|
|
|
if(!await exists(appDataDirPath)) {
|
|
await mkdir(appDataDirPath, { recursive: true });
|
|
}
|
|
|
|
if(await exists(filename, { baseDir })) {
|
|
console.log(`File ${filename} already exists, deleting it.`);
|
|
await remove(filename, { baseDir });
|
|
}
|
|
|
|
console.log(`Downloading PMTiles from ${url} to ${filename}`);
|
|
const res = await fetch(url);
|
|
|
|
if (!res.ok) {
|
|
throw new Error(`Failed to download PMTiles: ${res.statusText}`);
|
|
}
|
|
|
|
const path = await join(appDataDirPath, filename);
|
|
|
|
await download(url, path, ({ progress, total }) => {
|
|
console.log(`Download progress: ${Math.round((progress / total) * 100)}% (${progress}\tof ${total} bytes)`);
|
|
});
|
|
|
|
console.log(`Download completed: ${path}`);
|
|
}
|
|
|
|
export async function hasPMTiles(name: string): Promise<boolean> {
|
|
const filename = name + ".pmtiles";
|
|
const baseDir = BaseDirectory.AppData;
|
|
const appDataDirPath = await appDataDir();
|
|
|
|
if(!await exists(appDataDirPath)) {
|
|
return false;
|
|
}
|
|
|
|
const filePath = await join(appDataDirPath, filename);
|
|
return await exists(filePath, { baseDir });
|
|
}
|
|
|
|
export async function getPMTiles(name: string) {
|
|
const filename = name + ".pmtiles";
|
|
const baseDir = BaseDirectory.AppData;
|
|
const appDataDirPath = await appDataDir();
|
|
|
|
if(!await exists(appDataDirPath)) {
|
|
throw new Error("App data directory does not exist.");
|
|
}
|
|
|
|
const filePath = await join(appDataDirPath, filename);
|
|
|
|
if(!await exists(filePath, { baseDir })) {
|
|
throw new Error(`PMTiles file not found: ${filePath}`);
|
|
}
|
|
|
|
return `asset:/${filename}`;
|
|
// return convertFileSrc(filePath);
|
|
}
|
|
|
|
async function readBytes(name: string, offset: number, length: number): Promise<Uint8Array> {
|
|
const file = await open(name + ".pmtiles", { read: true, baseDir: BaseDirectory.AppData });
|
|
const buffer = new Uint8Array(length);
|
|
await file.seek(offset, SeekMode.Start);
|
|
await file.read(buffer);
|
|
await file.close();
|
|
return buffer;
|
|
}
|
|
|
|
export class FSSource implements Source {
|
|
name: string;
|
|
|
|
constructor(name: string) {
|
|
this.name = name;
|
|
}
|
|
|
|
async getBytes(offset: number, length: number, _signal?: AbortSignal, _etag?: string) { // TODO: abort signal
|
|
const data = await readBytes(this.name, offset, length);
|
|
return {
|
|
data: data.buffer as ArrayBuffer,
|
|
etag: undefined,
|
|
cacheControl: undefined,
|
|
expires: undefined
|
|
}
|
|
}
|
|
|
|
getKey = () => this.name;
|
|
}
|
|
|
|
interface RequestParameters {
|
|
url: string;
|
|
headers?: unknown;
|
|
method?: "GET" | "POST" | "PUT";
|
|
body?: string;
|
|
type?: "string" | "json" | "arrayBuffer" | "image";
|
|
credentials?: "same-origin" | "include";
|
|
collectResourceTiming?: boolean;
|
|
};
|
|
|
|
export class Protocol {
|
|
/** @hidden */
|
|
tiles: Map<string, PMTiles>;
|
|
metadata: boolean;
|
|
errorOnMissingTile: boolean;
|
|
|
|
/**
|
|
* Initialize the MapLibre PMTiles protocol.
|
|
*
|
|
* * metadata: also load the metadata section of the PMTiles. required for some "inspect" functionality
|
|
* and to automatically populate the map attribution. Requires an extra HTTP request.
|
|
* * errorOnMissingTile: When a vector MVT tile is missing from the archive, raise an error instead of
|
|
* returning the empty array. Not recommended. This is only to reproduce the behavior of ZXY tile APIs
|
|
* which some applications depend on when overzooming.
|
|
*/
|
|
constructor(options?: { metadata?: boolean; errorOnMissingTile?: boolean }) {
|
|
this.tiles = new Map<string, PMTiles>();
|
|
this.metadata = options?.metadata || false;
|
|
this.errorOnMissingTile = options?.errorOnMissingTile || false;
|
|
}
|
|
|
|
/**
|
|
* Add a {@link PMTiles} instance to the global protocol instance.
|
|
*
|
|
* For remote fetch sources, references in MapLibre styles like pmtiles://http://...
|
|
* will resolve to the same instance if the URLs match.
|
|
*/
|
|
add(p: PMTiles) {
|
|
this.tiles.set(p.source.getKey(), p);
|
|
}
|
|
|
|
/**
|
|
* Fetch a {@link PMTiles} instance by URL, for remote PMTiles instances.
|
|
*/
|
|
get(url: string) {
|
|
return this.tiles.get(url);
|
|
}
|
|
|
|
/** @hidden */
|
|
tilev4 = async (
|
|
params: RequestParameters,
|
|
abortController: AbortController
|
|
) => {
|
|
if (params.type === "json") {
|
|
const pmtilesUrl = params.url.substr(10);
|
|
let instance = this.tiles.get(pmtilesUrl);
|
|
if (!instance) {
|
|
instance = new PMTiles(new FSSource(pmtilesUrl));
|
|
this.tiles.set(pmtilesUrl, instance);
|
|
}
|
|
|
|
if (this.metadata) {
|
|
return {
|
|
data: await instance.getTileJson(params.url),
|
|
};
|
|
}
|
|
|
|
const h = await instance.getHeader();
|
|
|
|
if (h.minLon >= h.maxLon || h.minLat >= h.maxLat) {
|
|
console.error(
|
|
`Bounds of PMTiles archive ${h.minLon},${h.minLat},${h.maxLon},${h.maxLat} are not valid.`
|
|
);
|
|
}
|
|
|
|
return {
|
|
data: {
|
|
tiles: [`${params.url}/{z}/{x}/{y}`],
|
|
minzoom: h.minZoom,
|
|
maxzoom: h.maxZoom,
|
|
bounds: [h.minLon, h.minLat, h.maxLon, h.maxLat],
|
|
},
|
|
};
|
|
}
|
|
const re = new RegExp(/pmtiles:\/\/(.+)\/(\d+)\/(\d+)\/(\d+)/);
|
|
const result = params.url.match(re);
|
|
if (!result) {
|
|
throw new Error("Invalid PMTiles protocol URL");
|
|
}
|
|
const pmtilesUrl = result[1];
|
|
|
|
let instance = this.tiles.get(pmtilesUrl);
|
|
if (!instance) {
|
|
instance = new PMTiles(pmtilesUrl);
|
|
this.tiles.set(pmtilesUrl, instance);
|
|
}
|
|
const z = result[2];
|
|
const x = result[3];
|
|
const y = result[4];
|
|
|
|
const header = await instance.getHeader();
|
|
const resp = await instance?.getZxy(+z, +x, +y, abortController.signal);
|
|
if (resp) {
|
|
return {
|
|
data: new Uint8Array(resp.data),
|
|
cacheControl: resp.cacheControl,
|
|
expires: resp.expires,
|
|
};
|
|
}
|
|
if (header.tileType === TileType.Mvt) {
|
|
if (this.errorOnMissingTile) {
|
|
throw new Error("Tile not found.");
|
|
}
|
|
return { data: new Uint8Array() };
|
|
}
|
|
return { data: null };
|
|
};
|
|
}
|
|
|
|
export const protocol = new Protocol({
|
|
metadata: true,
|
|
errorOnMissingTile: false,
|
|
}).tilev4;
|