feat: improve offline tiles and add settings page
This commit is contained in:
@ -10,8 +10,9 @@
|
||||
import { location } from "./location.svelte";
|
||||
import { saved } from "$lib/saved.svelte";
|
||||
import RoutingLayers from "$lib/services/navigation/RoutingLayers.svelte";
|
||||
import { protocol } from "$lib/services/OfflineTiles";
|
||||
import { getPMTilesURL, hasPMTiles, protocol } from "$lib/services/OfflineTiles";
|
||||
import { layers, worldLayers } from "$lib/mapLayers";
|
||||
import { PMTilesProtocol } from "svelte-maplibre-gl/pmtiles";
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener("resize", map.updateMapPadding);
|
||||
@ -28,8 +29,8 @@
|
||||
</script>
|
||||
|
||||
<!-- <Protocol scheme="tiles" loadFn={protocol} /> -->
|
||||
<!-- <PMTilesProtocol /> -->
|
||||
<Protocol scheme="pmtiles" loadFn={protocol} />
|
||||
<PMTilesProtocol />
|
||||
<Protocol scheme="tiles" loadFn={protocol} />
|
||||
|
||||
<MapLibre
|
||||
class="w-full h-full"
|
||||
@ -47,7 +48,7 @@
|
||||
// if(worldUrl) {
|
||||
map.value!.addSource("ne2_shaded", { // TODO: rename to world
|
||||
type: "vector",
|
||||
url: "pmtiles://world",
|
||||
url: await getPMTilesURL("world"),
|
||||
attribution: "Natural Earth",
|
||||
// maxzoom: 6
|
||||
})
|
||||
@ -61,7 +62,7 @@
|
||||
// if(url) {
|
||||
map.value!.addSource("openmaptiles", {
|
||||
type: "vector",
|
||||
url: "pmtiles://tiles"
|
||||
url: await hasPMTiles("tiles") ? await getPMTilesURL("tiles") : "pmtiles://https://trafficcue-tiles.picoscratch.de/germany.pmtiles"
|
||||
})
|
||||
|
||||
|
||||
|
@ -29,6 +29,9 @@
|
||||
import InRouteSidebar from "./sidebar/InRouteSidebar.svelte";
|
||||
import say from "$lib/services/navigation/TTS";
|
||||
import { downloadPMTiles } from "$lib/services/OfflineTiles";
|
||||
import SettingsSidebar from "./sidebar/settings/SettingsSidebar.svelte";
|
||||
import AboutSidebar from "./sidebar/settings/AboutSidebar.svelte";
|
||||
import OfflineMapsSidebar from "./sidebar/settings/OfflineMapsSidebar.svelte";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const views: Record<string, Component<any>> = {
|
||||
@ -38,6 +41,9 @@
|
||||
trip: TripSidebar,
|
||||
search: SearchSidebar,
|
||||
user: UserSidebar,
|
||||
settings: SettingsSidebar,
|
||||
about: AboutSidebar,
|
||||
"offline-maps": OfflineMapsSidebar
|
||||
};
|
||||
|
||||
let isDragging = false;
|
||||
@ -156,7 +162,9 @@
|
||||
<UserIcon />
|
||||
</button>
|
||||
</RequiresCapability>
|
||||
<button>
|
||||
<button onclick={() => {
|
||||
view.switch("settings");
|
||||
}}>
|
||||
<SettingsIcon />
|
||||
</button>
|
||||
<!-- <button onclick={() => {
|
||||
|
15
src/lib/components/lnv/sidebar/settings/AboutSidebar.svelte
Normal file
15
src/lib/components/lnv/sidebar/settings/AboutSidebar.svelte
Normal file
@ -0,0 +1,15 @@
|
||||
<script>
|
||||
import SidebarHeader from "../SidebarHeader.svelte";
|
||||
</script>
|
||||
|
||||
<SidebarHeader>
|
||||
About
|
||||
</SidebarHeader>
|
||||
|
||||
<h1 style="font-size: 2em; font-weight: bold;">TrafficCue</h1>
|
||||
<span>Powered by:</span>
|
||||
<ul>
|
||||
<li>© OpenStreetMap contributors</li>
|
||||
<li>Natural Earth</li>
|
||||
<li>MapLibre</li>
|
||||
</ul>
|
@ -0,0 +1,32 @@
|
||||
<script>
|
||||
import { downloadPMTiles, getRemoteList } from "$lib/services/OfflineTiles";
|
||||
import { DownloadCloudIcon } from "@lucide/svelte";
|
||||
import SettingsButton from "./SettingsButton.svelte";
|
||||
import SidebarHeader from "../SidebarHeader.svelte";
|
||||
</script>
|
||||
|
||||
<SidebarHeader>
|
||||
Offline Maps
|
||||
</SidebarHeader>
|
||||
|
||||
{#await getRemoteList()}
|
||||
<p>Loading...</p>
|
||||
{:then list}
|
||||
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||
{#if list.length === 0}
|
||||
<p>No offline maps available.</p>
|
||||
{/if}
|
||||
|
||||
{#if !window.__TAURI__}
|
||||
<p>Offline maps are only available on mobile.</p>
|
||||
{/if}
|
||||
|
||||
{#each list as item, _index (item.file)}
|
||||
<SettingsButton disabled={!window.__TAURI__} icon={DownloadCloudIcon} text={item.name} onclick={async () => {
|
||||
await downloadPMTiles("https://trafficcue-tiles.picoscratch.de/" + item.file, item.name.includes("World") ? "world": "tiles");
|
||||
alert(`Downloaded ${item.name}`);
|
||||
location.reload();
|
||||
}} />
|
||||
{/each}
|
||||
</div>
|
||||
{/await}
|
@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import type { IconProps } from "@lucide/svelte";
|
||||
import type { Component } from "svelte";
|
||||
import { view } from "../../view.svelte";
|
||||
|
||||
const { icon: Icon, text, view: viewName, onclick, disabled }: { icon: Component<IconProps>, text: string, view?: string, onclick?: () => void, disabled?: boolean } = $props();
|
||||
</script>
|
||||
|
||||
<Button variant="secondary" style="width: 100%; height: 40px;" {disabled} onclick={() => {
|
||||
if(viewName) view.switch(viewName)
|
||||
if(onclick) onclick();
|
||||
}}>
|
||||
<Icon />
|
||||
{text}
|
||||
</Button>
|
@ -0,0 +1,42 @@
|
||||
<script>
|
||||
import { CodeIcon, InfoIcon, LanguagesIcon, MapIcon, PaintbrushIcon } from "@lucide/svelte";
|
||||
import SidebarHeader from "../SidebarHeader.svelte";
|
||||
import SettingsButton from "./SettingsButton.svelte";
|
||||
</script>
|
||||
|
||||
<SidebarHeader>
|
||||
Settings
|
||||
</SidebarHeader>
|
||||
|
||||
<div id="sections">
|
||||
<section>
|
||||
<h2>General</h2>
|
||||
<SettingsButton icon={LanguagesIcon} text="Language" disabled />
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Map</h2>
|
||||
<SettingsButton icon={MapIcon} text="Offline Maps" view="offline-maps" />
|
||||
<SettingsButton icon={PaintbrushIcon} text="Map Style" disabled />
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>About</h2>
|
||||
<SettingsButton icon={CodeIcon} text="Developer Settings" disabled />
|
||||
<SettingsButton icon={InfoIcon} text="About" view="about" />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
#sections {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
</style>
|
@ -37,7 +37,15 @@ export async function downloadPMTiles(url: string, name: string): Promise<void>
|
||||
console.log(`Download completed: ${path}`);
|
||||
}
|
||||
|
||||
export async function getRemoteList(): Promise<{ name: string, file: string }[]> {
|
||||
return await fetch("https://trafficcue-tiles.picoscratch.de/index.json").then(res => res.json());
|
||||
}
|
||||
|
||||
export async function hasPMTiles(name: string): Promise<boolean> {
|
||||
if(!window.__TAURI__) {
|
||||
return false; // Tauri environment is not available
|
||||
}
|
||||
|
||||
const filename = name + ".pmtiles";
|
||||
const baseDir = BaseDirectory.AppData;
|
||||
const appDataDirPath = await appDataDir();
|
||||
@ -50,23 +58,27 @@ export async function hasPMTiles(name: string): Promise<boolean> {
|
||||
return await exists(filePath, { baseDir });
|
||||
}
|
||||
|
||||
export async function getPMTiles(name: string) {
|
||||
export async function getPMTilesURL(name: string) {
|
||||
if(!window.__TAURI__) {
|
||||
return `pmtiles://https://trafficcue-tiles.picoscratch.de/${name}.pmtiles`;
|
||||
}
|
||||
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.");
|
||||
return `pmtiles://https://trafficcue-tiles.picoscratch.de/${name}.pmtiles`;
|
||||
// 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 `pmtiles://https://trafficcue-tiles.picoscratch.de/${name}.pmtiles`;
|
||||
// throw new Error(`PMTiles file not found: ${filePath}`);
|
||||
}
|
||||
|
||||
return `asset:/${filename}`;
|
||||
// return convertFileSrc(filePath);
|
||||
|
||||
return `tiles://${name}`;
|
||||
}
|
||||
|
||||
async function readBytes(name: string, offset: number, length: number): Promise<Uint8Array> {
|
||||
@ -152,7 +164,7 @@ export class Protocol {
|
||||
abortController: AbortController
|
||||
) => {
|
||||
if (params.type === "json") {
|
||||
const pmtilesUrl = params.url.substr(10);
|
||||
const pmtilesUrl = params.url.substr(8);
|
||||
let instance = this.tiles.get(pmtilesUrl);
|
||||
if (!instance) {
|
||||
instance = new PMTiles(new FSSource(pmtilesUrl));
|
||||
@ -182,10 +194,10 @@ export class Protocol {
|
||||
},
|
||||
};
|
||||
}
|
||||
const re = new RegExp(/pmtiles:\/\/(.+)\/(\d+)\/(\d+)\/(\d+)/);
|
||||
const re = new RegExp(/tiles:\/\/(.+)/);
|
||||
const result = params.url.match(re);
|
||||
if (!result) {
|
||||
throw new Error("Invalid PMTiles protocol URL");
|
||||
throw new Error("Invalid Tiles protocol URL");
|
||||
}
|
||||
const pmtilesUrl = result[1];
|
||||
|
||||
|
Reference in New Issue
Block a user