This repository has been archived on 2025-11-09. You can view files and clone it, but cannot push or open issues or pull requests.
Files
trafficcue-client/src/lib/services/OfflineTiles.ts
Cfp 3bcd7cdade
Some checks failed
TrafficCue CI / check (push) Failing after 56s
TrafficCue CI / build (push) Successful in 48s
fix: keyboard overlapping with controls
2025-08-10 14:48:49 +02:00

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;